ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Deployment更新策略:Recreate与RollingUpdate详解

Deployment更新策略:Recreate与RollingUpdate详解 不吹不黑Deployment 应该是 Kubernetes 里被用得最多的工作负载了。平时创建 Deployment、升级镜像、回滚版本命令看似简单但[更新策略](就藏在spec.strategy里很多人却一直没认真研究过。等到线上流量高峰想要安全发布或者遇到必须停服维护的服务才发现 Recreate 和 RollingUpdate 的差异远不止“停机”和“不停机”这么简单。这篇文章想把我自己生产环境里折腾 Deployment 更新策略的经验整理出来重点拆解这两种模式的工作原理、核心参数的计算逻辑、适用场景以及我踩过的坑。覆盖的读者范围很广刚入门 K8s 的运维和开发能看懂机制已在使用 Deployment 但没细抠过策略细节的人也能找到可落地的参数组合和排查思路。1. 更新策略背后的设计逻辑1.1 Deployment 不是直接管理 Pod 的很多新手容易产生一个误解认为 Deployment 直接管理着一堆 Pod。实际上 Deployment 和 Pod 之间还夹着一层 ReplicaSet。Deployment 只负责声明期望状态比如“我要 3 个副本、镜像 nginx:1.25”真正去保证 3 个 Pod 存在的是 ReplicaSet。这个模型看似绕了一圈其实是最精妙的设计。当 Deployment 的 Pod 模板发生变化比如镜像 tag 从 1.25 改成 1.26控制器会新建一个 ReplicaSet然后按照合适的节奏调整新旧两个 ReplicaSet 的副本数而不是直接去操作 Pod。Pod 在这里面只是 ReplicaSet 的附属物。理解了这三层关系后面看 Recreate 和 RollingUpdate 的差异就不会懵了。注意因为 Deployment 的滚动更新是通过 ReplicaSet 的伸缩实现的所以 kubectl rollout history 能看到的版本记录本质上就是一个个 ReplicaSet 的存档。这个特性后面排查问题时非常有用。1.2 更新策略到底在权衡什么把更新策略拆开看事情就清楚多了。任何一次镜像升级本质上都要经历“旧 Pod 终止”和“新 Pod 创建”这两个事件。这两个事件的发生顺序和重叠程度决定了更新期间服务是否中断、是否会有两个版本并存、资源消耗会有多高。先停后建把旧 Pod 全部停掉再创建新 Pod。优点是逻辑简单新旧版本绝不共存缺点是中间必然有一段空窗期。边建边停先建一部分新 Pod就绪后再删对应的旧 Pod反复推进。优点是无抖动地完成替换缺点是新旧版本在过渡期会短暂共存并且需要额外的资源承载新 Pod。Kubernetes 把这两条路分别封装成了 Recreate 和 RollingUpdate。没有哪一种绝对正确只有适合当前业务的哪一种。比如一个内部定时任务系统跑完任务就退出你让它滚动更新反而不合适一个面向用户的 Web 服务更新时如果出现 5 分钟空白估计马上就会被投诉淹没。2. Recreate 策略全停全开的停机更新2.1 Recreate 运行时发生了什么spec 里这样声明spec: replicas: 3 strategy: type: Recreate当 Pod 模板发生变化Deployment 控制器做的第一件事是把旧 ReplicaSet 的副本数缩到 0等所有旧 Pod 都彻底终止再去创建新 ReplicaSet并把新副本数拉伸到 3。整个过程可以脑补成“教室里先清空所有人再放下一批学生进来”。这里有个容易被忽略的细节控制器会等旧 Pod 完全消失而不是只要进入 Terminating 就算完。如果 Pod 设置了 terminationGracePeriodSeconds或者有 persistentVolumeClaim 正在卸载终止过程会被拉长。这意味着 Recreate 的停机时间不是“新 Pod 启动时间”而是“旧 Pod 终止时间 新 Pod 启动到就绪时间”两段相加。我见过很多人在割接时只预留了新镜像启动的时间忽略了旧 Pod 优雅终止的耗时最后在实际切换时超出了预期。2.2 什么业务适合 Recreate建议不要把 Recreate 一棍子打死它在一些特定场景里反而是最稳妥的选择。第一类场景是数据库版本升级。比如某个库要从 MySQL 5.7 升到 8.0或者业务依赖的表结构做了不兼容变更。如果新旧版本同时运行两个版本可能同时链接同一个库写入不同格式的数据这往往是灾难。Recreate 能保证整个生命周期内只有一个版本在跑风险边界非常清晰。第二类场景是单副本应用。如果本来 replicas 就是 1RollingUpdate 和 Recreate 的表现几乎一样反而 Recreate 的逻辑更直观不会出现 maxSurge 临时拉起多余副本的情况。第三类场景是测试或预发环境。这些环境对停机不敏感但需要保证环境整洁Recreate 可以很好地避免新旧 Pod 残留造成的脏数据。2.3 Recreate 的代价在哪里代价非常直接整个服务的可用窗口完全中断。从用户视角来看流量会打到空负载均衡后端上返回 502 或连接拒绝。如果是一个定时任务平台任务流会断档如果是消息消费服务队列中积压的数量会直线上升。另外Recreate 其实比 RollingUpdate 更依赖调度器的瞬时资源。因为所有旧 Pod 都删了新 Pod 需要在同一刻获得大量资源。如果集群本身没有空闲容量新 Pod 会长时间 Pending停机会从“秒级”拉长到“分钟级”。我在一个混合部署的集群里遇到过这种尴尬节点上既有在线业务又有离线任务Recreate 以后离线任务瞬间占了资源新的在线 Pod 等了 5 分钟才调度上去比预期停机时间多了好几倍。实操建议选择 Recreate 之前先看两个数字旧 Pod 平均终止耗时kubectl get events 里能看到和新 Pod 从启动到 Ready 的历史时长。两者相加就是你实打实的停机窗口。3. RollingUpdate 策略滚动替换的节奏控制3.1 RollingUpdate 的工作流程spec.strategy.type设为 RollingUpdate 之后可以再往下配置两个核心参数maxUnavailable和maxSurge。默认值各是 25%。更新过程大体是这样的假设有 10 个旧副本maxSurge 和 maxUnavailable 都是 25%。控制器计算得到最多允许 3 个不可用、最多允许超出 3 个。它会先把新 ReplicaSet 扩大到 3满足 surge等这 3 个新 Pod 就绪再把旧 ReplicaSet 缩掉 3满足 unavailable然后再把新 ReplicaSet 扩大到 6再缩旧到 7……反复进行直到新旧副本数完成交换。这个节奏初看像是“批量替换”Kubernetes 并不是严格一次替换多少只要任意时刻实际副本数不超过“期望副本数 maxSurge”并且可用副本数不低于“期望副本数 - maxUnavailable”控制器就认为安全可以继续推进。理解了这个约束你就能解释很多滚动更新中的怪现象。3.2 maxUnavailable允许多少个 Pod 同时不可用maxUnavailable 控制的是更新过程里允许“不可用的旧副本”上限。这个值可以是绝对数字也可以是百分比。用百分比时控制器会按照期望副本数乘以百分比以后再向上取整得到实际的不可用数量。例如 replicas5maxUnavailable25%计算结果是 ceil(5 * 0.25) 2允许同时有 2 个旧 Pod 进入 Terminating 状态。如果 replicas4maxUnavailable25%结果是 ceil(4 * 0.25) 1只允许一次停 1 个。这个参数直接决定了更新的“保守程度”。如果你把 maxUnavailable 设为 0就意味着更新过程中一个旧的也不能死必须保证所有副本时刻可用。但这会带来一个隐含要求maxSurge 必须大于 0否则先建新的会把整体副本数推到超出期望先删旧的不允许更新会直接卡死。3.3 maxSurge允许最多超出期望副本几个maxSurge 控制的是更新过程里允许“超出期望副本数”的上限。同样支持绝对数字和百分比。例如 replicas5maxSurge25%结果是 ceil(5 * 0.25) 2最多允许同时存在 7 个 Pod。这个参数本质上是为“启动新 Pod 所需的额外资源”买保险。因为滚动更新要想做到完全不中断必然需要先启动新 Pod再删除旧 Pod这个过程中总 Pod 数一定大于期望副本数。如果集群资源余量不足maxSurge 设置过大会把调度器压垮甚至影响集群里其他应用的扩容。我在实际中见过一个典型的错误配置某团队把 maxSurge 设成 100%maxUnavailable 设成 0%结果 20 个副本更新时控制器一次性创建了 20 个新 Pod。集群节点只有 25 个空闲 CPU 配额新 Pod 大量 Pending旧 Pod 还是满负荷跑着整组服务直接退化到“一半新一半旧新的一半全卡在 ContainerCreating”。3.4 默认值与参数组合速查RollingUpdate 默认值是 maxUnavailable25%、maxSurge25%。如果期望副本数只有 2按默认值计算后unavailable 和 surge 都取 ceil(2 * 0.25)1更新节奏是“先建 1 个新、等就绪、再删 1 个旧”。这个默认值对大多数无状态应用是合理的但遇到资源紧张或有状态敏感的场景需要手动调优。我整理了几种常用的组合供参考配置组合适用场景效果特征maxUnavailable1, maxSurge1常规 Web 服务一次最多替换 1 个资源占用少整体过程慢maxUnavailable25%, maxSurge25%默认无状态且资源不紧张平衡速度与可用性maxUnavailable0, maxSurge1 或 25%不能停服的在线服务始终维持足够副本但需要额外资源maxUnavailable50%, maxSurge0对资源极度敏感能容忍短暂不可用先删旧再建新相当于轻量级 Recreate但会分段重要提示maxUnavailable和maxSurge不能同时为 0不然滚动更新无法完成任何一步操作。在 Deployment 提交时Kubernetes 的 API 校验阶段就会拦截这种非法配置实际用的时候别硬试。4. 实操记录完整观察一次更新过程4.1 准备一个便于观察的 Deployment先说结论纸上谈兵看再多不如自己动手跑一次。为了观察滚动更新的完整路径我建了一个最小的实验 Deployment。先把镜像版本定为 v1后面更新成 v2通过 watch 命令看它每一步的动作。apiVersion: apps/v1 kind: Deployment metadata: name: demo-update spec: replicas: 3 selector: matchLabels: app: demo-update template: metadata: labels: app: demo-update spec: containers: - name: demo image: registry.example.com/demo:v1 ports: - containerPort: 8080 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1为了扩大观察窗口我把 readinessProbe 的 initialDelaySeconds 设成 5 秒periodSeconds 设成 5 秒这样新 Pod 从启动到 Ready 至少需要 10 秒左右足够看清滚动节奏。4.2 观察 RollingUpdate 的推进节奏执行更新命令同时开三个 watch 窗口kubectl set image deployment/demo-update demoregistry.example.com/demo:v2 kubectl get rs -w kubectl get po -w kubectl rollout status deployment/demo-update在kubectl get po -w的输出里你会清楚地看到新 ReplicaSet 先创建 1 个 pod状态变成 Running 并进入就绪随后旧 ReplicaSet 的一个 pod 进入 Terminating。之后再出现第 2 个新 pod然后第 2 个旧 pod 开始终止如此循环。这个节奏由 maxSurge1 和 maxUnavailable1 共同决定同一时间最多有一个新 Pod 在创建也最多有一个旧 Pod 在消亡。kubectl rollout status会持续输出“Waiting for deployment spec update to be observed...”“Waiting for rollout to finish: 1 out of 3 new replicas have been updated...”直到最终输出“deployment successfully rolled out”。实际经验观察滚动更新时我把下面的命令记成了口头禅先看 rs再找新旧版本号。如果老是看不清过程可以把 maxUnavailable 和 maxSurge 调成 1、replicas 调成 5把节奏放慢非常有效。4.3 实测 Recreate 与资源波动然后我把 strategy 改成 Recreate再来一次同样的镜像更新。这次用时间戳给 Pod 打个标记kubectl rollout restart deployment/demo-update在kubectl get po -w里你会看到完全不同的画面旧 ReplicaSet 的 3 个 Pod 同时进入 Terminating大约几秒后列表里出现一批全是ContainerCreating的新 Pod之后陆续变成 Running。这个过程中服务会短暂不可被访问。如果你在外部打了循环请求会看到一段连续的时间窗口内请求全部失败。我顺手测了一次在本地环境里 Recreate 的请求失败窗口大约是 18 秒。拆开看旧 Pod 优雅终止花了 3 秒镜像拉取到容器启动花了 10 秒就绪探针成功又花了 5 秒。这 18 秒就是 Recreate 最直白的成本。注意Recreate 模式下执行kubectl rollout restart同样会触发全停全开这常用于强制重建所有 Pod 的场景。如果你只是想短暂“重启服务”Recreate 的语义比 RollingUpdate 更符合直觉反正最终都会全量重建。4.4 参数计算与生效过程演示光看节奏还不够我再用一个更复杂的例子演示参数计算。假设 replicas10maxUnavailable30%maxSurge30%。数学结果是unavailable 数 ceil(10 * 0.3) 3surge 数 ceil(10 * 0.3) 3。整个更新过程中任意时刻最少要有 7 个 Pod 可用最多可以跑到 13 个 Pod。控制器推进的每一步都会以这两个指标为约束。如果换成 maxUnavailable1、maxSurge0那么任何时候最多只有 1 个旧 Pod 不可用但不能创建额外 Pod。这就意味着必须先删除一个旧 Pod再等新的起来再删下一个。整体表现更接近“逐台替换”适合资源余量不大但服务优先级较高的场景。如果 maxUnavailable0、maxSurge1则不允许旧 Pod 不可用但可以多建一个。控制器会先建一个新 Pod等它 Ready再终止一个旧 Pod。由于旧 Pod 在终止前新 Pod 已经顶上服务始终维持完整副本数这是对可用性要求最高的组合。5. 生产环境高频踩坑与排查方法5.1 更新卡住不动大概率是就绪探针或资源不足滚动更新卡住是非常常见的故障。表象是kubectl rollout status长时间停在某个百分比整个 Deployment 的更新不前进也不后退。排查思路我建议按“先看新 Pod 状态、再看调度事件、最后看控制器事件”三层来。先用kubectl get pods看新 Pod 是否 Ready如果一直 ContainerCreating就去kubectl describe pod pod里看 Events通常能看到磁盘压力、CPU 不足或者镜像拉取失败的记录。如果新 Pod 状态已经是 Running但一直不 Ready那问题多半在就绪探针上。探针请求的接口如果不健康或者探针超时设置太短新 Pod 永远进不了 Ready滚动更新就只能卡住。这里有个容易出问题的点很多应用的/health接口在启动初期会返回 503你只配置了initialDelaySeconds但没留够时间导致探针反复失败直到容器被 Restart。实操心得遇到滚动更新卡住先别急着调参数。按规定先看一下新 Pod 的 lastState 是不是有 CrashLoopBackOff如果容器在疯狂重启再平滑的参数也救不了。把关卡放在应用本身的健康检查上比什么都有用。5.2 maxSurge 设置过大导致的资源打满前面提到的 100% surge 案例并不是个例。很多团队把滚动更新参数从默认值改大后没考虑到集群剩余资源。更新时控制器一次性把新副本拉到峰值可能把集群空闲资源全部抢走导致其他应用的 Pod 被驱逐或者扩不起来。要规避这个问题一个好办法是给工作负载设置 Pod 的 requests 和 limits同时在部署发布前用类似kubectl describe nodes看看节点剩余可分配资源。更稳妥的做法是在部署流水线里加一步资源预估脚本计算 maxSurge 峰值所需的 CPU 和内存如果超出集群空闲资源直接阻止发布。从经验讲如果集群资源利用率已经常年超过 70%保守的 maxSurge1 比任何花哨的百分比配置都靠谱。批量更新虽然慢一点但整体可控。5.3 Recreate 模式下所有副本掉线的连锁反应某一次我对一个负责鉴权的内部服务执行了 Recreate 更新结果所有依赖它的服务在停机窗口里全部报错。这不算 K8s 的锅纯粹是选型失误。鉴权这类东西调用频率极高任何不可用都会被下游放大。后来我强制规定所有被多个服务依赖的内部组件必须使用 RollingUpdate并且 maxUnavailable 不允许大于 0。如果一定要用 Recreate至少要保证服务发现或负载均衡层面有“摘流”机制。有些团队会在发布前先通过 Service 对象的 selector 把后端清空等 Recreate 完成后再把 selector 加回来实际上这是把运维复杂度转移到了外部脚本里不如直接用滚动更新合适。5.4 滚动更新中两个版本并存的兼容性问题RollingUpdate 的副作用是新旧 Pod 会短暂共存。如果升级过程中新旧两个版本同时接收流量而你的应用出现了数据格式不兼容或者缓存结构变化就会产生脏数据。这不是滚动更新本身的问题而是发布规范的问题。我处理过的最典型场景是一个消息消费者从 v1 升到 v2v2 启动后开始消费一批消息并把消息转成新的结构写到数据库里而 v1 还在消费同一条队列读到新结构以后直接反序列化报错。解决办法不是改用 Recreate而是在发布流程里先对消费者做“摘流量”处理等消费者全量更新完毕后再恢复流量。也就是说滚动更新适合无状态应用但对有状态或对版本兼容性有强要求的服务需要额外设计发布编排。5.5 排查命令组合推荐排查一次滚动更新我会固定用这几条命令组合kubectl rollout status deployment/name kubectl get rs -o wide kubectl get pods | grep name kubectl describe pod pod kubectl get events --sort-by.lastTimestamp kubectl rollout history deployment/name最有信息量的是kubectl get rs -o wide它能直接从 DESIRED、CURRENT、READY 三列看出新旧 ReplicaSet 是否在按预期推进。如果旧 RS 一直没缩容说明控制器被某个 maxUnavailable 约束卡住了如果新 RS 一直没扩容说明 maxSurge 约束卡住了或者新 Pod 始终不 Ready。6. 我的选型心得与参数建议RollingUpdate 和 Recreate 的选择我一般用“版本共存是否有风险”这个维度来做第一轮过滤。如果两个版本绝对不能共存比如数据库迁移、离线任务重放我直接选 Recreate。如果两个版本可以短暂共存就选 RollingUpdate然后再根据可用性要求和资源余量去调 maxUnavailable 和 maxSurge。参数调整方面我的习惯是面向用户的在线服务优先保可用性用maxUnavailable: 0, maxSurge: 1代价是要多付 1 个 Pod 的资源。资源紧张的服务用maxUnavailable: 1, maxSurge: 0代价是更新期间有一点抖动但不会额外占用资源。处于两者之间的大多数场景默认的 25% / 25% 其实已经足够。还有一个平时不会注意到的点更新策略决定的是“每次发布”的节奏但发布频率高不高也很重要。如果一天发几十个版本每个版本都让滚动更新跑一遍完整的探针等待周期这个过程还是挺耗时的。这时候可以把minReadySeconds、progressDeadlineSeconds一起配合使用前者保证新 Pod 稳定后再继续滚动后者避免一个异常版本把发布流程卡死太长时间。最后说点实在话。我见过很多团队在 K8s 上跑了很久却从没仔细看过 Deployment 的 strategy 配置默认就是 RollingUpdate然后某次遇到数据库升级把整个服务搞挂了才回头研究 Recreate。其实这两者的取舍并不复杂复杂的是你得清楚自己业务里“新旧共存”会带来什么后果。如果你现在的服务是纯无状态的 Web API大胆用 RollingUpdate 并保持默认参数不会有任何问题。如果你的服务里有共享存储、有数据格式迁移或者用户量大到不能容忍任何一秒的掉线那就要认真设计 maxUnavailable 和 maxSurge 的组合并在发布前做好资源和探针的检查。这套逻辑想清楚了Deployment 的更新对你来说就不再是黑盒。
返回列表