先说个真实场景。我们环境里有个业务服务,早期偷懒,直接把Pod的yaml写好就用kubectl apply往上怼,够简单也确实能跑。结果某天宿主机出问题,Pod被驱逐,业务直接断了几十分钟。那一晚我在电脑前一边看告警一边想:如果当时用的是Deployment,最多十几秒一个新Pod就补上来了,根本不至于这么狼狈。从那以后,凡是K8s里跑应用,我只用Deployment,裸Pod只留着做一两次性的调试工具。
这篇文章就把K8s里用Deployment部署Pod这件事讲透,重点围绕三个最常用的操作:部署、滚动更新、回滚。适合刚开始学K8s的运维同学,也适合从开发转过来的朋友,帮你把Deployment的底层逻辑和实操命令一次性串起来,少踩几个我没少踩的坑。
1. 先别急着写yaml:为什么在K8s里部署Pod首选Deployment
1.1 裸Pod到底哪里不省心
很多人初次接触K8s时,学到的第一个工作负载对象就是Pod。Pod是最小的调度和运行单元,这点没错,但它离"生产可用"还差得很远。
裸Pod有几个天生缺陷。第一,它没有自愈能力。Pod所在的节点挂了,Pod不会自动在你其他节点上重建,控制器不管它,调度器也懒得管它,它就这么"消失"了。第二,它不支持漂移。Pod的IP会变、节点会变、生命周期是短暂的一次性对象,你如果拿裸Pod跑无状态应用,每次更新都得手动删掉再创建,运维体验约等于石器时代。第三,它没法做滚动更新和回滚。你想发布新版本,只能先删旧Pod再建新Pod,中间服务必然有一段时间不可用,更谈不上"出问题马上退回去"。
当然,裸Pod不是没有价值。跑临时任务、调试镜像、做一次性批处理的时候,直接起个Pod反而干净利落。但一旦涉及持续对外提供服务的应用,就必须把生命周期托管给高级控制器。
1.2 Deployment的三层监管结构:Controller、ReplicaSet与Pod的关系
Deployment之所以是K8s里最常见的应用部署方式,核心在于它把"你要什么"和"系统怎么做"彻底分开了。整条链路分三层:
最顶层是Deployment,它只负责描述期望状态,比如"我要4个副本,镜像版本为v2,容器端口是8080"。它不直接管Pod,而是通过管理ReplicaSet来间接管理。
中间层是ReplicaSet,它负责保证Pod副本数始终等于期望值。多一个就删,少一个就补。这个"补"的过程就是它一直在盯着Pod数量,发现掉队就立刻创建新的。
最底层是Pod,承载容器,真正干活的工人。这样设计的好处是职责单一、互不干扰。Deployment更新版本时,不需要自己去逐个操作Pod,只需要把Pod模板换掉,系统会新建一个ReplicaSet,然后自动交接流量和副本数。
可以把这个结构理解成三层管理:Deployment是包工头,只对甲方负责,说好"给你几份活"就行;ReplicaSet是小组长,死盯着工人数量;Pod就是工人,谁走了谁顶上,小组长说了算。
1.3 裸Pod与Deployment的选择对照表
为了让你更直观地做决定,我把两者拉一张表:
| 对比项 | 裸Pod | Deployment |
|---|---|---|
| 节点故障时自动重建 | 不支持 | 支持,自动在新的可用节点拉起 |
| 扩缩容 | 手动创建/删除 | 改replicas字段即可 |
| 应用更新 | 手动删旧建新 | 滚动更新,逐步替换 |
| 回滚能力 | 无 | 通过rollout历史记录秒级回滚 |
| IP稳定性 | 每次变化 | 配合Service获得固定入口 |
| 典型场景 | 调试、一次性任务 | 持续运行的无状态应用 |
我在实际操作中的结论是:只要应用是"长期在线、需要持续可用"的,一律用Deployment。裸Pod那套留在心智模型里当底层原理理解就够了。
2. 部署一个Pod的全过程:从yaml到Pod Ready
2.1 一份最小可用Deployment的yaml逐行解读
先看一份最常见的Deployment配置文件,别嫌简单,核心概念全在里面:
apiVersion: apps/v1 kind: Deployment metadata: name: web-demo labels: app: web-demo spec: replicas: 3 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: web-demo image: registry.example.com/web-demo:v1 ports: - containerPort: 8080这段配置里有几个必须搞清楚的细节。
apiVersion: apps/v1是Deployment在稳定版本的正确API组。早期版本用的是extensions/v1beta1,现在早就废弃了,我见过不少人从老教程复制出来还是旧写法,直接apply会报错。
spec.replicas: 3很容易理解,就是期望副本数。但真正需要动脑的是selector.matchLabels和template.metadata.labels的关系。template里面定义的是Pod的标签,而selector是Deployment用来"识别"自己手下Pod的标准。注意,selector一旦创建就不能随便改,因为它决定了ReplicaSet怎么认领Pod。如果两个Deployment的selector重叠,就会出现互相抢Pod的混乱局面。
template的部分本质上就是一份Pod模板,里面定义了容器的镜像、端口、环境变量等。K8s控制器就是拿着这份模板去创建新Pod的。很多新手会忽略template.metadata.labels这一层,导致selector匹配不上,Deployment一直创建不出Pod。
2.2 apply之后,先看这四类资源
配置文件搞定后,执行部署:
kubectl apply -f deployment.yaml这一步之后不要傻等,先用一组命令确认状态。我固定的四条如下:
kubectl get deployment web-demo kubectl get rs kubectl get pods -o wide kubectl rollout status deployment/web-demokubectl get deployment看的是Deployment整体状态,包括期望副本数、当前副本数、可用副本数。kubectl get rs看ReplicaSet的生成情况,第一次部署会新建一个RS,比如web-demo-xxxxx。kubectl get pods -o wide能直接看到每个Pod被调度到了哪个节点、IP是什么、容器有没有Ready。kubectl rollout status则是Docker时代没有的"发布状态跟踪器",它会一直等着,直到发布完成,输出类似deployment "web-demo" successfully rolled out。
如果Pod一直卡在Pending或者ImagePullBackOff,第一反应是跑kubectl describe pod <pod名>。describe的输出里藏着大量信息:调度失败的报错原因、镜像拉取失败的具体消息、事件列表的最后几条。别一上来就删Pod重启,先看事件。
2.3 提一个实验:删除一个Pod,看看Deployment如何"补位"
这块我建议你亲手做一次,比看十篇文档都有用。
kubectl delete pod web-demo-xxxxx注意,我删的是某个具体Pod的名称。过几秒再执行kubectl get pods,你会看到一个全新名字的Pod出现在列表里,状态迅速变成Running,旧Pod彻底消失。这就是ReplicaSet的补偿逻辑:它发现当前Pod数量变成了2,期望值却是3,于是立刻拿着Deployment的模板补了一个新Pod。
这个实验能让你真正理解"声明式"这三个字的含义——你不用告诉系统怎么做,只需要告诉它"我要3个",系统就会想尽办法去对齐。相比之下,裸Pod删除后就是真的删了,系统无动于衷。
实战中,这个"自动补位"的机制也是Deployment承载业务稳定性的基石。每次发布新代码或者调整镜像,都是基于这一套机制在运作。
3. 应用更新:滚动更新不是玄学,是一套精密的版本交接
3.1 触发更新的三种姿势与各自的适用场景
Deployment更新Pod模板的方式很灵活,最常用的是这三种:
第一种,直接改配置文件然后重新apply。你修改本地的deployment.yaml,比如把image: registry.example.com/web-demo:v1改成v2,然后执行一次kubectl apply -f deployment.yaml。这是最规范的做法,适合所有场景,因为你始终保留了配置文件的单一事实来源。
第二种,用命令行直接设置镜像:
kubectl set image deployment/web-demo web-demo=registry.example.com/web-demo:v2这个命令适合在紧急修复bug时快速替换镜像,不需要临时改文件。注意set image后面有两个名字:左边web-demo是Deployment名称,右边web-demo是容器名,容器名必须和yaml里的containers.name一致,否则报错。
第三种,用kubectl edit deployment/web-demo直接在线编辑Deployment对象,改完保存立即生效。这个适合临时微调参数,比如某个环境变量改一下,但修改的过程中没有经过版本管理,容易留下配置漂移,不推荐常态化使用。
不管用哪种方式触发,只要Deployment的spec.template发生了变更,都会产生一次新的发布。
3.2 滚动更新中途到底发生了什么:新旧ReplicaSet的交接过程
假设我们有4个副本,执行更新命令后,Deployment会立刻创建一个新的ReplicaSet,然后新旧两个RS并行运行一段时间。这个"并行"和"逐步交接"的过程,就是滚动更新的核心,也是K8s相比传统"停机发布"最优雅的地方。
说得再具体一点。默认策略下,系统会先启动一个新的v2 Pod,等它通过健康检查、状态Ready之后,再杀掉一个旧版v1 Pod。然后继续拉一个v2 Pod,再杀掉一个旧版v1 Pod,如此循环,直到所有Pod都变成v2,旧RS的副本数归零。
整个过程我只观察不干预,服务几乎无缝切换。如果你留意kubectl get pods的输出,你会在某一刻看到新旧版本Pod同时在列表里,可能v1还剩1个、v2已经有3个,一眼就能看出"正在滚动"。
这份精细的操作,正是Deployment最值钱的地方。它把"版本切换"从一把梭变成了可控的、可暂停、可恢复、可回退的工程流程。
3.3 maxSurge与maxUnavailable决定"更新幅度",不要默认躺平
滚动更新并不是傻乎乎一次换一个,过程中同时最多可以超多少、允许少多少,由两个参数控制。
maxSurge控制的是更新过程中,最多允许超出期望副本数的Pod数量。默认是25%,比如期望4个副本,那么滚动过程中最多可以有5个Pod同时存在。
maxUnavailable控制的是更新过程中,最多允许有多少个Pod不可用。默认也是25%,即4个副本中最多允许有1个暂时不可用,至少保证3个可用。
这两个参数是"安全阀"和"速度阀"。如果你的应用对抖动极其敏感,可以设置成maxUnavailable: 0、maxSurge: 1,代价是更新速度变慢,因为系统必须先起新Pod、确认Ready后才会缩旧Pod。反过来,如果希望更新快一点,可以把maxUnavailable调大,但可用Pod数会短暂下降,有容量风险。
我建议在配置里显式写出这两个字段,不要默认躺平。尤其是业务高峰期做发布的时候,maxUnavailable: 0这种严格策略能保命。
spec: replicas: 4 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 03.4 readinessProbe是更新成败的闸门
滚动更新里最容易被忽略、影响却最大的是就绪探针readinessProbe。它决定了系统怎么判断"新Pod是否可以对外服务"。
如果没有配置readinessProbe,Pod只要容器进程启动成功就会变成Ready,K8s就以为它能承接流量了。如果新版本应用本身启动就要白屏十秒、或者启动后接口暂时不响,这十秒内流量一打进来就是一批报错。
配置一个合理的readinessProbe并不复杂:
containers: - name: web-demo image: registry.example.com/web-demo:v2 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 3 periodSeconds: 5这样系统在滚动更新时,只有新Pod的/healthz接口返回成功,才会继续缩旧Pod、上前新流量。如果新版本一直不健康,滚动更新会卡在原地,当前可用的旧Pod数量保持在阈值内,不会造成全量宕机。
我之前有一次发布,新版本镜像在启动时依赖一个临时文件没有自动创建,Pod反复CrashLoopBackOff。因为加了readinessProbe,滚动更新自动停住,旧版本Pod还稳稳扛着流量,我回滚后业务几乎没受影响。如果没这个探针,后果就是流量全打到坏Pod上,整个服务雪崩。
4. 回滚操作:十五分钟内完成一次安全的版本还原
4.1 revision是怎么生成的,rollout history能查到什么
Deployment每次更新Pod模板,系统都会把这次变更记录为一个revision。你可以通过命令查看历史记录:
kubectl rollout history deployment/web-demo输出会显示类似这样的信息:
REVISION CHANGE-CAUSE 1 kubectl apply --filename=deployment.yaml 2 kubectl set image deployment/web-demo web-demo=registry.example.com/web-demo:v2每个revision本质上都对应了当时的Pod模板快照,这也是Deployment能回滚的底牌。如果没有记录、没有快照,回滚就成了一纸空谈。
需要注意的是,revision编号是递增的,每产生一次模板变更就加1。哪怕这次变更和很久以前的某个版本一模一样,也会生成一个新的revision号。很多人在回滚到老版本之后看到revision又涨了,以为搞错了,其实这是正常现象——回滚动作本身也是一次变更。
4.2 回滚命令的完整链路:undo之后依次看什么
回滚最常见的场景是新版本上线后出现异常,需要立刻恢复。最直接的操作是:
kubectl rollout undo deployment/web-demo这条命令会把Deployment的Pod模板回退到最近一次revision,也就是上一个版本,然后同样触发滚动更新过程。
执行完之后,立刻执行状态确认:
kubectl rollout status deployment/web-demo再查看Pod列表:
kubectl get pods -o wide你会看到新版本的Pod逐渐被替换成旧版本。等rollout status提示成功,再访问服务确认接口正常,整个过程几分钟内完成。
有一点要提醒:不要以为undo会瞬间还原。它一样走滚动更新的流程,受maxSurge和maxUnavailable约束。如果业务正在高负载运行,回滚也可能需要一些时间,别慌,盯住rollout status就行。
4.3 指定版本回滚与change-cause记录,别在紧急时翻旧账
如果你回滚时并不想要"上一个版本",而是想明确回到某个具体的revision,用--to-revision参数:
kubectl rollout undo deployment/web-demo --to-revision=1这个命令在跨多个版本时非常有用。假设你连续发了三个版本,v3出问题了,但你不想回到v2,而是想直接回到v1,那就指定revision编号。
我要多说一句关于变更原因记录的问题。老版本的kubectl apply --record可以在历史里记录变更命令,但现在的新版Kubectl已经逐渐弃用--record了。如果你依然想在任何revision里都能追溯到"这个版本到底是为什么发的",我现在的习惯是直接在做变更前给Deployment打个注解:
kubectl annotate deployment/web-demo kubernetes.io/change-cause="fix: 修复登录接口超时问题"这个注解会直接出现在kubectl rollout history的CHANGE-CAUSE列里。线上环境多版本并行、多同事操作的时候,这个习惯能让你在凌晨三点排查问题时清清楚楚知道该回滚到哪一版,而不是靠猜。
5. 实战中踩过最深的三个坑,和现在固定的操作自查流程
5.1 坑1:selector和labels不匹配,Pod起来了却不是你的
我第一次写Deployment时,模板里Pod的labels写的是app: my-web,而selector里写的是app: my-web-v2。结果apply后Deployment一直显示unavailable,却没有任何Pod被创建出来。
排查了很久才明白:Deployment的ReplicaSet创建Pod之前,会先检查创建的Pod的labels是否匹配selector。如果不匹配,系统就认为这个Pod不在自己的管理范围内,警务着不创建,并一直重复创建、删除的循环。整个过程看起来很诡异,日志也没有明显的错误。
后来我的做法是:把selector和template的labels保持完全一致,除非有多个Deployment共享标签的场景,否则直接复制粘贴上面的app: web-demo就行。少用app: xxx-web和app: xxx-web-v2这种"稍微不一样"的写法,很容易翻车。
5.2 坑2:镜像tag写latest,回滚时连"上一个版本"都没有
有段时间我为了方便,镜像tag统一用latest。每次更新也只是把新代码push上去,然后重新apply。
刚开始没问题,后来上线了一个有问题的版本,我想回滚,执行kubectl rollout undo,结果回滚前后都是同一个镜像latest。因为Deployment发现Pod模板里的镜像没变,它根本不会触发任何更新操作,历史revision里存的全是"同一个镜像"——等于没有任何可以回退的版本。那个"上一个版本"的镜像早就被覆盖了,连镜像仓库里都找不到旧内容。
从此我立了一条规矩:任何部署到集群里的应用镜像,tag必须用语义化版本号,比如v1.0.1、commit-abc123,禁止使用latest。这不仅是回滚的前提,也是所有排障能对齐代码版本的基石。
5.3 坑3:把maxUnavailable设成1,服务无端抖动半个钟
有一回优化滚动更新速度,我把maxUnavailable设成了1,期望副本数2。更新时,系统先把一个旧Pod删掉,再创建一个新Pod。新Pod的健康检查没通过,状态一直NotReady,而唯一剩下的旧Pod还在扛着所有流量,负载到了临界值,接口延迟明显上升。整个集群告警响了一片。
问题不在于参数本身,而在于我把"剩余可用副本数"压得太低,新Pod一卡,整个服务容量就只剩下一个Pod在撑。后来我对于这种关键业务改成maxUnavailable: 0,配合readinessProbe,让系统先起来新Pod再缩旧Pod,可用容量始终维持在100%。速度慢一点没关系,稳定性优先。
5.4 我现在部署应用走的三步自查流程
踩完这些坑之后,我现在每次发布或回滚,固定按照下面这套流程走,省心很多。
第一步,部署前检查配置。重点是selector.matchLabels与template.metadata.labels是否一致,镜像tag是否可回溯,是否配置了readinessProbe,maxUnavailable是否合理。
第二步,发布后监控。执行kubectl rollout status盯着进度,同时用kubectl get pods -o wide看Pod从哪里拉起、IP和节点分布。发布完成后跑一次接口健康检查,确认业务层OK。
第三步,回滚前冷静确认。先rollout history看revision和change-cause,确认要回滚到哪个版本,再执行undo或指定--to-revision。回滚期间继续盯rollout status,别一敲完命令就离开工位。
这里还有一个我后来养成的习惯:在真正大流量上线前,先在一个小规模的namespace或者副本数较少的集群里跑一次完整的滚动更新和回滚演练。演练的目的不是看命令会不会成功,而是确认业务的健康检查、启动时间、容器退出逻辑能不能配合上滚动更新的节奏。这些细节只有在真实演练里才会暴露出来。
K8s的Deployment看着简单,翻来覆去就那几个概念,但真要把它用顺,靠的是把部署、更新、回滚这三条主线上的每个小细节都理解到位。希望这篇分享能让你少走点弯路,至少在下次深夜出问题时,能从容地敲下那条回滚命令。