Files
Qiniu/technical/k8s/k8s-04-deployment-and-replicaset.md
T

25 KiB
Raw Blame History

tags, create time
tags create time
k8s
deployment
replicaset
devops
2026-07-07 15:48

Deployment 与 ReplicaSet

概述

Deployment 是 Kubernetes 中最常用的无状态工作负载控制器,它在 ReplicaSet 之上提供了声明式更新、滚动发布、版本回滚等高级能力。日常开发中,绝大多数无状态服务(Web API、微服务后端、定时任务 Worker)都通过 Deployment 来管理 Pod 的生命周期。理解 Deployment 与 ReplicaSet 的协作关系,是掌握 Kubernetes 应用交付模型的基础。

ReplicaSet 详解

什么是 ReplicaSet

ReplicaSet(简称 RS)的核心职责只有一件事:确保指定数量的 Pod 副本始终处于运行状态。如果某个 Pod 意外退出或被删除,RS 会立即创建新的 Pod 来弥补差额;如果 Pod 数量超过期望值,RS 会删除多余的 Pod。

# 一个独立的 ReplicaSet 示例(通常不直接创建,由 Deployment 自动管理)
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx-rs
  namespace: default
spec:
  replicas: 3                    # 期望副本数
  selector:
    matchLabels:
      app: nginx                 # Label Selector:通过标签匹配 Pod
  template:
    metadata:
      labels:
        app: nginx               # Pod 必须携带此标签才能被 RS 管理
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80

Label Selector 机制

RS 通过 Label Selector 来发现和管理 Pod,而非维护一个固定的 Pod 列表。这意味着:

  • 手动创建的 Pod 只要标签匹配,也会被 RS 纳入管理(数量超出则删除,不足则新建)
  • 修改某个 Pod 的标签可以将其从 RS 管理中"摘除",这在调试时非常有用
# 将一个 Pod 从 RS 中移除(不删除 Pod,仅修改标签使其脱离管理)
kubectl label pod nginx-rs-xxxx app=detached --overwrite

# 此时 RS 会发现少了一个副本,自动新建一个 Pod 来补足

[!tip] 调试技巧 需要排查某个 Pod 问题时,先把它从 RS 中"摘出来"(修改标签),这样 RS 不会因为 Pod 异常而自动重建,你可以从容排查。

ReplicaSet vs ReplicationController

ReplicationController(RC)是 RS 的前身,两者核心区别在于 Selector 语义:

对比维度 ReplicationController ReplicaSet
Selector 类型 仅支持等式(env=prod) 支持等式 + 集合(env in (prod, staging))
标签匹配灵活性 低 高
推荐程度 已废弃,不建议使用 推荐使用
滚动更新 需要 kubectl rolling-update(客户端执行) 由 Deployment 在服务端协调

RC 已经是遗留概念,新资源应统一使用 ReplicaSet。不过实践中你几乎不会直接创建 RS,而是通过 Deployment 间接管理。

Deployment 核心机制

声明式管理

Deployment 采用声明式模型:你只需描述"期望状态"(desired state),Kubernetes 控制面会持续将其与"实际状态"(actual state)做对比,自动执行必要的操作来弥合差距。这个循环叫做 Reconcile Loop(调谐循环)。

graph LR
    A["用户提交 Deployment<br/>(desired state)"] --> B["Deployment Controller"]
    B --> C{"desired == actual ?"}
    C -->|No| D["创建/删除/更新<br/>ReplicaSet / Pod"]
    D --> B
    C -->|Yes| E["状态稳定<br/>不做任何操作"]

工作流程:

  1. 用户通过 kubectl apply 提交 Deployment YAML,声明 replicas: 3、image: nginx:1.25 等期望状态
  2. Deployment Controller 观察到当前没有匹配的 RS,创建一个新的 RS
  3. RS Controller 观察到 Pod 数量为 0,创建 3 个 Pod
  4. 持续监控:如果某个 Pod 崩溃,RS Controller 检测到 actual < desired,立即补充新 Pod
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
  namespace: default
spec:
  replicas: 3                  # 声明:我想要 3 个副本
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25       # 声明:我想用这个镜像
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

滚动更新(Rolling Update)

滚动更新是 Deployment 最核心的价值。当你修改了 Pod 模板(比如升级镜像版本),Deployment 不会一次性杀掉所有旧 Pod,而是逐批替换,在更新过程中始终保持服务可用。

关键参数

参数 默认值 含义
maxSurge 25% 更新过程中允许超出 replicas 的最大 Pod 数量
maxUnavailable 25% 更新过程中允许不可用的最大 Pod 数量

[!info] 参数计算 对于 replicas: 4、maxSurge: 25%、maxUnavailable: 25% 的配置:

  • maxSurge = ceil(4 * 0.25) = 1,最多可以有 5 个 Pod 同时存在
  • maxUnavailable = floor(4 * 0.25) = 1,至少保证 3 个 Pod 可用

这意味着每批替换 1 个 Pod,共需 4 批完成更新。

滚动更新过程

sequenceDiagram
    participant U as 用户
    participant D as Deployment Controller
    participant RS1 as ReplicaSet v1
    participant RS2 as ReplicaSet v2

    U->>D: kubectl apply(image: nginx:1.26)
    D->>RS2: 创建新 RS(image: 1.26, replicas: 0)
    D->>RS2: scale up +1(新建 1 个新 Pod)
    Note over RS1,RS2: maxSurge=1, 新 Pod 启动
    D->>RS1: scale down -1(销毁 1 个旧 Pod)
    Note over RS1,RS2: maxUnavailable=1, 保证可用
    D->>RS2: scale up +1
    D->>RS1: scale down -1
    Note over RS1,RS2: 重复直到 RS1=0, RS2=4
    D->>RS1: 保留 RS1(revision=1, 可回滚)

逐批替换的过程(以 replicas: 4 为例):

阶段 RS v1 (旧) RS v2 (新) 总 Pod 数 可用 Pod
初始 4 0 4 4
第 1 批 4 → 3 0 → 1 5 4
第 2 批 3 → 2 1 → 2 4 4
第 3 批 2 → 1 2 → 3 4 4
第 4 批 1 → 0 3 → 4 4 4

滚动更新配置

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  replicas: 4
  strategy:
    type: RollingUpdate           # 更新策略
    rollingUpdate:
      maxSurge: 1                 # 最多多 1 个 Pod
      maxUnavailable: 1           # 最多少 1 个 Pod
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        readinessProbe:            # 重要:就绪探针决定新 Pod 何时接收流量
          httpGet:
            path: /healthz
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10

[!warning] readinessProbe 的重要性 滚动更新依赖 readinessProbe 判断新 Pod 是否就绪。如果没有配置就绪探针,新 Pod 一创建就被视为 Ready,可能导致流量打到尚未初始化完成的 Pod 上,造成请求失败。

回滚策略

Kubernetes 为每次 Deployment 变更自动记录 revision(版本号),支持快速回滚到任意历史版本。

# 查看 rollout 历史
kubectl rollout history deployment nginx-deploy

# 输出示例:
# REVISION  CHANGE-CAUSE
# 1         <none>
# 2         <none>
# 3         <none>

# 查看某个 revision 的详细信息
kubectl rollout history deployment nginx-deploy --revision=2

# 回滚到上一个版本
kubectl rollout undo deployment nginx-deploy

# 回滚到指定版本
kubectl rollout undo deployment nginx-deploy --to-revision=1

# 查看 rollout 状态(是否完成、耗时等)
kubectl rollout status deployment nginx-deploy

revisionHistoryLimit 配置

默认保留 10 个历史 revision。过多的历史 RS 会占用 etcd 资源,过少则可能丢失回滚能力。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  revisionHistoryLimit: 5        # 只保留最近 5 个 revision
  # 设为 0 则禁用回滚(不推荐)
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
      annotations:
        kubernetes.io/change-cause: "升级到 nginx 1.26"  # 记录变更原因,显示在 rollout history 中
    spec:
      containers:
      - name: nginx
        image: nginx:1.26

[!tip] 记录变更原因 在 Pod template 的 annotations 中添加 kubernetes.io/change-cause,这样 kubectl rollout history 会显示每次变更的原因,方便排查。

扩缩容

手动扩缩容

# 命令行直接扩缩
kubectl scale deployment nginx-deploy --replicas=5

# 条件扩缩(只有当前副本数为 3 时才执行)
kubectl scale deployment nginx-deploy --replicas=5 --current-replicas=3

也可以直接修改 YAML 中的 replicas 字段后 kubectl apply。

HPA 自动扩缩

Horizontal Pod Autoscaler(HPA)根据 CPU、内存或自定义指标自动调整 Deployment 的副本数。

# 前置条件:集群中需要安装 Metrics Server
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nginx-hpa
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nginx-deploy               # 目标 Deployment
  minReplicas: 2                      # 最小副本数
  maxReplicas: 10                     # 最大副本数
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60        # CPU 使用率超过 60% 时扩容
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80        # 内存使用率超过 80% 时扩容
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60  # 扩容稳定窗口:60s 内取最大值
      policies:
      - type: Pods
        value: 4                      # 每次最多扩 4 个 Pod
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300 # 缩容稳定窗口:300s,防止抖动
      policies:
      - type: Percent
        value: 10                     # 每次最多缩 10%
        periodSeconds: 60
# 创建 HPA
kubectl apply -f hpa.yaml

# 或用命令行快速创建
kubectl autoscale deployment nginx-deploy --cpu-percent=60 --min=2 --max=10

# 查看 HPA 状态
kubectl get hpa nginx-hpa

[!warning] HPA 与手动 scale 冲突 如果同时使用 HPA 和手动 kubectl scale,HPA 会在下一次指标采集时覆盖手动设置。两者不应混用。

Deployment 更新策略

Deployment 支持两种更新策略,通过 spec.strategy.type 指定:

对比维度 RollingUpdate Recreate
更新方式 逐批替换 Pod 先杀全部旧 Pod,再创建全部新 Pod
更新期间可用性 始终可用(前提:正确配置探针) 有短暂不可用窗口
资源占用 可能临时需要更多资源(maxSurge > 0 时) 只需要新版本的资源
适用场景 大多数无状态服务 不能有两个版本同时运行的场景
典型用例 Web 服务、API 后端 涉及全局独占资源(如本地文件锁、全局数据库迁移)
# Recreate 策略示例
apiVersion: apps/v1
kind: Deployment
metadata:
  name: legacy-app
spec:
  replicas: 3
  strategy:
    type: Recreate                # 全量替换,更新期间服务不可用
  selector:
    matchLabels:
      app: legacy
  template:
    metadata:
      labels:
        app: legacy
    spec:
      containers:
      - name: app
        image: legacy-app:2.0

[!info] 什么时候用 Recreate? 当新旧版本不能共存时使用 Recreate,比如:应用使用了本地文件锁、需要执行不可逆的数据库 Schema 迁移、或者旧版本会干扰新版本的初始化。大多数情况下 RollingUpdate 是更好的选择。

金丝雀发布与蓝绿部署

Deployment 本身不直接支持金丝雀发布和蓝绿部署,但可以通过组合多个 Deployment + Service 来实现。

金丝雀发布(Canary Release)

核心思路:同时运行两个版本的 Deployment,通过控制副本比例来控制流量分配。

# 主 Deployment:稳定版本,占 90% 流量
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-stable
spec:
  replicas: 9                      # 9 个副本
  selector:
    matchLabels:
      app: myapp
      track: stable
  template:
    metadata:
      labels:
        app: myapp
        track: stable              # 核心标签 app: myapp 相同,Service 可以选中
    spec:
      containers:
      - name: app
        image: myapp:v1.0
---
# 金丝雀 Deployment:新版本,占 10% 流量
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-canary
spec:
  replicas: 1                      # 1 个副本
  selector:
    matchLabels:
      app: myapp
      track: canary
  template:
    metadata:
      labels:
        app: myapp
        track: canary
    spec:
      containers:
      - name: app
        image: myapp:v2.0
# Service 通过 app: myapp 同时选中两个 Deployment 的 Pod
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp                     # 不指定 track,两个版本都会被选中
  ports:
  - port: 80
    targetPort: 8080

流量分配:9 个旧版本 Pod + 1 个新版本 Pod = 约 10% 流量到达新版本。验证无误后,逐步调整副本比例(如 canary: 5, stable: 5),最终全部切换到新版本。

graph LR
    Client --> Service
    Service -->|~90%| Pod1["Pod v1.0 x9"]
    Service -->|~10%| Pod2["Pod v2.0 x1"]

蓝绿部署(Blue-Green Deployment)

核心思路:同时运行蓝(当前版本)和绿(新版本)两套完整的 Deployment,通过切换 Service 的 selector 来瞬间完成流量切换。

# 蓝色 Deployment(当前生产版本)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
      - name: app
        image: myapp:v1.0
---
# 绿色 Deployment(新版本,先部署但不接收流量)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: green
  template:
    metadata:
      labels:
        app: myapp
        version: green
    spec:
      containers:
      - name: app
        image: myapp:v2.0
# Service:切换 selector.version 即可瞬间切换流量
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp
    version: blue               # 当前指向蓝色,切换为 green 即完成上线
  ports:
  - port: 80
    targetPort: 8080
# 切换流量到绿色版本
kubectl patch service myapp-svc -p '{"spec":{"selector":{"version":"green"}}}'

# 回滚:切换回蓝色版本
kubectl patch service myapp-svc -p '{"spec":{"selector":{"version":"blue"}}}'

蓝绿部署的优点是切换瞬间完成(无中间状态),缺点是需要双倍资源。适合对上线/回滚速度要求极高的场景。

Pod 模板字段详解

spec.template.spec 是 Pod 模板的核心,以下是常用字段的逐一说明:

spec:
  template:
    metadata:
      labels:                    # Pod 标签,用于 Service Selector 和 ReplicaSet 匹配
        app: myapp
        version: v1
      annotations:               # 非标识性元数据,供工具或控制器使用
        kubernetes.io/change-cause: "初始部署"
    spec:
      # ---- 容器定义 ----
      containers:
      - name: app                # 容器名称,Pod 内唯一
        image: myapp:v1.0        # 镜像地址
        imagePullPolicy: IfNotPresent  # Always / IfNotPresent / Never
        ports:
        - containerPort: 8080    # 容器监听端口(声明性质,不强制)
        env:                     # 环境变量
        - name: APP_ENV
          value: "production"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:        # 从 Secret 读取敏感信息
              name: db-secret
              key: password
        resources:               # 资源请求与限制
          requests:              # 调度依据:节点至少需要这么多资源
            cpu: 100m
            memory: 128Mi
          limits:                # 上限:超出会被 OOM Kill 或 CPU 限流
            cpu: 500m
            memory: 256Mi
        readinessProbe:          # 就绪探针:决定 Pod 是否接收流量
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:           # 存活探针:失败则重启容器
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        startupProbe:            # 启动探针:慢启动应用专用,通过前忽略 liveness
          httpGet:
            path: /healthz
            port: 8080
          failureThreshold: 30
          periodSeconds: 10

      # ---- 初始化容器 ----
      initContainers:            # 在主容器启动前顺序执行
      - name: init-db-migration
        image: myapp:v1.0
        command: ["./migrate.sh"]

      # ---- 卷挂载 ----
      volumes:
      - name: config-vol
        configMap:
          name: app-config       # 挂载 ConfigMap 作为文件
      - name: data-vol
        emptyDir: {}             # 临时卷,Pod 删除后数据丢失

      # ---- 调度相关 ----
      nodeSelector:              # 简单节点选择
        disk: ssd
      affinity:                  # 高级亲和性规则
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/os
                operator: In
                values: ["linux"]
      tolerations:               # 容忍污点
      - key: "dedicated"
        operator: "Equal"
        value: "gpu"
        effect: "NoSchedule"

      # ---- 其他 ----
      terminationGracePeriodSeconds: 30   # 优雅终止等待时间
      serviceAccountName: myapp-sa        # 使用的 ServiceAccount

[!tip] imagePullPolicy 最佳实践

  • 使用 :latest 标签时,必须设置 imagePullPolicy: Always,否则可能使用本地缓存的旧镜像
  • 使用固定版本标签(如 :v1.2.3)时,IfNotPresent 更高效,避免重复拉取
  • Never 仅用于本地开发环境(如 minikube 中直接使用本地构建的镜像)

常见陷阱与最佳实践

更新卡住排查

滚动更新卡住是常见问题,通常由以下原因导致:

# 1. 查看 Deployment 状态
kubectl rollout status deployment nginx-deploy
# 可能输出:Waiting for rollout to finish: 2 out of 4 new replicas have been updated...

# 2. 查看 Deployment 的 Conditions
kubectl get deployment nginx-deploy -o yaml | grep -A 20 conditions:
# 常见条件:
# - Progressing: False(更新未推进)
# - Available: True/False(是否有足够可用 Pod)

# 3. 查看新 ReplicaSet 的事件
kubectl describe rs nginx-deploy-xxxxxx   # 查看新 RS 的 Events
# 常见原因:
# - FailedScheduling:资源不足,没有节点能满足 requests
# - ImagePullBackOff:镜像拉取失败(镜像名错、私有仓库无凭证)
# - CrashLoopBackOff:新容器启动后立即崩溃

# 4. 查看 Pod 事件
kubectl get pods -l app=nginx --sort-by=.metadata.creationTimestamp
kubectl describe pod nginx-deploy-xxxxxx-yyyyy

常见原因和解决方案:

症状 原因 解决方案
ImagePullBackOff 镜像不存在或无权限 检查镜像名称、imagePullSecrets
CrashLoopBackOff 应用启动失败 查看 Pod 日志 kubectl logs
Pending 资源不足或节点不满足约束 检查 kubectl describe pod 中的 Events
readinessProbe 一直失败 健康检查路径或端口错误 确认探针配置与应用实际端口一致

[!warning] 超时回滚 Deployment 有一个 progressDeadlineSeconds 参数(默认 600 秒),如果更新在该时间内未完成,Deployment 会标记为 Progressing=False。注意:这不会自动回滚,只是标记状态。需要手动执行 kubectl rollout undo 或编写自动化脚本。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deploy
spec:
  progressDeadlineSeconds: 300   # 5 分钟内未完成更新则标记失败
  minReadySeconds: 10            # Pod Ready 后等待 10 秒才视为 Available
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.26

imagePullPolicy 导致的镜像缓存问题

# 场景:修改了 YAML 中的 image 字段但没改 tag,apply 后 Pod 没有更新
# 原因:imagePullPolicy 默认为 IfNotPresent,本地已有旧镜像就不会重新拉取

# 解决方案 1:始终使用不同的 tag(推荐)
image: myapp:v1.0.1   # 而不是 myapp:latest

# 解决方案 2:强制设置 imagePullPolicy
imagePullPolicy: Always

# 解决方案 3:使用 SHA256 digest(最精确)
image: myapp@sha256:abc123...

Deployment vs StatefulSet 选择

特性 Deployment StatefulSet
Pod 名称 随机(nginx-5d6b8f7c9-abc12) 有序(web-0, web-1, web-2)
Pod 启动顺序 并行 有序(0 → 1 → 2)
Pod 终止顺序 并行(默认) 逆序(2 → 1 → 0)
网络标识 不稳定 稳定(web-0.ng-svc)
持久存储 共享或无 每个 Pod 独立 PVC
适用场景 无状态服务 有状态服务(数据库、MQ、ZooKeeper)
滚动更新 替换 Pod 逐个更新

[!info] 选择原则 如果你的应用满足以下任意条件,应考虑 StatefulSet:

  • 需要稳定的网络标识(如 ZooKeeper 集群节点互相发现)
  • 需要持久化的、与 Pod 绑定的存储(如 MySQL 数据目录)
  • 需要有序的部署/扩缩/更新

其余场景一律使用 Deployment。

其他最佳实践

  1. 始终配置资源请求和限制:没有 requests 的 Pod 会被调度器视为零消耗,可能导致多个 Pod 被调度到同一节点,引发资源争抢。

  2. 始终配置 readinessProbe:没有就绪探针,新 Pod 创建后立即被标记为 Ready,流量可能打到未初始化完成的 Pod。

  3. 不要直接管理 Deployment 创建的 ReplicaSet:RS 的生命周期由 Deployment 控制器管理,手动修改 RS(如 scale)会被 Deployment 覆盖。

  4. 使用 kubectl apply 而非 kubectl create:apply 支持声明式管理,可以幂等执行,适合 GitOps 工作流。

  5. 合理设置 minReadySeconds:该参数让 Pod Ready 后再等待一段时间才被视为 Available,可以用来检测"启动后很快崩溃"的场景。

# 推荐的最小 Deployment 模板
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 3
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 300
  minReadySeconds: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0            # 保证更新期间始终有足够的可用 Pod
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: myapp
        image: myapp:v1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20

延伸阅读