可控的线上变更:Deployment 批次部署

wallhaven-6ld8xl

前言

K8S原生Deployment支持滚动更新和重建两种发布模式。

滚动更新模式

Pod升级自动进行,失败自动暂停。通过以下策略控制升级速率:

  • Max Unavailable : 最大不可用实例数/比例
  • Max Surge : 调度过程中,可超过期望实例数的数/比例

然而,由于发布期间无法暂停,导致没有时间验证新版本正确性,仅依赖Pod Ready状态判断过于片面,基本没有灰度能力。因此在生产环境使用非常危险。

重建模式

先删除所有旧版本Pod,再创建新版本Pod,期间服务停机,不适用于在线服务。

对于生产环境,往往需要节奏可控、可灰度、可回滚的发布方式。常见的有批次发布、金丝雀发布、蓝绿发布等策略。本文以批次发布为例,在不改变Workload的情况下,对原生Deployment增强,实现可控的渐进式更新。

注意:本文是以学习探究为目的,如果要在生产环境使用,推荐OpenKruiseArgo Rollouts

流程分析

批次发布流程

批次发布流程.excalidraw

回滚流程

功能需求

批次发布

  • 每批更新的Pod支持指定数量或百分比
  • 批次之间设置卡点,人工确认后继续发布
  • 不涉及流量管理

连续发布

  • 假设当前线上版本为V1,正在发布V2(未完成),支持重新发布V3

回滚

  • 发布期间或发布结束后,可快速回滚到发布前的版本

HPA兼容

  • 批次发布期间发生扩缩容,新旧版本比例满足当前批次比例

概要设计

采用旁路设计,无需修改Deployment Controller代码。新增 BatchRelease CRD、BatchRelease Controller。

核心机制:

  1. 发布时阻止原生Deployment Controller工作
  2. BatchRelease Controller根据发布策略控制ReplicaSet扩缩容
  3. 发布完成后归还控制权

BatchRelease原理.excalidraw

详细设计

阻止原生Deployment Controller工作

为避免BatchRelease Controller和原生Deployment Controller冲突,需要在BatchRelease工作时禁止原生Deployment Controller调谐。

方案:通过设置以下参数实现:

spec:
  paused: true
  strategy:
    type: Recreate

原理分析:

原生Deployment Controller核心调谐逻辑中有以下判断:

// 1. Paused=true 阻止发布和回滚
if d.Spec.Paused {
    return dc.sync(ctx, d, rsList)
}

// 2. 在sync中处理扩缩容
// 但BatchRelease发布期间存在多个活跃ReplicaSet
// FindActiveOrLatest会返回nil,不会执行扩容逻辑
if activeOrLatest := deploymentutil.FindActiveOrLatest(newRS, oldRSs); activeOrLatest != nil {
    // 扩缩容逻辑
}

// 3. Recreate模式阻止按比例扩缩容
if deploymentutil.IsRollingUpdate(deployment) {
    // 按比例扩缩容逻辑
}

总结:

  • deployment.Spec.Paused = true 阻止发布和回滚
  • deployment.Spec.Strategy.Type = "Recreate" 阻止按比例扩缩容
  • BatchRelease发布完成后恢复为 Paused=falseRollingUpdate

BatchRelease状态机设计

BatchRelease状态机-1.excalidraw

状态转换示例:

假设发布策略为 [1, 50%, 100%],Deployment期望副本数为10:

Initial → RollingUpdate
  ├─ Batch 0 分组状态: Initial → Upgrade(1个新Pod,9个老Pod) → Blocking(等待确认) → Completed
  ├─ Batch 1 分组状态: Initial → Upgrade(5个新Pod,5个老Pod) → Blocking(等待确认) → Completed
  └─ Batch 2 分组状态: Initial → Upgrade(10个新Pod) → Completed
→ Finalizing → Completed

BatchRelease CR 设计

精简示例:

apiVersion: rollouts.yuyy.com/v1alpha1
kind: BatchRelease
metadata:
  name: yuyy-nginx
spec:
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: yuyy-nginx
  strategy:
    steps:
    - replicas: 1      # 第一批: 1个Pod
    - replicas: 10%    # 第二批: 10%
    - replicas: 30%    # 第三批: 30%
    - replicas: 100%   # 第四批: 全部
  template:
    spec:
      containers:
      - name: nginx
        image: nginx:1.15
status:
  phase: Completed
  currentStepIndex: 3
  currentStepState: Completed
  observedUpdateReversion: "5c66667f6"  # 当前发布版本Hash

关键字段说明:

  • spec.strategy.steps: 发布批次策略,支持数字或百分比
  • spec.template: 发布的Pod模板
  • status.phase: 当前发布阶段
  • status.currentStepIndex: 当前批次索引
  • status.currentStepState: 当前批次状态

完整CR示例见附录。

实现批次发布

1. Initial 阶段

职责:

  1. 初始化 BatchRelease Status
  2. 记录当前发布的 PodTemplate Hash 到 status.observedUpdateReversion
  3. 更新 Deployment,阻止原生 Deployment Controller 调谐

核心代码逻辑:

// 接管Deployment控制权
d.Spec.Template = br.Spec.Template
d.Spec.Paused = true
d.Spec.Strategy.Type = apps.RecreateDeploymentStrategyType
d.Annotations[BatchReleaseControlInfoAnno] = controllerRef

2. RollingUpdate 阶段

职责:

  1. 优先处理扩缩容事件,按当前批次比例扩缩容
  2. 对当前批次进行滚动更新

滚动更新策略:

  1. 先扩容新版本 ReplicaSet,直到达到 maxSurge 限制
  2. 再缩容旧版本 ReplicaSet,受 maxUnavailable 控制
  3. 满足当前批次 partition 后进入 Blocking 状态

扩缩容兼容:

// 检测扩缩容事件
for rs := range activeReplicaSets {
    if rs.Annotations["desired-replicas"] != deployment.Spec.Replicas {
        return true // 发生了扩缩容
    }
}

// 按当前批次比例进行扩缩容
newRSReplicasLimit := NewRSReplicasLimit(currentPartition, deployment)
oldRSReplicasLimit := deployment.Spec.Replicas - newRSReplicasLimit

3. Finalizing 阶段

职责:

  1. 归还 Deployment 控制权
  2. 记录版本快照到 Annotation

核心代码逻辑:

// 归还Deployment控制权
d.Spec.Paused = false
d.Spec.Strategy.Type = apps.RollingUpdateDeploymentStrategyType
d.Spec.Strategy.RollingUpdate = &RollingUpdateDeployment{
    MaxSurge: br.Status.MaxSurge,
    MaxUnavailable: br.Status.MaxUnavailable,
}
delete(d.Annotations, BatchReleaseControlInfoAnno)

4. Completed 阶段

职责:

  1. 等待下一次发布或回滚

实现回滚

BatchRelease回滚流程.excalidraw

版本记录机制

控制器在 BatchRelease 的 Annotation 中维护版本快照:

Annotation 说明
current-stable-reversion 当前稳定版本的 Pod 模板 Hash
current-stable-reversion-raw 当前稳定版本的 Pod 模板完整 YAML
last-stable-reversion 上一个稳定版本的 Pod 模板 Hash
last-stable-reversion-raw 上一个稳定版本的 Pod 模板完整 YAML

更新时机: Finalize阶段更新

// last <- current
br.Annotations["last-stable-reversion"] = br.Annotations["current-stable-reversion"]
br.Annotations["last-stable-reversion-raw"] = br.Annotations["current-stable-reversion-raw"]

// current <- new
br.Annotations["current-stable-reversion"] = newHash
br.Annotations["current-stable-reversion-raw"] = yaml.Marshal(newTemplate)

触发回滚

用户添加 Annotation rollback: "true" 触发回滚:

kubectl annotate batchrelease yuyy-nginx rollback=true

回滚场景:

  1. 发布期间回滚

    • 使用 current-stable-reversion-raw 的 PodTemplate
    • 回滚到发布前的稳定版本
  2. 发布完成后回滚

    • 使用 last-stable-reversion-raw 的 PodTemplate
    • 回滚到上一个稳定版本

快速回滚策略:

为快速止损,回滚时强制使用 [1, 100%] 策略:

  • 第一批: 1个Pod,用于灰度验证
  • 第二批: 全部Pod
if isRollback {
    br.Spec.Strategy.Steps = []Step{
        {Replicas: intstr.Parse("1")},
        {Replicas: intstr.Parse("100%")},
    }
}

实现连续发布

场景: V1 → V2(进行中) → V3

处理逻辑:

// 每次调谐时检查 PodTemplate 是否变化
currentHash := ComputeHash(br.Spec.Template)
if br.Status.ObservedUpdateReversion != currentHash {
    // 重新进入 Initial 阶段
    br.Status.Phase = PhaseInitial
}

效果: 自动中断V2发布,开始V3发布

使用示例

1. 创建 Deployment

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: yuyy-nginx
spec:
  replicas: 10
  selector:
    matchLabels:
      app: yuyy-nginx
  template:
    metadata:
      labels:
        app: yuyy-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
EOF

2. 创建 BatchRelease 开始发布

kubectl apply -f - <<EOF
apiVersion: rollouts.yuyy.com/v1alpha1
kind: BatchRelease
metadata:
  name: yuyy-nginx
spec:
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: yuyy-nginx
  strategy:
    steps:
    - replicas: 1
    - replicas: 30%
    - replicas: 100%
  template:
    metadata:
      labels:
        app: yuyy-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.15  # 更新镜像
        ports:
        - containerPort: 80
EOF

3. 查看发布状态

# 查看 BatchRelease 状态
kubectl get batchrelease yuyy-nginx
# NAME         PHASE           INDEX   STATE       REASON
# yuyy-nginx   RollingUpdate   0       Blocking    StepBlocking

# 查看 Pod 状态
kubectl get pods -l app=yuyy-nginx
# 会看到 1 个新版本 Pod (nginx:1.15) 和 9 个旧版本 Pod

4. 继续发布下一批次

当前批次验证通过后,继续发布:

# 修改 currentStepState 为 Completed 触发下一批次
kubectl patch batchrelease yuyy-nginx --type='merge' --subresource='status' -p '{"status":{"currentStepState":"Completed"}}'

5. 回滚操作

发现问题需要回滚:

kubectl annotate batchrelease yuyy-nginx rollback=true

总结

本项目通过旁路设计方式,在不修改 Kubernetes 原生 Deployment Controller 的前提下,实现了:

  • 批次发布 - 支持自定义批次策略,每批支持数字或百分比
  • 人工卡点 - 每批完成后暂停,等待确认
  • 快速回滚 - 发布期间或完成后可快速回滚
  • 连续发布 - 支持发布中断并开始新版本发布
  • HPA兼容 - 发布期间扩缩容保持批次比例

核心技术要点:

  1. 通过 Paused=true + Strategy.Type=Recreate 阻止原生Deployment Controller 调谐
  2. 状态机驱动发布流程
  3. Annotation 存储版本快照实现回滚
  4. 兼容原生 Deployment 的扩缩容和滚动更新策略

完整代码: https://github.com/EchoGroot/batch-release.git


附录

附录A: 原生Deployment Controller滚动更新原理

基于 Kubernetes v1.27.0 源码分析

入口

// cmd/kube-controller-manager/app/controllermanager.go:450
register("deployment", startDeploymentController)

核心调谐逻辑

// pkg/controller/deployment/deployment_controller.go:581
func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error {
    // 1. 获取所有ReplicaSet
    rsList, err := dc.getReplicaSetsForDeployment(ctx, d)

    // 2. 处理删除
    if d.DeletionTimestamp != nil {
        return dc.syncStatusOnly(ctx, d, rsList)
    }

    // 3. 处理暂停
    if d.Spec.Paused {
        return dc.sync(ctx, d, rsList)
    }

    // 4. 处理回滚
    if getRollbackTo(d) != nil {
        return dc.rollback(ctx, d, rsList)
    }

    // 5. 处理扩缩容
    if isScalingEvent {
        return dc.sync(ctx, d, rsList)
    }

    // 6. 处理发布
    switch d.Spec.Strategy.Type {
    case apps.RecreateDeploymentStrategyType:
        return dc.rolloutRecreate(ctx, d, rsList, podMap)
    case apps.RollingUpdateDeploymentStrategyType:
        return dc.rolloutRolling(ctx, d, rsList)
    }
}

滚动更新逻辑

// pkg/controller/deployment/rolling.go:32
func (dc *DeploymentController) rolloutRolling(ctx, d, rsList) error {
    newRS, oldRSs, _ := dc.getAllReplicaSetsAndSyncRevision(ctx, d, rsList, true)

    // 1. 先扩容新版本
    scaledUp, _ := dc.reconcileNewReplicaSet(ctx, allRSs, newRS, d)
    if scaledUp {
        return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
    }

    // 2. 再缩容旧版本
    scaledDown, _ := dc.reconcileOldReplicaSets(ctx, allRSs, oldRSs, newRS, d)
    if scaledDown {
        return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
    }

    // 3. 清理Replicaset
    if deploymentutil.DeploymentComplete(d, &d.Status) {
        dc.cleanupDeployment(ctx, oldRSs, d)
    }

    return dc.syncRolloutStatus(ctx, allRSs, newRS, d)
}

扩容计算:

maxSurge := deployment.Spec.Strategy.RollingUpdate.MaxSurge
maxReplicas := deployment.Spec.Replicas + maxSurge

缩容计算:

maxUnavailable := deployment.Spec.Strategy.RollingUpdate.MaxUnavailable
minAvailable := deployment.Spec.Replicas - maxUnavailable
newRSUnavailablePodCount := newRS.Spec.Replicas - newRS.Status.AvailableReplicas
maxScaledDown := allPodsCount - minAvailable - newRSUnavailablePodCount

附录B: BatchRelease CR 完整示例

apiVersion: rollouts.yuyy.com/v1alpha1
kind: BatchRelease
metadata:
  annotations:
    current-stable-reversion: 5c66667f6
    current-stable-reversion-raw: |
      metadata:
        creationTimestamp: null
        labels:
          app: yuyy-nginx
      spec:
        containers:
        - image: nginx:1.15
          name: nginx
          ports:
          - containerPort: 80
    last-stable-reversion: 6d99c799f9
    last-stable-reversion-raw: |
      metadata:
        creationTimestamp: null
        labels:
          app: yuyy-nginx
      spec:
        containers:
        - image: nginx:1.14.2
          name: nginx
          ports:
          - containerPort: 80
  name: yuyy-nginx
  namespace: default
spec:
  strategy:
    steps:
    - replicas: 1
    - replicas: 10%
    - replicas: 30%
    - replicas: 60%
    - replicas: 100%
  template:
    metadata:
      labels:
        app: yuyy-nginx
    spec:
      containers:
      - image: nginx:1.15
        name: nginx
        ports:
        - containerPort: 80
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: yuyy-nginx
status:
  phase: Completed
  currentStepIndex: 4
  currentStepState: Completed
  lastUpdateTime: "2025-12-07T09:19:17Z"
  maxSurge: 25%
  maxUnavailable: 25%
  message: ""
  observedGeneration: 7
  observedUpdateReversion: 5c66667f6
  reason: ""
  updatedReadyReplicas: 10
作者:Yuyy
博客:https://yuyy.info
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇