1. Deployment 到底解决了什么问题
先抛个很现实的问题:如果你在生产环境直接跑一个裸 Pod,业务一崩就没了,流量一涨也没人管,手动去重建 Pod 的操作够你半夜爬起来几次的。K8s deployment 这套控制器出现,就是为了解决这类 "Pod 没人管" 的问题。
Deployment 是 Kubernetes 里最常用、也最应该先掌握的一种工作负载控制器。你可以把它理解成"Pod 的运营经理":它盯着 Pod 的数量对不对、版本该不该更新、更新过程中有没有把服务搞挂、挂掉的 Pod 要不要自动拉起来。你在 yaml 里写一句"我要 3 个副本,用 nginx:1.24 这个镜像",剩下的事情由 Deployment 负责落到实处。
这篇文章写给谁?给那些已经会用 kubectl 跑通一个 demo、但对 Deployment 的理解还停留在"能用就行"阶段的开发者。也写给准备考 CKA 或者正在搭生产集群的运维同学。我会把 Deployment 的底层设计、yaml 字段含义、滚动更新机制、回滚操作、以及我踩过的坑一次讲完,尽量用"人话"解释,不堆概念。
我的建议是:这篇内容值得你从头读一遍,再存下来当操作手册。很多细节,比如 maxSurge 和 maxUnavailable 的计算方式、rollout 历史版本怎么保留,是平时看文档容易忽略、但线上出问题时又特别关键的地方。
2. 整体设计与工作原理:Deployment 不是一个人在战斗
2.1 Deployment、ReplicaSet、Pod 三层结构
先搞清楚一件事:Deployment 并不直接创建 Pod,它管的是 ReplicaSet,再由 ReplicaSet 去创建和维持 Pod 副本数。三层结构大概是:
Deployment -> ReplicaSet -> Pod
这个设计不是多此一举,它恰恰是滚动更新能够实现的根基。每当你修改 Deployment 的 Pod 模板(比如换了镜像版本),Deployment 会生成一个新的 ReplicaSet,把副本数慢慢从老的 ReplicaSet 向新的 ReplicaSet 迁移。整个过程是"叠加"而不是"推倒重建",这样服务就不会断。
举个例子:某公司把一个支付服务部署为 10 个副本,旧版本是 ReplicaSet A。现在要把镜像从 v1.0 升级到 v1.1,Deployment 会创建 ReplicaSet B,先扩容 B 到 1 个副本,等它 ready 了,再把 A 缩到 9 个,再扩 B 到 2 个……如此交替,直到 B 达到 10 个、A 降到 0 个。这个动作就是我们常说的"滚动更新"(RollingUpdate)。
2.2 控制器循环:K8s 的"盯盘"机制
Deployment 之所以能保证最终状态和你声明的一致,核心在于控制器模式。K8s 里几乎所有控制器都遵循同一个逻辑循环:
- 期望状态(Desired State):来自你提交的 yaml,比如"副本数=3、镜像=nginx:1.24"。
- 当前状态(Current State):来自 API Server 中实时记录的对象信息。
- 差异比对:控制器不断比较期望和当前是否一致。
- 调谐动作:不一致就执行操作,比如创建 Pod、删除多余 Pod、滚动升级等。
这个过程是持续进行的,不是"执行一次就完事"。哪怕你手动删了一个 Pod,控制器也会立刻补一个新的;哪怕有人改了副本数,控制器也会调谐到新的期望值。
注意:Deployment 的"自愈"能力只保证副本数符合期望。如果 Pod 一直 CrashLoopBackOff(反复崩溃重启),ReplicaSet 也只会反复重建,不会自动帮你修 bug。它保证的是"数量达标",不保证"业务健康"。
2.3 更新策略:Recreate 和 RollingUpdate
Deployment 支持两种更新策略,写在 spec.strategy.type 字段里:
| 策略类型 | 行为 | 适用场景 |
|---|---|---|
| Recreate | 先删掉所有旧版本 Pod,再创建新版本 Pod | 数据初始化重、无法多版本并存的场景 |
| RollingUpdate | 滚动替换,边删边建,保证服务始终有可用副本 | 大多数无状态服务,默认策略 |
RollingUpdate 是默认值,也是生产环境最常用的。但有两个参数你必须搞明白,否则更新行为可能跟你预想的不一样:
- maxSurge:更新过程中最多允许超出期望副本数的 Pod 数量或比例。可以是绝对值(如 2)或百分比(如 25%)。
- maxUnavailable:更新过程中最多允许不可用副本的数量或比例。同样支持绝对值和百分比。
计算公式是这样的:如果期望副本数为 10,maxSurge=25%,maxUnavailable=25%,那么在更新过程中,系统允许的 Pod 总数最多为 10 + ceil(1025%) = 13 个,允许不可用的最多为 floor(1025%) = 2 个。也就是说,在任意时刻至少有 8 个可用 Pod,最多可以同时多出 3 个临时 Pod。
这两个值怎么配?我自己的习惯是,对延迟敏感的核心服务,设置 maxUnavailable=0,maxSurge=1 或 2,确保更新过程中旧版本始终全部在线,虽然速度慢一点,但最稳。对非核心服务,可以适当放宽 maxUnavailable,加快发布速度。
3. yaml 核心字段解析与配置要点
3.1 一个最小可用 Deployment 的完整拆解
先看一个最基础的示例,别急着复制,我会一个字段一个字段解释。
apiVersion: apps/v1 kind: Deployment metadata: name: demo-web namespace: default labels: app: demo-web spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: demo-web image: nginx:1.24 ports: - containerPort: 80 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 3 periodSeconds: 5逐段拆解:
apiVersion 必须是 apps/v1,这是 Deployment 当前稳定的 API 版本。kind 固定是 Deployment。metadata.name 是 Deployment 对象的名称,全局唯一(在同一个 namespace 内)。labels 是给 Deployment 本身打的标签,规划好标签体系对后期的筛选和监控非常重要。
往下看 spec:
- replicas:期望副本数,不写默认是 1。
- selector.matchLabels:这个字段非常关键,它决定了 Deployment 管理哪些 Pod。它必须与 template 里的 labels 匹配,而且一经创建就不可修改(这是硬限制,改了会报错)。记住一条经验:selector 的标签要选"稳定不变"的,别把版本号、日期这类经常变的标签写进去。
- template:Pod 模板,描述要创建的 Pod 长什么样。template 里面的 labels 一定要跟 selector 匹配,否则 ReplicaSet 创建的 Pod 不受管控。
3.2 探针配置:决定流量是否打到 Pod 上
readinessProbe(就绪探针)和 livenessProbe(存活探针)是 Deployment 配置里最容易出问题的两个地方。
readinessProbe 的作用是告诉 K8s"这个 Pod 能不能接流量"。只有 readiness 探针通过了,Pod 才会出现在对应 Service 的 Endpoints 里,才会被 Service 转发流量。如果探针失败,K8s 不会重启 Pod,只是把 Pod 从负载均衡池中摘除。
livenessProbe 的作用是判断"这个 Pod 要不要重启"。如果探针失败,kubelet 会杀掉容器并按照 restartPolicy 重启。
这两者的区别新手经常搞混。简单说:readiness 管"要不要给你流量",liveness 管"要不要弄死你"。
配置探针时有几个常见误区:
- 初始延迟(initialDelaySeconds)设置太短,应用还没启动完成就被杀,导致 CrashLoopBackOff。
- 探针路径没考虑应用的启动耗时,建议先用 kubectl exec 手动 curl 一下,看看应用真正 ready 需要多久。
- 探针超时时间(timeoutSeconds)默认 1 秒,很多应用在高峰期响应会慢,频繁失败,可以在 yaml 里调大到 3 或 5 秒。
我踩过一个真实的坑:某服务里有个定时任务会在整点占用大量 CPU,liveness 探针响应超时,Pod 被反复重启,结果业务负载更高,形成恶性循环。后来把探针路径换成一个轻量的内存态接口,并且把 timeoutSeconds 调大,问题才解决。探针一定要探"轻量、稳定"的端点,不要探那些依赖外部服务的接口。
3.3 资源 requests 和 limits 怎么定
resources 配置是另一个容易踩坑的地方。requests 是调度器用来做 Pod 调度决策的依据,也就是说 node 上必须至少有这么多资源才能调度 Pod;limits 是容器运行时层面的硬限制,超过就可能被杀死或限流。
一个需要记住的规则:limits 尽量不要比 requests 大太多。如果 requests 设 100m CPU、limits 设 4 核,某个 node 上可以调度 10 个这种 Pod,但当它们同时打满时,CPU 竞争会非常严重,而且 node 上实际资源会被撑爆。Node 的内存是超卖不了的(内存超限就是 OOM 被 kill),CPU 可以超卖但会影响性能。
我的建议:先跑一段时间看监控,再定 requests。一般把 CPU requests 设为日常峰值的 70% 左右,limits 设为 raply 峰值的 1.2 到 1.5 倍;内存 requests 按常驻内存来,limits 给常驻内存的 1.5 倍左右,留出余量。没有监控数据就拍脑袋配的,迟早要回火。
提示:配置 resources 之前先看看集群里还有多少可分配资源。kubectl describe node 可以看到每个节点的 Allocatable 和已分配情况。很多新手明明每个 Pod 的 requests 都不高,但 Deployment 一直 Pending,查下来是节点资源不够。
4. 实操全过程:从创建到回滚
4.1 创建 Deployment 的几种方式与选择建议
创建 Deployment 一般有三种方式:
# 方式一:命令行直接创建,适合快速测试 kubectl create deployment demo-web --image=nginx:1.24 --replicas=3 # 方式二:从文件创建,适合一次性导入已有 yaml kubectl create -f deployment.yaml # 方式三:声明式应用,最推荐生产使用 kubectl apply -f deployment.yamlcreate 和 apply 的区别我用一句话讲清楚:create 是"创建新对象",如果对象已存在会报错;apply 是"声明期望状态",对象不存在就创建,存在就对比差异并更新。生产环境强烈建议只用 apply,配合 Git 仓库管理 yaml,任何变更都走 review,回滚也方便。
如果用 create deployment 命令直接生成,K8s 会附带一些默认参数,比如 restartPolicy=Always、无探针、无资源限制。测试可以,生产不建议直接用命令行,还是得写完整 yaml。
创建完成后查看状态:
kubectl get deployment demo-web kubectl get rs kubectl get pods -l app=demo-web kubectl rollout status deployment/demo-webrollout status 这个命令可以实时看到滚动更新的进度,格式是"Waiting for deployment spec update to be observed...",直到输出"deployment "demo-web" successfully rolled out"。
4.2 扩缩容操作与 HPA 自动伸缩
手动扩缩容是最常见的操作。三种方式:
# 方式一:直接命令 kubectl scale deployment demo-web --replicas=5 # 方式二:修改 yaml 后 apply # 把 replicas 改成 5,再执行 kubectl apply -f deployment.yaml # 方式三:动态更新(edit 是最直接但最容易手误的方式) kubectl edit deployment demo-web从生产规范角度,我更推荐方式二:所有变更都体现在 Git 里,有迹可循。kubectl edit 在排查问题时应急用一下就好,但如果长期依赖 edit 直接改线上配置,早晚会出事故——改错了都不知道是谁改的。
如果是应对流量突增,可以配合 HPA(HorizontalPodAutoscaler)实现自动扩缩容。这里不展开讲 HPA 的完整配置,只提一点:HPA 要基于准确的数据源工作。如果你的集群没有安装 metrics-server,HPA 拿不到 CPU 指标,扩缩容就不会生效。用 kubectl top nodes 看不了指标,优先排查是不是 metrics-server 没装。
4.3 更新镜像与滚动更新过程观察
更新镜像最常用的方式:
# 直接指定新镜像 kubectl set image deployment/demo-web demo-web=nginx:1.25 # 或者修改 yaml 后 apply kubectl apply -f deployment.yaml注意 set image 的格式是"容器名=镜像名",不是直接用 Deployment 名。如果记不清容器名,先 kubectl describe deployment demo-web 查看 Containers 字段。
更新完成后,你可以用下面的命令观察滚动更新的整个过程:
kubectl rollout status deployment/demo-web kubectl get rs -w kubectl get pods -w实操中会看到新 ReplicaSet 的副本数逐渐增加,旧 ReplicaSet 的副本数逐渐减少,直到新 RS 达到期望副本数。这个过程里,如果发现卡住了,赶紧 kubectl describe rs 看事件信息,多半是镜像拉取失败或者探针不通过。
4.4 回滚:线上发布翻车的应急手段
Deployment 的一大好处是天然支持版本回滚。K8s 会保留 ReplicaSet 的变更历史,你可以随时回到上一个版本或指定版本。
# 回滚到上一个版本 kubectl rollout undo deployment/demo-web # 回滚到指定版本 kubectl rollout undo deployment/demo-web --to-revision=2 # 查看历史版本 kubectl rollout history deployment/demo-web # 查看某个历史版本的详细配置 kubectl rollout history deployment/demo-web --revision=2rollout history 输出里,如果没有 CHANGE-CAUSE 列,说明创建 Deployment 时没有加 --record 或 annotation 记录变更原因。现在推荐的做法是在更新命令里加 --record=true,或者在 yaml 的 metadata 里维护 kubernetes.io/change-cause 注解,方便后期排查"为什么要回滚到这一版"。
有一个细节很多人忽略:K8s 默认只保留最近 10 个 ReplicaSet 历史版本,可以通过 revisionHistoryLimit 字段调整。如果保留太少,想回到更早的版本就无能为力了;如果保留太多,会占用很多 etcd 存储和 API 资源。一般生产环境我建议保留 5 到 10 个,配一个清晰的 change-cause 记录,足够覆盖正常发布周期里的回滚需求。
回滚的机制其实就是把旧的 ReplicaSet 重新拉起来,同样走滚动更新流程,不是瞬间完成。如果回滚后还发现问题,别急着再回滚,先看一下新起来的 Pod 是不是还是异常,如果是因为镜像 tag 拉取的是旧 digest,要去确认镜像仓库里的 tag 是否被覆盖过。
4.5 暂停与恢复滚动更新
还有一个实用操作:暂停(pause)和恢复(resume)。这个用在灰度发布场景非常有用。
# 暂停 Deployment 的更新 kubectl rollout pause deployment/demo-web # 此时修改镜像、环境变量等配置,不会触发新的滚动更新 kubectl set image deployment/demo-web demo-web=nginx:1.25 # 恢复更新,刚才修改的配置开始生效 kubectl rollout resume deployment/demo-web暂停更新的作用就是"收集多个变更,然后一次性发布"。比如你既要换镜像,又要加环境变量,还要调探针参数,如果一个个 apply,每改一次就触发一次滚动更新,既慢又容易造成多次短暂的不稳定。先 pause,把所有改动做完了,再 resume,一次滚动到位。
4.6 删除 Deployment
删除也有讲究。如果你只想"停掉服务但保留 yaml 定义",推荐删 Pod 而不是删 Deployment——但这样不优雅。实际上删 Pod 后 Deployment 会重新拉起新的 Pod,达不到停服效果。
真正删除 Deployment:
kubectl delete deployment demo-web这样会把 Deployment 管理的 ReplicaSet 和 Pod 全部删除,但不会删除关联的 Service。如果你的 Service 没了后端 Pod,Service 依然存在,但访问会 Connection Refused 或者 No route to host。要清理干净,得把 Service 也一并删掉。
如果你只想"临时下线"一批 Pod,正确做法是把 replicas 改成 0,或者用 scale 缩到 0:
kubectl scale deployment demo-web --replicas=0需要恢复的时候再改成需要的副本数就行。这个操作比 delete 安全得多,尤其是当你想保留 Deployment 的配置、后续可能还要重新拉起时。
5. 常见问题与排查技巧实录
5.1 Pod 一直 Pending 或 ContainerCreating
现象:kubectl get pods 看到 Pod 停留在 Pending 或 ContainerCreating 很久。
排查思路:
kubectl describe pod <pod-name>如果事件里出现 FailedScheduling,说明调度失败。常见原因:
- 节点资源不足。看 node 的 Allocatable 和已用资源。
- nodeSelector 或亲和性配置不匹配。检查 Deployment 里有没有配置 nodeSelector、nodeAffinity、污点容忍等。
- PVC 无法挂载。如果用了存储卷,确认 StorageClass 是否存在、PV 是否满足 PVC 需求。
如果事件里出现 Failed to pull image 或 ImagePullBackOff,说明镜像拉取失败。检查镜像名是否写错、镜像仓库认证是否正确、节点上是否有足够的磁盘空间。
5.2 CrashLoopBackOff 的排查套路
这是最常见的故障之一。现象是 Pod 反复重启,状态一直是 CrashLoopBackOff。
排查步骤:
# 1. 查看 Pod 状态和重启次数 kubectl get pods # 2. 查看容器日志,应用崩溃原因一般在日志里 kubectl logs <pod-name> --previous # 3. 查看 describe 里的事件,Liveness probe failed 常见 kubectl describe pod <pod-name>三个高频原因:
- 应用启动报错(配置缺失、数据库连不上),日志里很明显。
- liveness 探针配置太激进,应用还没起来就被杀。调整 initialDelaySeconds 和 failureThreshold。
- 启动后因为资源限制被 OOM killed。用 kubectl describe pod 看 Last State 里是不是 OOMKilled,调大内存 limits 或排查应用内存泄漏。
--previous 参数很有用,因为容器重启后当前日志会被清空,只有上一个实例的日志还保留着。很多新手直接 kubectl logs 看到空日志就懵了,加上 --previous 就能看到崩溃前的输出。
5.3 滚动更新卡住,Pod 一直处于 Terminating 或 ContainerCreating
滚动更新卡住通常涉及几个原因:
- 新版本镜像拉取失败,ReplicaSet 一直无法创建新的 Ready Pod。
- readiness 探针一直失败,新 Pod 无法进入 Ready 状态,系统不敢缩旧副本。
- maxUnavailable 配置太小 + 节点资源不够,新 Pod 无法调度,系统既不能缩旧副本(因为 maxUnavailable 限制),又不能扩新副本(资源不够),死锁。
排查命令用这一套:
kubectl rollout status deployment/demo-web kubectl get rs kubectl describe rs <rs-name> kubectl get pods -l app=demo-web如果是资源死锁,把 maxUnavailable 调大一些再 rollout resume,一般能解开。如果镜像有问题,先去修镜像,再用 rollout undo 回到稳定版本。
另外有一个容易被忽略的点:Service 的 selector 一定要和 Pod 的 labels 匹配。如果 Service 选不到新的 Pod,即使滚动更新成功了,外部流量也访问不到服务。排障时先 kubectl get endpoints 看看有没有 Endpoints,没有的话去检查 Service 和 Pod 的 label 是否对得上。
5.4 ReplicaSet 历史版本过多,导致 etcd 压力大
在长时间频繁发布的服务里,revisionHistoryLimit 如果设置得很大,ReplicaSet 对象会积累几十上百个,API Server 响应会变慢。
处理办法是显式设置 revisionHistoryLimit,一般 5 到 10 个就够。如果已经积累了很多,可以手动清理不用的 ReplicaSet:
kubectl delete rs <old-rs-name>但注意,如果你只是想减少历史版本,直接删除 ReplicaSet 就可以了,Deployment 会自动管理剩下的。不过删除后对应版本的 rollout history 就看不到了,回滚不了那么早的版本。所以清理之前想好你还需要哪些历史版本,建议保留最近两三个发布周期的。
5.5 排查速查表
| 现象 | 可能原因 | 首选排查命令 | 参考处理 |
|---|---|---|---|
| Pod Pending | 节点资源不足 | kubectl describe node | 扩容节点或降低 requests |
| ImagePullBackOff | 镜像不存在/仓库认证失败 | kubectl describe pod | 检查镜像名、凭据 |
| CrashLoopBackOff | 应用启动失败或探针太激进 | kubectl logs --previous | 修复应用 / 调整探针 |
| OOMKilled | 内存 limits 过小 | kubectl describe pod | 调大 limits 或排查泄漏 |
| 滚动更新卡住 | 探针失败 / 资源死锁 | kubectl rollout status | 调 maxUnavailable / 回滚 |
| Service 无法访问 | selector 不匹配 | kubectl get endpoints | 检查 label 对应关系 |
| 变更没有生效 | 使用了 create 而非 apply | kubectl get deployment -o yaml | 改用 apply 声明式管理 |
6. 一些我个人的实操心得
Deployment 用熟了之后,你会逐渐意识到它其实是一个"发布工具",不只是"副本管理器"。发布流程、回滚策略、灰度节奏,都应该提前设计好,而不是上线前临时想。
几个经验供参考:
- 生产环境所有 Deployment 必须走 Git 仓库管理,任何变更都要有 record。线上出问题时要能回答"这次改了什么""为什么改""谁改的"。
- 探针配置一定基于真实应用启动时间。用 kubeadm 搭测试集群的过程,把应用压测一下,记录从容器启动到能正常响应的时间,再留 2 到 3 秒余量填进 initialDelaySeconds。
- 滚动更新参数不要照抄网上的模板。先想清楚你的服务能不能接受短暂不可用,再看业务峰值流量,决定 maxSurge 和 maxUnavailable 的比例。
- 回滚不是银弹。如果新版本引入了数据库结构变更,回滚代码可能不够,还要考虑数据兼容问题。发布前想好"能不能回滚""回滚后数据怎么办"。
我最初管理 K8s 集群时,也犯过一个很低级的错:把 maxUnavailable 设置成了 70%,想着"发布快一点",结果更新过程中压垮了剩余 Pod,业务直接雪崩。一次事故之后,我给自己定了个规矩:无状态服务 maxUnavailable 默认 0,maxSurge 默认 1,除非业务明确能接受短时不可用,否则不调大。
还有个细节:如果你给 Deployment 配置了 terminationGracePeriodSeconds,要注意这个时间指的是 Pod 从收到 SIGTERM 到被强制杀死的时间。应用要在这么短时间内优雅退出——把存量请求处理完、连接池关闭、临时文件清理掉。我见过很多应用直接忽略 SIGTERM,导致滚动更新时出现大量 502,这种问题从 Deployment 配置层面很难解决,需要在代码里实现优雅退出逻辑。
Deployment 是 Kubernetes 里最基础也最核心的工作负载控制器,值得花时间把它的每一个参数吃透。上面这些操作和排障方法,都是我实际用过、验证过的。你照着走一遍,基本能覆盖日常 90% 以上的场景。剩下 10% 的稀奇古怪问题,一般都在日志和事件里藏着答案,多用 describe、多查 history、多试 undo,总能找到出路。