在生产集群里待久了你会发现,大家聊 Kubernetes 时提到最多的对象之一就是 Deployment。Deployment 用来承载无状态应用的 Pod,开发者提交一份 YAML,Pod 副本数量、更新方式、回滚策略全在里面。说白了,它就是帮你在 K8s 里安全发布和迭代应用的“发布系统”。今天这篇就围绕 Deployment 部署 Pod,以及应用更新、回滚这套完整流程做一次实战向拆解,适合刚上手 K8s 的运维同学,也适合在开发侧写部署清单的工程师。我会把每个关键字段的逻辑、每条命令的实际输出、踩过的坑一起交代清楚,看完你至少能独立完成一次从部署到回滚的完整发布演练。
1. 为什么说 Deployment 是 Pod 的“专职管家”
1.1 直接创建 Pod 会遇到哪些坑
如果刚开始接触 K8s,很多人会直接写一个 Pod YAML,然后kubectl apply -f pod.yaml。这样当然能把应用跑起来,验证阶段完全够用。但只要你把视角切到生产环境,问题立刻暴露:Pod 本身没有自愈能力。节点重启、Pod 被驱逐、容器 OOM,只要这个 Pod 没了,它就是没了,不会有人帮你再拉起来。
你可能会说,那我用 ReplicaSet 不就行了?ReplicaSet 确实能保证集群里始终有指定数量的 Pod 副本,但它只管“数量对不对”,不管“新版本怎么发布”。生产环境真正需要的是这样的能力:发新版时不能断服务,新旧版本要平滑切换,发完还能一键回退。ReplicaSet 给不了这些,于是 Deployment 出现了。
Deployment 可以理解为更上层的控制器,它通过管理 ReplicaSet 来间接管理 Pod。你在 Deployment 里描述“我想要 3 个副本,镜像版本是 v2.0.0”,Deployment 就会确保底层有 3 个 Pod 在跑,并且镜像版本始终是你声明的版本。更重要的是,它自带滚动更新和回滚能力。每次你改模板镜像,Deployment 就会自动创建新的 ReplicaSet,把流量从旧版本滚到新版本;一旦新版本出问题,几十秒内就能退回旧版本。
1.2 Deployment、ReplicaSet、Pod 三层关系怎么理解
我经常用一个不太严谨但好记的类比:Deployment 是物业管委会,ReplicaSet 是具体楼栋的管家,Pod 是租户。管委会定规则,管家执行规则,租户实际提供服务。你不需要直接找租户,也不需要手动安排管家,管委会发一条指令,整栋楼都会按流程运转。
这种三层结构给运维带来的直接好处是:职责分离。Deployment 只关心模板和副本数,ReplicaSet 只关心某个版本下的副本数量,Pod 只关心容器是否运行。你更新 Deployment 里的镜像时,K8s 不会去改旧 ReplicaSet 里的 Pod,而是新建一个 ReplicaSet,让新 ReplicaSet 的 Pod 慢慢启动,等它们 Ready 了再把旧 ReplicaSet 的 Pod 删掉。这就是滚动更新的本质。
实际使用中有一个很重要的习惯:不要手工去改 Deployment 管理下的 ReplicaSet。很多新手看到kubectl get rs底层有多个 ReplicaSet,就想着直接编辑某个 RS 改副本数,这个操作会被 Deployment 控制器立刻纠正回来。Deployment 和它下面的 RS 是强绑定关系,所有版本变更都应该只发生在 Deployment 这个层面,否则整个发布历史就乱了。
2. 部署清单的设计思路:YAML 里的每个字段都不是摆设
2.1 模板与选择器:Pod 不认识 Deployment,只认标签
直接看一份实际能用的 Deployment YAML,后面所有操作都基于它:
apiVersion: apps/v1 kind: Deployment metadata: name: demo-app namespace: demo spec: replicas: 3 selector: matchLabels: app: demo-app revisionHistoryLimit: 10 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 1 template: metadata: labels: app: demo-app spec: containers: - name: app image: registry.example.com/demo-app:v1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 20 periodSeconds: 10先看spec.template.metadata.labels。这个标签不是随便写的,Pod 创建出来后会带这组标签,Deployment 靠spec.selector.matchLabels来找到并管理这些 Pod。两者必须匹配,否则创建 Deployment 时会被 API Server 直接拒绝,报错内容也很直白:selector 跟 template labels 不匹配。
还有一点需要注意,spec.selector在 Deployment 创建之后是不能改的。因为 selector 天然就是“找 Pod”的过滤条件,一旦运行中的 Deployment 改了 selector,底层 ReplicaSet 和 Pod 的归属关系就全乱了。所以如果后期发现标签设计有问题,通常只能删掉重建 Deployment,代价很大。最开始设计标签时就要想清楚,最好带上版本无关的稳定维度,比如app、tier,不要把版本号放进 selector。
replicas: 3是期望副本数,Deployment 会一直尝试把实际副本数对齐到 3。这里提醒一句:如果集群资源紧张,replicas 设太大只会让 Pod 长时间 Pending,不会自动缩容去适配资源,该扩容节点还是得扩容。资源配额问题后面还会细说。
2.2 更新策略:maxSurge 和 maxUnavailable 怎么搭配
YAML 里strategy.type支持两种:Recreate和RollingUpdate。Recreate是先删掉所有旧 Pod,再创建新 Pod,发布期间服务会完全中断,只适合那些不能双实例并行且能接受停机的内部任务。绝大多数在线服务都应该用默认的RollingUpdate。
滚动更新有两个核心参数,很多人背了概念但不会配。先说默认值:maxSurge默认 25%,maxUnavailable默认 25%。它们的含义要记准:maxSurge是滚动更新过程中最多允许超出期望副本数的 Pod 数量,maxUnavailable是最多允许不可用的 Pod 数量,单位可以是绝对数值,也可以是百分比。百分比会基于副本数向上取整,比如 3 个副本,25% 向上取整就是 1。
所以当 replicas=3、maxSurge=1、maxUnavailable=1 时,滚动更新期间整个 Deployment 的 Pod 总数最多是 4(3+1),最少可用的 Pod 数是 2(3-1)。这意味着发布过程中不会出现服务全部不可用的情况,同时允许短暂地多占一点资源。我经常把这两个值拆开讲:
maxSurge=1, maxUnavailable=0:严格保持可用副本数不低于 3,每新起一个 Ready Pod,才回收一个旧 Pod。发布节奏慢,但最稳,适合对可用性极其敏感的核心服务。maxSurge=1, maxUnavailable=1:允许短暂有一个 Pod 不可用,同时最多多跑一个 Pod,发布速度适中,绝大多数 Web 服务可以这么配。maxSurge=1, maxUnavailable=2:发布更快,但某一瞬间可用 Pod 可能掉到 1,服务容量会出现明显抖动,测试环境可以这么省时间。
实际配置时不要照抄默认值,先想清楚:如果 3 个副本里瞬间只剩 2 个,服务会不会被打爆?如果不会,maxUnavailable=1没毛病;如果很敏感,就把maxUnavailable设为 0,让发布慢一点。很多发布事故不是代码问题,而是滚动窗口期容量被击穿导致的连锁故障。
2.3 其他影响发布和回滚的字段
revisionHistoryLimit是经常被忽略但关键时刻能救命的字段。它控制 Deployment 保留多少个历史 ReplicaSet,默认值是 10。这个值决定了你能回滚到多远的历史版本。如果你把它设成 0,Deployment 不会保留任何历史版本,kubectl rollout undo基本就废了。我见过有人为了省资源把revisionHistoryLimit设成 1,结果发坏版本后连前一个版本都回不去,只能在代码里重新切 tag。建议保留默认的 10,或者根据发布频率适当调大,历史 RS 占用的资源非常有限,别拿回滚安全性换那一点 etcd 空间。
progressDeadlineSeconds控制滚动更新超时时间,默认 600 秒。如果新版本在 600 秒内没有完成滚动,Deployment 会被标记为ProgressDeadlineExceeded,kubectl rollout status会返回失败。但注意,标记失败不等于自动回滚,旧 RS 和新 RS 可能还停在那里,需要你手动处理。这个字段更像是一个信号灯,提醒你“这次发布卡住了,赶紧去看看”。
minReadySeconds也建议配置,它表示 Pod 进入 Ready 状态后,还要稳定运行多少秒才算真正可用。比如设成 15,那么 Pod 必须 Ready 后持续 15 秒没有崩溃,才会被算作可用副本。没有这个字段时,Pod 只要通过 readiness 探针就算可用,很多应用刚启动时进程活着但连接池还没建好,会被快速拉进流量然后超时。配置一个 10~30 秒的minReadySeconds,可以显著过滤启动抖动。
3. 从零部署到发布,一次完整的实操记录
3.1 环境准备与基础 YAML
实操之前先确认两件事:集群能正常访问,kubectl能用。我用的是虚拟的示例镜像registry.example.com/demo-app,版本号从 v1.0.0 开始。实际生产环境建议用私有镜像仓库,并且不要用latest标签,原因后面专门说。
把上面那段 YAML 保存成deployment.yaml,然后执行:
kubectl create namespace demo kubectl apply -f deployment.yaml如果 YAML 有问题,API Server 会直接报错。最常见的两个错误:selector 和 labels 不匹配,或者镜像名写错。都没问题的情况下,输出会显示deployment.apps/demo-app created。此时 Deployment 已经创建,但 Pod 可能还在拉镜像,需要继续观察。
3.2 首次部署和验证
创建一个 Deployment 不等于发布完成。后面这些命令是我每次发布都要敲的:
kubectl get deployment demo-app -n demo -o wide kubectl get rs -n demo -l app=demo-app kubectl get pods -n demo -l app=demo-app -o wide kubectl rollout status deployment/demo-app -n demo第一条看 Deployment 的期望副本数、当前副本数、可用副本数;第二条看底下的 ReplicaSet,首次部署会出现一个 RS,名字类似demo-app-7f6c9d8d4f,后面的随机串是 Pod 模板哈希;第三条看实际 Pod 状态,重点观察READY和STATUS,正常情况下会从ContainerCreating变成Running,READY变成3/3;第四条会直接告诉你滚动更新是否完成,如果输出deployment "demo-app" successfully rolled out,说明这次发布成功。
kubectl rollout status是个很有用的命令,它像阻塞等待,会持续观察 Deployment 的滚动过程。如果滚动卡住,这个命令会一直等,直到超时或你自己 Ctrl-C 中断。注意 Ctrl-C 只是停止观察,滚动更新在集群里仍在继续,别以为取消了。
首次部署生成的 RS 对应 Revision 1。你可以通过kubectl rollout history deployment/demo-app -n demo看到这个历史记录。后面每次更新模板镜像,都会新增一个新的 Revision。
3.3 应用更新:set image 与 apply 两种方式
现在模拟一次常规版本迭代,把镜像从 v1.0.0 升到 v2.0.0。第一种常见方式是直接用命令:
kubectl set image deployment/demo-app app=registry.example.com/demo-app:v2.0.0 -n demo命令里的app是容器名,对应 YAML 里spec.template.spec.containers[0].name。如果容器名写错,命令会提示找不到容器。执行后,Deployment 的 template 发生了变化,控制器会立刻创建新的 ReplicaSet,并开始滚动更新。
第二种方式是改 YAML 再 apply。一般配合 Git 代码仓库做 GitOps 发布:在仓库里把image改成 v2.0.0,提交,然后在集群上执行:
kubectl apply -f deployment.yaml这种方式的好处是清单文件始终和线上状态保持一致,回滚时也能用同一个文件历史来追溯。我个人推荐团队采用这种方式,而不是有人用set image、有人直接kubectl edit,时间一长集群里的 Deployment 和仓库里的 YAML 就产生漂移,后面排障会非常痛苦。
无论哪种方式,背后逻辑都一样:只有spec.template发生变化才会触发新版本发布。如果你只是改了replicas,Deployment 不会新增历史 Revision,只会在当前 RS 上调副本数。所以不要以为改了 Deployment 就一定有 rollout,得看改的是什么字段。
更新过程中可以随时执行kubectl rollout status deployment/demo-app -n demo看进度,同时开另一个终端看:
kubectl get pods -n demo -l app=demo-app -w你会看到新 RS 的 Pod 一个接一个变成 Running 和 Ready,旧 RS 的 Pod 同步被终止,Pod 名后的哈希也逐渐替换成新版本。
3.4 暂停与继续:控制发布节奏的隐藏技能
滚动更新默认是自动执行完的,但有些场景需要你中途控制节奏。比如新版本已经发到一半,你突然在监控图上看到异常,想先别让新版本继续扩散,这时候可以用:
kubectl rollout pause deployment/demo-app -n demo暂停之后,Deployment 不会再创建新的 Pod,已经创建的新 Pod 继续运行,旧 Pod 也不会继续缩容,整个发布过程停在当前状态。这个状态非常适合“灰度一半先观察”,确认没问题后再执行:
kubectl rollout resume deployment/demo-app -n demo恢复之后,Deployment 会继续完成未完成的滚动更新。还有一类用法是:发布前先 pause,然后连续修改多次 Pod 模板,此时不会每次都触发滚动更新,直到 resume 才统一按最终模板发布一次。这样可以避免中间态频繁触发新 RS,减少无意义的滚动。
这里必须提醒一个坑:在 pause 状态下直接执行kubectl rollout undo,行为可能不符合直觉。Cluster 上 rollout 历史是有的,但暂停状态下回滚动作不会立刻反映到 Pod 上。我踩过之后学到的经验是:先rollout resume,确认发布流程恢复,再做回滚,不要在一个 paused 的 Deployment 上盲目 undo。
4. 回滚操作:别让故障版本在线上过夜
4.1 查看发布历史与 Revision
接着说回滚。假设 v2.0.0 发布后发现接口报错率直线上升,你要尽快恢复。第一步永远不是直接执行 undo,而是先看清楚自己站在哪个位置。
kubectl rollout history deployment/demo-app -n demo输出大致是这样的:
REVISION CHANGE-CAUSE 1 <none> 2 <none>CHANGE-CAUSE 在新版 K8s 里默认是<none>,旧版本里的--record参数已经被废弃,所以不用指望它自动记录变更原因。如果你想让 history 可读,比较好的做法是在 CI/CD 平台里记录每次发布的版本号和 commit,集群里的 Revision 跟发布平台上的版本 ID 做一个映射。真要临时在 K8s 里记录原因,可以手动给 Deployment 加注解,但这不是主力方案。
想查看某个 Revision 的完整模板信息,可以执行:
kubectl rollout history deployment/demo-app -n demo --revision=2这里能看到这个 Revision 对应的 Pod Template 里的镜像版本、标签、探针配置。回滚前确认一下 Revision 2 是不是真的就是你想要的那个旧版本,比直接猜要稳妥得多。
4.2 回滚到上一个版本和指定版本
快速回到上一个版本,执行:
kubectl rollout undo deployment/demo-app -n demo默认情况下,它会回滚到当前 Deployment 历史里的上一个 Revision。对于刚才的场景,就是 v1.0.0。如果想明确回滚到某个指定版本,后面加参数:
kubectl rollout undo deployment/demo-app -n demo --to-revision=1这里要理解一个容易混淆的细节:回滚不是把集群“时光倒流”到 Revision 1,而是基于 Revision 1 的模板再触发一次新发布。回滚完成后,kubectl rollout history里会多一个 Revision 3,它和 Revision 1 的镜像内容相同,但 Revision 编号是新的。所以不要惊讶为什么回滚后历史里少了 Revision 2,其实它还在,只是被新 Revision 3 替代了当前状态。
回滚到指定版本的前提是,目标 Revision 对应的 ReplicaSet 还保留在集群里,也就是revisionHistoryLimit没有把它清掉。如果你发现--to-revision=1后 Deployment 没有反应,先确认历史列表里是否还有 1 这个编号。
kubectl rollout undo同样可以配合kubectl rollout status观察结果。如果回滚过程中发现选错了版本,你可以立马再执行一次 undo 回滚到另一个方向,反正它本质就是一次普通发布。
4.3 回滚时的观察点
回滚发布看起来很简单,但线上操作时不能只盯一条命令。我回滚时的习惯是同时观察四个维度:
kubectl get deployment demo-app -n demo -w kubectl get rs -n demo -l app=demo-app kubectl get pods -n demo -l app=demo-app kubectl get events -n demo --sort-by=.lastTimestampDeployment 的AVAILABLE列如果一直低于期望副本数,说明回滚也没顺利;RS 列表里新 RS 和旧 RS 的副本数变化能告诉你滚动到哪一步了;Pod 的状态和 Events 会直接暴露具体失败原因。我从不在回滚时只看rollout status到成功就关掉终端,因为“成功”只代表控制器按预期完成了流程,不代表业务已经恢复。最好在回滚后继续观察几分钟监控指标,确认错误率降下来,再宣布“回滚完成”。
还有一个容易被忽略的点:如果新版本发布时镜像 tag 是不可变的,但旧版本镜像已经被覆盖或者从仓库删了,回滚也会失败。生产环境尽量保证历史版本镜像长期可用,至少保留最近几个版本,别因为清理镜像省成本,把回滚后路堵死。
5. 线上排查实录与高频问题速查
5.1 滚动更新卡住的典型现场
有次我在测试环境模拟发布一个故意把健康检查写错的应用,现场是这样的:kubectl get deployment里DESIRED是 3,CURRENT可能是 4,AVAILABLE只有 2,Pod 列表里新版本 Pod 一直不是Running就是CrashLoopBackOff,旧版本 Pod 也不减少。
这正是滚动更新卡住的典型状态。原因通常有三种:资源不足导致 Pod 一直 Pending;映像拉取失败导致 Pod 处于ImagePullBackOff;readiness 探针一直失败导致新 Pod 永远不被认为 Ready,控制器不敢继续缩容旧副本。
排查顺序我建议是:先kubectl get pods -l app=demo-app看整体状态,再kubectl describe pod <pod-name>看具体事件,最后查 Deployment 和 RS 的 condition。不要一上来就重启 Pod,先看原因。如果发现是 Pending,用kubectl describe pod看节点有没有资源压力;如果是 ImagePullBackOff,看 Events 里的拉取错误;如果是探针失败,看 Pod 日志和/healthz接口返回。
如果卡住的服务正在线上,应急处置不是直接删 Deployment,而是用kubectl rollout undo先把流量拉回旧版本。等新版本的问题排查清楚,再重新发布。滚动卡住时 undo 通常能顺利执行,因为它是基于历史版本模板,不需要依赖当前新 RS 里面的问题代码。
5.2 镜像拉取失败的原因链
镜像拉取问题是新手上路最高频的故障之一,现象是 Pod 状态显示ImagePullBackOff或ErrImagePull。原因链条其实不长:镜像名称拼写错误、镜像 tag 不存在、私有仓库需要认证、节点访问不了仓库。
排查时先执行:
kubectl describe pod <pod-name> -n demo在 Events 里能看到类似Failed to pull image的提示。如果指向认证失败,需要在 Deployment 的 template 中加imagePullSecrets:
spec: template: spec: imagePullSecrets: - name: registry-keyregistry-key需要提前在命名空间创建好。如果节点本身访问不了镜像仓库,就得检查节点上的 DNS、路由和仓库白名单。另一个隐蔽坑是镜像 tag 用latest:节点上如果已经有本地缓存,即使远端latest更新了,Pod 也不一定会重新拉取,要看imagePullPolicy的取值。对latest默认是Always,但如果你指定了具体版本号,默认策略是IfNotPresent,本地有缓存就会用缓存。生产发布尽量用语义化 tag,别依赖latest,否则更新和回滚的语义都会变得很模糊。
5.3 Readiness 探针失效与发布“假成功”
readiness 探针是滚动更新能否正确判定“新 Pod 可用”的关键。如果没配 readiness 探针,Pod 只要进程启动成功,就会被标记为 Ready。结果是滚动更新可以在几秒内完成,坏版本瞬间全量上线。很多严重的发布事故就是这种“发布太快”导致的。
配置探针时要注意几个细节。initialDelaySeconds要给够,应用启动慢但探针检查太早,会导致 Pod 反复被杀死。periodSeconds建议不低于 5,太频繁会给应用造成不必要的压力。timeoutSeconds和failureThreshold也要按应用特性设,不能只看默认值。
readiness 和 liveness 分开配置,不要共用同一个接口:readiness 失败只是不把 Pod 加入流量,liveness 失败会直接重启容器。如果把一个重负载接口同时用作两种探针,接口一慢,Pod 可能还没 ready 就被 kill,形成恶性循环。我习惯 readiness 指向一个业务就绪检查接口,liveness 指向一个不需要依赖外部组件就能响应的轻量接口。
5.4 避坑清单与个人经验
整理一个实际问题速查表,都是我踩过或者帮别人排查过的:
| 现象 | 常见原因 | 处理思路 |
|---|---|---|
| 更新后 Pod 一直 Pending | 节点资源不足、亲和性规则太严格 | describe pod 看 Node 事件,评估调度条件 |
| ImagePullBackOff | 镜像名或 tag 错、私有仓库未认证 | describe pod 看拉取错误,补齐 imagePullSecrets |
| 滚动更新卡住,AVAILABLE 不足 | readiness 探针失败 | 看新 Pod 日志和健康检查接口,必要时 undo |
| undo 后没有效果 | 目标 Revision 已被清理,或 Deployment 处于 pause | 检查 history 列表,先 resume 再回滚 |
| 发布成功但业务异常 | 探针配置太宽松,坏版本已全量 | 回滚后收紧探针,延长灰度观察时间 |
| 历史版本消失 | revisionHistoryLimit 太小或为 0 | 调整 revisionHistoryLimit,重新规划版本保留策略 |
最后分享一个我在实际运维中养成的小习惯:任何一次发布或回滚,我都会先记录当时的基线状态,包括kubectl rollout history里的 Revision、当前镜像版本、重点监控指标。别依赖自己的记忆。很多回滚事故不是因为命令不对,而是操作太快,根本没确认自己回滚到了哪里。每次执行kubectl rollout undo之前,先敲一遍kubectl rollout history deployment/demo-app -n demo,看清楚--to-revision的编号,再动手。这个习惯救过我太多次,也希望你早点养成。