ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K8s Deployment实战:滚动更新与回滚机制详解

K8s Deployment实战:滚动更新与回滚机制详解 K8s系列走到第四篇终于要聊正事了。前三篇我们把集群规划、核心组件协作、Pod是怎么被创建出来的串了一遍这些都属于地基。地基砌好之后你总得往上盖房子在 Kubernetes 里“盖房子”这件事对应的就是工作负载而工作负载里日常使用频率最高的我敢说是 Deployment。这篇我把 Deployment 单独拎出来从设计原理到滚动更新再到回滚完整走一遍实战。目标读者是已经搭过集群、跑过简单 Pod但对 Deployment 的滚动机制和回滚操作还停留在“听过”阶段的同学。看完你至少能做到两件事发布新版本时心里有数线上出了问题时能冷静地把它退回去。1. 工作负载里为什么偏偏绕不开 Deployment1.1 先认人工作负载家族里谁管哪种活儿Kubernetes 里的工作负载不是只有 Deployment 一类它是按“应用形态”分家的。最底层的是 Pod但生产环境几乎不会直接创建裸 Pod因为没人想手动维护几十个副本的生命周期。于是 K8s 在 Pod 之上包了一层又一层控制器。工作负载管什么典型场景Deployment管理一组无状态 Pod支持滚动更新与回滚Web 服务、API、微服务StatefulSet管理有状态 Pod保证稳定网络标识和持久化数据库、消息队列、ZooKeeper 这类需要身份和存储的组件DaemonSet保证每个节点上恰好跑一个 Pod日志采集、监控 Agent、网络插件Job / CronJob执行一次性或定时任务任务结束 Pod 就退出批处理、数据迁移、定时清理这么一分你会发现Deployment 管的是“无状态、可以随时替换”的应用。所谓无状态不是没内存没磁盘而是说任何一个副本被干掉、被重建都不影响整体服务。Web 服务、API 网关、业务后端基本都是这个形态。1.2 三层继承关系你写的 Deployment 最终控制的其实是 Pod很多人第一次听说 ReplicaSet 会有点懵我明明只写了 DeploymentK8s 为啥又给我搞出来一个 ReplicaSet这里要先捋清 Kubernetes 对象之间的“血缘”。在 Deployment 出现之前老版本用的是 ReplicationController简称 RC功能很直接保持 Pod 副本数稳定。但 RC 对“更新应用”这件事支持得不好于是后来社区用 ReplicaSet 替代了 RC再后来又用 Deployment 包装了 ReplicaSet。完整的关系是Deployment 管理 ReplicaSetReplicaSet 管理 Pod。Deployment 每次触发一次更新就会创建一个新的 ReplicaSet让新 RS 里的 Pod 按规则慢慢长大让旧 RS 里的 Pod 慢慢缩掉。为什么要保留多个 RS就是为了回滚。旧版本不是被删掉了而是缩容到 0、留着历史记录等你想退回去的时候旧 RS 随时待命。所以实际下发的链路是你 writeDeployment控制器创建ReplicaSetReplicaSet 创建Pod一句话概括Deployment 是“老板”ReplicaSet 是“工头”Pod 是“干活的工人”。1.3 选型时的一道判断题不是所有场景都适合上 DeploymentDeployment 虽然好用但别形成条件反射看什么都先写个 Deployment。这里的判断标准就一条你的应用能不能接受 Pod 被随机调度、随机替换、随机重启如果应用需要稳定的网络标识和稳定的存储比如 Redis、MySQL、Kafka你用 Deployment 就相当于把数据库副本当“临时工”管理Pod 重建之后名字变了、IP 变了、数据没了这时候应该用 StatefulSet或者直接用对应的 Operator。如果要在每个节点上跑一个采集组件比如 node-exporter、fluentd那用 DaemonSet 更合适。如果是跑一次性数据清洗任务那就交给 Job。我见过不少生产事故源头就是把 Redis 集群用 Deployment 部署Pod 一重启节点挂了、数据丢了。工作负载先选型再写 YAML这个顺序不能省。2. 声明式设计是怎么让“滚动更新”这件事成立的2.1 期望状态与控制器循环你只负责提需求Deployment 最核心的设计思想叫“声明式”你告诉集群“我要 3 个 nginx 副本镜像版本 1.27”剩下的事 K8s 自己想办法。这个“想办法”的过程靠的是控制器循环reconcile loop。ReplicaSet 控制器会不停地做一件事比对“期望副本数”和“当前实际 Pod 数”多了就删少了就建。这个循环没有终点它一直挂着时刻等着现实偏离期望的状态。拿空调打比方最直观你设 26 度是期望状态温度传感器是当前状态空调压缩机就是控制器温度高了就制冷、低了就停机直到误差归零。Deployment 的控制器循环比 ReplicaSet 多了一层它还要比对“期望的 Pod 模板”和“当前 RS 里的 Pod 模板”。一旦发现模板变了就认为这是一次新版本发布于是创建新的 ReplicaSet再让新 RS 和旧 RS 按滚动策略调整副本数量。理解了这一层你就能明白K8s 里几乎所有“自动修复”的能力都靠这种循环。它不是定时任务是常驻的反馈回路。2.2 拆 YAML那些真正决定行为的关键字段一个最小可用的 Deployment YAML 长这样apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 15 periodSeconds: 10我不打算把每个字段都念一遍只说你必须吃透的五个。第一是replicas期望副本数。它就是个数字但你要清楚它会被 HPA水平自动扩缩容接管如果配了 HPA 就别手动改副本数改了会被 HPA 调回来。第二是selector决定了这个 Deployment 管哪些 Pod。Deployment 通过matchLabels去挑选 Pod。这里有个关键限制selector 一旦创建就不可修改因为控制器靠它识别归属你中途改了 selector相当于跟集群说“我不认识原来那些 Pod 了”旧的 ReplicaSet 和 Pod 就变成了无主对象全部失控。第三是template也就是 Pod 模板。Deployment 判断要不要发布新版本就是看这个模板的哈希值有没有变化。镜像改个 tag模板哈希就变触发滚动更新。第四是strategy发布策略。可选值只有两个Recreate和RollingUpdate。Recreate 简单粗暴先把旧 Pod 全删了再建新的数据不丢但服务必断适合不能同时存在两个版本的场景。RollingUpdate 是默认值也是这篇文章的主角。第五是revisionHistoryLimit保留多少份历史 ReplicaSet。默认值是 10这个值直接决定了你能回滚几步。2.3 maxSurge 和 maxUnavailable滚动更新的两个旋钮滚动更新的行为由两个参数决定maxSurge和maxUnavailable默认都是 25%。这俩参数很多人配置过但真正理解算法的不多。maxSurge滚动期间允许超出期望副本数的最大 Pod 数。比如期望 3 个副本maxSurge25%那么最多可以同时存在 4 个 Pod多出来的 1 个会用来创建新版本 Pod。maxUnavailable滚动期间允许最多有多少个 Pod 处于不可用状态。maxUnavailable25%3 个副本向下取整是 0也就是说最理想情况下新 Pod 没就绪前旧 Pod 一个都不能删保证服务不降级。算一下 3 副本、两个参数都 25% 的场景maxSurge 3 * 25% 0.75向上取整为1maxUnavailable 3 * 25% 0.75向下取整为0所以滚动期间K8s 会先把新 RS 的 Pod 从 0 加到 1新 Pod 通过就绪探针之后再缩旧 RS 的 Pod整个过程始终保证可用 Pod 不少于 3 个。如果你希望“既快又不抖动”可以把maxSurge设大、maxUnavailable设小如果你愿意接受短暂容量下降可以反过来。这两个值必须有一个不为 0否则滚动永远无法推进。2.4 一次滚动到底发生了什么时间线我按时间线把一次理想的滚动更新拆成五个阶段方便你对照观察你执行kubectl apply修改镜像版本Deployment 控制器发现 Pod 模板哈希变了。控制器创建新的 ReplicaSet比如nginx-deploy-7d9f8cbf5f初始副本数 0。由于maxSurge1新 RS 开始创建 1 个新 Pod新 Pod 进入 ContainerCreating 状态然后启动等待就绪探针通过。新 Pod Ready 后控制器才把旧 RS 的副本数从 3 减到 2于是又有名额创建第二个新 Pod如此往复。最后新 RS 副本数到 3旧 RS 副本数到 0Deployment 的 Available 状态为 True滚动完成。整个过程不是在“替换”Pod而是在“增”和“减”之间找平衡。新 Pod 不 Ready旧 Pod 就不会被大规模缩容——这就是滚动更新不会导致全量中断的根本原因。3. 完整实战从 apply 一个 Deployment 到观察滚动全程3.1 先把清单写明白带就绪探针的那种我建议你一开始就养成写 YAML 的习惯别总是kubectl run一把梭。文件化有两个好处第一是可审查发布前 diff 一下就知道改了什么第二是可复现换环境不需要凭记忆敲命令。这里给一份可以直接用的清单保存为nginx-deploy.yamlapiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: default spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.27 imagePullPolicy: IfNotPresent ports: - containerPort: 80 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 15 periodSeconds: 10 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi注意两个小细节。一是readinessProbe。我在第 5 部分会重点讲它这里先记住没有就绪探针的滚动更新是残缺的K8s 无法判断新 Pod 是不是真的“能用”只能默认“进程活着就算 Ready”。二是resources。你把 requests 写清楚调度器才能去算哪些节点放得下这批 Pod。不写 requests 导致的问题通常不会立刻爆发等集群资源紧张时才发现 Pod 全挤在一起那时就难收拾了。3.2 创建之后先做状态确认执行kubectl apply -f nginx-deploy.yaml然后不要急着刷get pods我一般先看两层状态。kubectl get deploy nginx-deploy -o wide kubectl get rs -l appnginx -o wide看 Deployment 的状态列重点关注AVAILABLE是不是等于READY。如果 READY 是 3 而 AVAILABLE 也是 3说明整个副本组都已被负载均衡接受。再看 RS第一次创建时只会有一个 RS名字后缀是一串随机字符下面是它的 3 个 Pod。如果你想确认当前是哪个版本可以看kubectl get deploy nginx-deploy -o jsonpath{.metadata.annotations.deployment\.kubernetes\.io/revision}{\n}版本号是 1这就对了。Revision 从 1 开始计数后面每次模板变更都会递增。3.3 触发更新的三种方式我推荐哪一种触发滚动更新有三种常见方式。第一种是kubectl set imagekubectl set image deployment/nginx-deploy nginxnginx:1.28优点是一条命令搞定适合临时应急缺点是没有留下可 review 的变更记录多人操作时你不知道谁改的。第二种是kubectl editkubectl edit deployment/nginx-deploy会打开默认编辑器直接改内存里的对象。方便但同样不适合生产任何人都能改且改完难以追溯。第三种是改 YAML 文件后重新kubectl applyvim nginx-deploy.yaml kubectl apply -f nginx-deploy.yaml我强烈推荐第三种。原因是 YAML 文件是你的声明式资产它应该进 Git配合 CI 走发布流程。发布之前看一眼 diff发布之后留一条 commit线上到底什么状态全部有据可查。个人玩测试集群怎么折腾都行但在生产环境你希望每一次变更都像代码一样被记录。3.4 滚动过程中的 Pod 变化逐条看改完镜像重新 apply 之后新开一个终端观察滚动。watch kubectl get pods -l appnginx -o wide你会看到类似这样的过程先出现第 4 个 Pod名字后缀是新的 RS 前缀状态从 Pending 变成 Running然后 READY 从 0/1 变成 1/1。这期间旧的 3 个 Pod 没有任何变化。新 Pod Ready 之后旧 Pod 才一个一个变成 Terminating。接着第 5 个新 Pod 被创建旧 Pod 再减一个直到新旧交替完成。这个过程用kubectl get rs看更直观kubectl get rs -l appnginx滚动前只有一个 RS显示DESIRED3 CURRENT3 READY3。滚动中你会同时看到两个 RS旧的 DESCIRED 在往下减新的 DESCIRED 在往上加。滚动结束后旧的 RS 变成DESIRED0 CURRENT0 READY0但它还留在列表里这就是回滚的历史资产。如果你想等滚动彻底结束再走用这个命令kubectl rollout status deployment/nginx-deploy --timeout120s滚动完成的输出是deployment nginx-deploy successfully rolled out。如果滚动卡住了这个命令会一直等直到超时返回非零状态码。在 CI 里发布后接这条命令能让流水线第一时间发现发布失败。3.5 容易误解的一件事滚动更新和流量切换不是一回事我在这里要专门踩一个很多人都会误解的坑Deployment 滚动更新不等于你做的灰度发布更不等于流量立刻切到新版本。Kubernetes 的滚动更新只管“创建新 Pod、删除旧 Pod”。那流量为什么没断因为你的 Service 的 Endpoints 列表里只放 Ready 的 Pod IP新 Pod Ready 之后它的 IP 会自动加入 Endpoints旧 Pod 被删之前它的 IP 会自动摘除。这个摘除和加入是异步的中间会有极短暂的重叠但对大多数应用来说无感。问题是这个机制并不懂“业务流量按比例分配”。它只是让新旧 Pod 短暂共存共存的时长和比例完全由maxSurge/maxUnavailable决定没有“先把 10% 流量导到新版本”这种逻辑。想要按比例灰度得用 Istio、Argo Rollouts 这类专门工具或者自己在 Service 和 Deployment 之间做多层控制。所以当你听到“Deployment 支持金丝雀发布”这句话时心里要留个底它只是支持“暂停和分批”但它不做流量权重。4. 事故现场的救命技能回滚与发布版本管理4.1 版本是怎么被你留下的回滚的前提是有历史版本可以回。Deployment 的历史版本其实就是那些“缩容到 0 的旧 ReplicaSet”。每次 Pod 模板发生变化控制器就创建一个新 RS并把旧的 RS 保留下来数量受revisionHistoryLimit控制。查历史用这个命令kubectl rollout history deployment/nginx-deploy输出会类似deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 1 none 2 none如果你的 YAML 或命令带了kubernetes.io/change-cause注解CHANGE-CAUSE 那一列就会显示你写的备注方便一眼看出每个版本改了什么。给当前版本补备注可以这样做kubectl annotate deployment/nginx-deploy kubernetes.io/change-causebump nginx to 1.28注意这个注解要在发布发生时已经存在才会被记录到历史里事后补的是给当前修订版本补一笔备注效果也够用。新版 kubectl 里--record已经被弃用了我劝你别再依赖它老老实实用注解。每个新版本都会生成新的 RS注意回滚操作本身也会生成一个新版本号而不是回到旧的 revision 编号。所以rollout history里的 REVISION 会一直往上递增这一点和 Git 有点像revert 也是一次新的 commit。4.2 事故现场一次完整的回滚命令演练我模拟一个最常见的故障你把镜像 tag 改成了一个不存在的版本比如nginx:1.28.8Pod 全部拉不到镜像处于ImagePullBackOff。此时你看到kubectl get pods一堆ImagePullBackOff但服务其实还没断因为旧 RS 的 Pod 还没被全部缩掉。可如果你等滚动持续推进旧 Pod 会被慢慢删光到时才是真的事故。回滚第一条命令回到上一个版本kubectl rollout undo deployment/nginx-deploy执行后Deployment 控制器会把新 RS 的 Pod 往下缩把旧 RS 的 Pod 往上抬整个过程同样是滚动式的。跑完后确认kubectl rollout status deployment/nginx-deploy看到successfully rolled out就说明旧版本已经重新接管。这里有个细节回滚也不暂停。它也是滚动更新所以如果你的应用新旧版本完全不兼容回滚也可能造成短暂的新旧共存。真正的“立即切换”只能用 Recreate 策略或者手动把新 RS 直接缩到 0、旧 RS 拉满但这属于紧急止血手段不太优雅。如果想回退到指定版本比如第 1 版kubectl rollout undo deployment/nginx-deploy --to-revision1回滚前我建议先确认一下目标版本和当前版本的差异用kubectl rollout history deployment/nginx-deploy --revision1会列出那个版本对应的完整 Pod 模板信息比对着看一遍再决定回不回避免回错了又来回折腾。4.3 暂停发布想先放一批试水有时候你不想一次性滚完想分阶段确认。Deployment 的暂停能力可以帮到你。发布前先暂停kubectl rollout pause deployment/nginx-deploy然后修改镜像kubectl set image deployment/nginx-deploy nginxnginx:1.28此时你会发现 Pod 没有任何变化——因为 Deployment 处于暂停状态控制器不会推进 rollout。你可以先看看这次要发布的模板是不是写对了甚至再用一个临时 Deployment 验证一下新镜像的启动。确认没问题之后恢复kubectl rollout resume deployment/nginx-deploy从 resume 这一刻起滚动更新才真正开始。这个模式的本质是把“发布动作”和“发布推进”拆开给操作者留一个手动的确认阀门。不过我还是要诚实地说这个暂停/恢复只能控制“是否创建新 Pod”控制不了流量比例。想真正让 1/10 的用户访问到新版本还是要借助服务网格或专门的发布工具。Deployment 的 pause/resume 更适合解决“分阶段演练人工判断通过后继续”的场景。4.4 revisionHistoryLimit保留多少历史版本才合适默认 10 这个值对大多数应用来说偏多了。每个 RS 及其 Pod 模板都占 etcd 存储版本太多会拖累 apiserver。我自己的习惯是设成 3 或者 5。spec: revisionHistoryLimit: 3设成 3 意味着你只能回滚最近 3 次发布超过的旧 RS 会被控制器回收。太少了怕不够回太多了浪费存储3 到 5 这个区间在大多数业务场景里都够用。另外提一句如果你对一个 Deployment 只改replicas不碰模板这不会产生新版本因为 RS 的 Pod 模板没有变控制器只需要原地调整副本数。这个行为经常让人困惑但不影响使用记住即可。5. 生产环境里撞过的墙和固定排查套路5.1 滚动卡在“等待中”多半和探针有关我在测试环境模拟故障时最爱做的就是让新版本镜像一启动就报错或者故意让就绪探针检查一个不存在的路径。这时候滚动更新的表现非常典型新 Pod 一直处于0/1 Running旧 Pod 一个都不动Deployment 的副本数显示比期望多 1 个但 Available 的副本数始终到不了 3。原因就是maxUnavailable的保护新 Pod 不 Ready控制器就不能缩旧 Pod否则可用副本数就会低于期望值。于是整个滚动停在原地直到progressDeadlineSeconds超时。默认 600 秒超时后 Deployment 会标记为ProgressDeadlineExceeded。看状态命令kubectl describe deployment/nginx-deployConditions 里会出现Progressing: False (ProgressDeadlineExceeded)这并不代表集群会帮你自动回滚它只是告诉你“我没能在超时时间内完成你自己看着办”。真正的止损动作还得你手动rollout undo。所以探针不是可有可无的装饰它直接决定发布流程是自动推进还是卡死等你收拾。5.2 镜像 tag 没变导致发布了但感觉“没更新”这是另一个高频事故。你改了代码重新构建镜像但构建出来的镜像是同一个 tag比如永远叫nginx:latest。然后你执行kubectl apply发现 Deployment 没动静Pod 也没重建。原因很简单imagePullPolicy默认在 tag 不是latest时是IfNotPresent节点上已经有这个镜像K8s 就直接拿来用了。而且 Deployment 比对的是 Pod 模板哈希模板没变根本不会触发滚动。这类问题的修复建议非常明确镜像 tag 必须唯一。用 Git commit SHA 做 tag 是最廉价的方案保证每次构建的 tag 都不同模板哈希必然变化发布必然触发。如果你非要省事用latest记得把imagePullPolicy改成Always这至少能保证 Pod 重建时会去重新拉取但你不能依赖它解决“模板没变不滚动”的问题那需要主动重启kubectl rollout restart deployment/nginx-deploy这条命令会强制触发一次滚动常用于 ConfigMap 更新后想让 Pod 重新加载配置的场景。5.3 selector 是创建后不能改的我见过有人为了让 Deployment 容纳新的 Pod 标签试图去改selector.matchLabels结果是 apiserver 直接拒绝The Deployment nginx-deploy is invalid: spec.selector: Invalid value: ... field is immutable这不是 K8s 疯了是设计如此。selector 变了Deployment 就不再认识自己管理的 RS 和 Pod那整个控制器循环就崩了。换标签的正当做法是新建 Deployment或者通过kubectl label给 Pod 打标并用新 selector 管理但老 Deployment 要先删掉或缩容避免两个控制器抢同一个 Pod。说实话普通业务很少需要改 selector。如果你发现想改它先停下来想想是不是一开始的标签设计得太粗了。5.4 看着全是 Pending先查调度和配额滚动更新还有一种卡法新 Pod 起不来一直Pending事件里写着FailedScheduling或者FailedCreate。前者通常是节点资源不够kubectl describe pod里能看到0/5 nodes are available: 2 Insufficient cpu, 3 Insufficient memory.这时候你该去查的是集群容量和当前 Pod 的 requests而不是在 Deployment 上瞎折腾。后者常见于 namespace 配额限制failed to create pod: pods nginx-deploy-xxx is forbidden: exceeded quota如果配了 ResourceQuota新 RS 创建 Pod 时会因为配额不足而失败RollingUpdate 也会卡住。所以发布不是只看镜像资源和配额也是发布系统的组成部分这是很多新手容易忽略的。5.5 五步定位法从 Deployment 到 Pod 日志Deployment 出问题很多人习惯上来就kubectl logs其实顺序反了。我的固定套路是从上到下看一共五步。第一步看 Deployment 整体状态kubectl get deploy nginx-deploy -o wide如果AVAILABLE不等于READY问题已经存在。第二步看 Deployment 条件kubectl describe deploy nginx-deploy重点看 Conditions 里的Progressing和Available以及底部 Events能快速判断是卡滚动、拉镜像失败还是副本缩不了。第三步看 ReplicaSetkubectl get rs -l appnginx -o wide新旧两个 RS 的 DESIRED 和 READY 一对比立刻就能看出滚动进行到哪一步。第四步看具体 Podkubectl get pods -l appnginx kubectl describe pod pod-namePod 的状态Pending / ImagePullBackOff / CrashLoopBackOff / Running和 Events 会告诉你问题出在调度、拉取还是启动阶段。第五步才轮到日志kubectl logs pod-name kubectl logs pod-name --previous--previous在 Pod 崩溃重启后非常管用能看上一轮容器退出前的日志。很多人上来就看当前日志结果容器重启了无数次日志早被冲掉了。这套顺序是“自顶向下排查”的常规做法逻辑是先缩小范围再定位细节。发布类的故障百分之八十都能在这五步内找到根因。最后说一点个人经验。Deployment 这个对象我用了几年最大的体会是它把运维里最复杂的一类操作——安全的版本变更——压缩成了一个声明式动作。但声明式不等于“不用懂内部”恰恰相反你越理解 ReplicaSet 和 Pod 之间的那层关系越知道探针和策略参数在滚动里扮演的角色就越能在故障发生的几分钟内做出正确决定。滚动更新不是魔法它是一套有边界的自动化边界之外的事还得人来兜底。希望这篇能帮你把那部分边界摸清楚。
返回列表