1. 从实例出发,看懂 Deployment YAML 的全貌
拿到一个 Kubernetes Deployment 的 YAML,很多新手的第一反应是“字段太多了,不知道哪些必须改,哪些可以不管”。这个感觉非常正常,因为 K8s 的 YAML 本质上是一份“声明式配置清单”,它把“我想要什么状态”描述给 API Server,剩下的协调工作由控制器完成。而我个人在实际使用中的体感是:Deployment 的 YAML 是日常接触最多的资源清单,没有之一——因为大部分无状态服务、Web 应用、API 服务的部署都会落到 Deployment 上。它解决的问题非常直接:保证指定数量的 Pod 副本在集群中运行,并且支持滚动更新、失败回滚、水平伸缩。
先看一份完整可用的 Deployment YAML,这份配置覆盖了生产环境常见的核心字段,后面我会逐段拆解每个块的作用和背后的设计逻辑。
apiVersion: apps/v1 kind: Deployment metadata: name: demo-web namespace: default labels: app: demo-web tier: frontend spec: replicas: 3 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: demo-web image: registry.example.com/demo-web:v1.2.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8080 name: http protocol: TCP env: - name: APP_ENV value: "production" - name: DB_CONN_STRING valueFrom: secretKeyRef: name: demo-db-secret key: conn_string resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 timeoutSeconds: 3 successThreshold: 1 failureThreshold: 3 livenessProbe: httpGet: path: /livez port: http initialDelaySeconds: 15 periodSeconds: 20 timeoutSeconds: 3 failureThreshold: 3 restartPolicy: Always terminationGracePeriodSeconds: 30 nodeSelector: disktype: ssd tolerations: - key: "dedicated" operator: "Equal" value: "frontend" effect: "NoSchedule" affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - demo-web topologyKey: kubernetes.io/hostname --- apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: demo-web-hpa namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: demo-web minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70这份 YAML 拆开看其实就三大部分:Deployment 本体、Pod 模板(template 块)、HPA 伸缩规则。其中 Deployment 本体负责定义“我要几个副本”“怎么选择 Pod”“怎么更新”,Pod 模板定义“这个 Pod 里跑什么容器、给多少资源、怎么探活”,HPA 则告诉集群“CPU 超过多少就自动加副本”。
需要特别强调一个细节:selector是 Deployment 的核心枢纽,它通过matchLabels匹配 Pod 模板里的labels。这个匹配关系一旦建立,ReplicaSet 就负责维护副本数量,Deployment 只关心“当前有没有足够多的可用副本”。如果selector和 Pod 模板标签不一致,K8s 会直接拒绝创建,这一点我在文章后面的问题排查部分会专门展开。
2. 为什么用 Deployment 而不是其他控制器
很多人会问:StatefulSet、DaemonSet、Job 都能跑 Pod,为什么偏偏 Deployment 最常用?答案其实藏在“无状态”和“实例对等”这两个关键词里。
Deployment 管理的 Pod 是完全对等、可随意替换的。任意一个 Pod 挂了,ReplicaSet 会立刻创建新的 Pod 顶替,新 Pod 的 IP、节点都变了,但对外提供的服务能力不变。这种“一切皆可替换”的设计决定了它最适合 Web 服务、API 网关、后台 Worker 这类无状态应用。反过来,如果业务需要稳定的网络标识和持久化存储(比如数据库主从、消息队列节点),那要选 StatefulSet,它保证了 Pod 的序号、DNS 名称、PVC 绑定关系的稳定性。
从更新策略的角度看,Deployment 提供了滚动更新和回滚的能力,这是它区别于直接操作 Pod 或 ReplicationController 的根本优势。举个例子,我把镜像从v1.2.0升级到v1.3.0,Deployment 会创建一个新的 ReplicaSet,然后按maxSurge和maxUnavailable的约束逐步替换旧 Pod。这个过程是渐进式的——如果新版本起不来(比如启动崩溃),滚动更新会卡住,但旧的 ReplicaSet 还在,随时可以rollout undo回退。
还有一个隐藏优势:Deployment 天然支持版本历史管理。spec.revisionHistoryLimit控制了保留多少个历史 ReplicaSet 版本,默认是 10。这意味着你可以从当前版本向前回滚任意一个历史版本,这对线上事故处理非常重要。我用过一个场景:发布 v1.5.0 后连续回滚了两个版本到 v1.3.0,就是因为 v1.4.0 的配置缓存有问题,而 K8s 回滚到 v1.3.0 后直接恢复了正常。
那么什么时候不该用 Deployment?如果你发现业务实例之间需要“身份”(每个实例有固定的序号和存储),或者每个节点必须跑一个且只能跑一个实例(日志采集、节点监控),就不要用 Deployment。前者交给 StatefulSet,后者交给 DaemonSet。选错控制器会让运维变得非常痛苦,我曾经见过有人用 Deployment 部署单副本的数据库,Pod 每次重启后 IP 都变,应用连接串天天要改,最后不得不重构成 StatefulSet 才消停。
3. 核心字段逐个拆解:看懂每一行的意图
3.1 apiVersion、kind、metadata:资源身份的三要素
apiVersion决定了资源使用的 API 版本。Deployment 在现在的稳定版 K8s 中固定是apps/v1,这个从extensions/v1beta1一路演进过来的过程,已经在 v1.16 之后彻底定版。如果你的集群版本还在 v1.15 之前,那要小心旧版 API 的差异,但现在主流集群基本都不用管这个了。唯一需要警惕的是,HPA 的apiVersion是autoscaling/v2,和 Deployment 不一样,写错版本会直接报错。
kind不用多说,就是Deployment。metadata里的name是在命名空间内唯一的标识,一旦定了就不要随意改——因为 Deployment 的名字会和 ReplicaSet 名字拼接生成 Pod 的前缀。比如 Deployment 叫demo-web,那 Pod 名字就会像demo-web-7d8f9c6d4-abcde,中间那段是 ReplicaSet 的 Pod 模板哈希值。改名字意味着所有关联资源的名字都会变,对外部访问可能造成影响。
3.2 spec.replicas:副本数不是拍脑袋定的
replicas直接声明“我希望保持多少个 Pod 副本”。这个数值可以是固定的,也可以交给 HPA 动态调整。固定副本数的情况下,我建议至少 2 起步,原因你就想想:如果 1 个 Pod 挂了,Deployment 重建 Pod 需要时间(拉镜像、起进程、过就绪探针),这期间服务如果是单副本就直接不可用了。节点故障的话,需要等 Kubelet 上报 NodeNotReady,再等控制器迁移 Pod,耗时更久。
副本数和资源配额也要匹配。如果每个 Pod request 了256Mi内存,节点总内存又有限,那副本数设得再高也只能让 Pod 处于 Pending 状态。更合理的做法是用 HPA 配合requests的 CPU 使用率来动态扩缩,这也是我在演示 YAML 里同时给出 HPA 的原因。HPA 的averageUtilization: 70意思是,当所有副本的 CPU 平均使用率超过 70% 时扩容,低于安全水位时缩容,但最少不低于minReplicas。
3.3 selector 与 template:Deployment 的“左右手配合”
selector和template.metadata.labels必须匹配,这是 Deployment 正常工作的大前提。selector 是 Deployment 用来“找”Pod 的眼睛——它会创建 ReplicaSet,ReplicaSet 根据 selector 创建 Pod,Pod 带着对应标签,整个链条就串起来了。
这里的常见误区是:selector 不是“可选项”,是必填项。有些服务以在写 Deployment 时会忘记声明 selector,K8s 会在创建时直接报错提示你。我的建议是:既然必填,就把app这个标签作为项目级统一约定,所有资源(Service、Ingress、Deployment、Pod)都打上app: 项目名,后续排查问题按标签筛选特别方便。
3.4 template 里的关键配置:镜像、探针、资源、环境变量
template.spec.containers[].image是每个 Pod 真正跑起来后的业务程序来源。imagePullPolicy有三个值:Always、IfNotPresent、Never。默认逻辑是:镜像 tag 为latest时强制Always,其他 tag 默认IfNotPresent。生产环境千万别依赖latest做发布,因为每次都要重新拉镜像不说,版本不可追溯,回滚更是灾难。我踩过一次坑:某服务镜像 tag 忘了改,一直用latest,结果某次拉到了新镜像,行为变了但代码没变,排查半天才发现是镜像内容被污染了。
resources里的requests是调度器做资源分配的依据,limits是运行时资源上限。一个常见错误是只写limits不写requests,这会导致调度器按照limits来给 Pod 分配节点资源,却无法反映真实需求。比如你limits写4Gi内存,但业务通常只占500Mi,那集群会为这个 Pod 预留 4Gi,可用容量白白浪费。反过来,如果requests写太低而流量暴涨,Pod 会被 OOMKilled。所以我的经验是:用压测数据填 requests,用极限兜底值填 limits,两者都填,别耍小聪明。
就绪探针readinessProbe控制的是“什么时候把 Pod 加入 Service 的 Endpoints 列表”,存活探针livenessProbe控制的是“Pod 死了要重启它”。初学者最常犯的错是把两个探针配成同一个接口。就绪探针失败和存活探针失败的后果完全不同:前者流量不打过来,Pod 保留;后者 Kubelet 直接杀掉 Pod 重启。两者用同一个接口,一旦接口逻辑出问题,就绪探针先挂,流量被切走,但 Pod 活着;如果存活探针几分钟内周期性失败达到failureThreshold,Pod 就被杀了。这个区分特别重要,我见过因为探针路径写错导致 Pod 不断被重启、业务一直进不来的事故。
3.5 更新策略:maxSurge 和 maxUnavailable 的黄金配比
spec.strategy是 Deployment 滚动更新的核心控制区。maxSurge表示滚动更新时最多允许多创建几个新 Pod(可以是绝对数或百分比),maxUnavailable表示最多允许多少个旧 Pod 不可用(同样支持绝对数或百分比)。两个参数相互配合,本质上是“用多少额外资源换多少可用性”。
举个例子,replicas: 3,maxSurge: 25%(向上取整为 1),maxUnavailable: 25%(向上取整为 1),那么滚动更新时的 Pod 数量变化是这样的:
- 先创建 1 个新 Pod(avail +1),此时总数 4,可用 4;
- 再淘汰 1 个旧 Pod(unavail +1),此时总数 3,可用 3;
- 再创建 1 个新 Pod(avail +1),此时总数 4,可用 4;
- 再淘汰 1 个旧 Pod,以此类推,直到旧 Pod 全部替换完。
这个策略的好处是:任何时刻能用的 Pod 数不低于 2 个(3 - 1),同时允许短时间多跑 1 个 Pod 来加速滚动。如果你的服务要求“任何一个 Pod 都不能掉线”,可以把maxUnavailable设为 0,maxSurge设为 1,这样至少保证 n+1 个 Pod 在线,代价是滚动速度稍慢,且需要集群有额外资源容纳临时 Pod。
我参与过的生产实践中,比较稳妥的组合是maxSurge: 1+maxUnavailable: 0,尤其适合对可用性要求高的 Web 服务。在流量低谷期发布时,也可以放宽maxUnavailable到 1 来加速发布。而像批处理任务、非关键服务的发布,直接Recreate策略最省事——先杀掉所有旧 Pod 再起新的,但会造成短暂服务中断,不适合常驻服务。
3.6 其他重要的 Pod 调度字段
nodeSelector是个非常简单的“硬性条件调度”:只有带指定标签的节点才会被选中。这在“必须跑在 GPU 节点”或“必须跑在 SSD 节点”的场景下很好用。但它的缺点是——如果没有任何节点满足标签,Pod 就会无限 Pending,也没有“如果找不到就退而求其次”的能力。所以关键任务别硬绑nodeSelector,用亲和性更灵活。
tolerations是用来“容忍”污点的。节点上挂了一堆taint(比如专门的运维节点、GPU 节点),普通 Pod 根本不会被调度上去,只有带对应tolerations的 Pod 才能进去。这个机制和nodeSelector不是一回事:nodeSelector是主动挑选,tolerations是“被允许进入”。
affinity里的podAntiAffinity是让同类 Pod 尽量分散到不同节点上。我演示的配置里用了preferredDuringSchedulingIgnoredDuringExecution(软要求),意思是“尽量不把 demo-web 的两个副本放在同一台节点”,但不强制。如果换成requiredDuringSchedulingIgnoredDuringExecution(硬要求),集群就会在调度时强制保证,但节点不够时会一直 Pending。刚才提到 HPA 的下方,我会把topologyKey: kubernetes.io/hostname视为“以节点为维度做反亲和”,这是最常用的配置。
4. 从 YAML 到集群:完整实操流程与现场记录
YAML 写完了,关键还在于让它真正跑起来。下面是我个人在测试集群里的完整操作记录,你照着做基本不会踩坑。
4.1 创建 Deployment 并验证状态
将上面的 YAML 保存为demo-web.yaml,然后执行:
kubectl apply -f demo-web.yaml这里用apply而不是create,是因为apply支持声明式更新,后续改 YAML 再执行apply就能自动 diff 并更新资源。对比create,它面向“第一次创建”的场景,反复create同一个资源会直接报 AlreadyExists。
创建完成后,第一时间查看 Pod 状态:
kubectl get pods -l app=demo-web -o wide观察STATUS列。正常情况是Running,但刚创建时会先经过ContainerCreating状态。如果你看到CrashLoopBackOff或者ImagePullBackOff,说明镜像拉取或启动出了问题,这时候看日志定位:
kubectl logs -l app=demo-web --tail=200以后遇到任何 Pod 异常,我的套路都是三步走:先get看状态,再describe看事件,最后logs看应用日志。describe比logs更重要,因为调度失败、探针失败、拉镜像失败这类问题都记录在 Event 里,光看应用日志解决不了。
4.2 滚动更新:发布新版本镜像
假设我要发布 v1.3.0:
kubectl set image deployment/demo-web demo-web=registry.example.com/demo-web:v1.3.0这种方式适合快速改版本号。但更推荐改 YAML 文件里的image字段,然后kubectl apply -f demo-web.yaml。原因很简单:文件即事实来源,所有变更都能通过 Git 追踪,符合 GitOps 思路。
发布过程中实时观察滚动状态:
kubectl rollout status deployment/demo-web输出的Waiting for deployment "demo-web" rollout to finish: 1 out of 3 new replicas have been updated...说明滚动正在按maxSurge和maxUnavailable的节奏推进。如果一直卡住不动,说明新 Pod 一直不就绪,等一会儿会出现RolloutFailed事件。
4.3 回滚:事故场景下的救命稻草
发布出了问题怎么办?不要慌,rollout undo是事故应急的一等功臣:
kubectl rollout undo deployment/demo-web默认回滚到上一个版本。如果想回滚到特定历史版本,先查看版本历史:
kubectl rollout history deployment/demo-web输出里的REVISION就是历史版本号。然后指定回滚:
kubectl rollout undo deployment/demo-web --to-revision=2这里要提醒一个坑:每次rollout undo本身也会生成新的 REVISION。假设 REVISION 3 是你当前坏版本,回滚到 REVISION 2 后,实际版本号会变成 REVISION 4,但内容等于 REVISION 2。如果你又执行kubectl rollout undo(不带--to-revision),它回滚到的是 REVISION 3(也就是刚才那个坏版本),别搞混了。
4.4 扩缩容:手动与自动并存
手动扩容很简单:
kubectl scale deployment/demo-web --replicas=5如果配了 HPA,手动扩容要小心:HPA 会盯住 metrics 持续调整副本数。手动scale到 5,但如果 CPU 利用率不高,HPA 很快会把它缩回minReplicas=3。所以在启用 HPA 的 Deployment 上,手动scale只是临时手段,真正的容量管理交给 HPA 就够了。
4.5 清理资源
kubectl delete deployment/demo-web但注意,delete deployment只会删除 Deployment 和它管理的 ReplicaSet,不会删除关联的 Service、Ingress、Secret、ConfigMap。需要逐个清理,或者用kubectl delete -f demo-web.yaml按文件批量清理。
5. 常见问题与排查技巧实录
这部分我梳理了 Deployment 使用中最高频的坑,每一个都是我在现场踩过或围观过的,按“问题表现 → 排查思路 → 解决方案”整理成速查表。
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| Pod 一直 Pending | 节点资源不足或调度约束无法满足 | kubectl describe pod <pod-name>查看 Events,确认是 Insufficient cpu/memory 还是 nodeSelector/toleration 导致;扩容节点或调整 requests |
| ImagePullBackOff | 镜像不存在、仓库认证失败、tag 写错 | kubectl describe pod看 Events 里的具体错误;检查镜像名是否完整、是否写全 registry 地址;用docker pull手动验证镜像是否可拉取 |
| CrashLoopBackOff | 应用启动即崩溃,或存活探针杀掉 Pod 后重启仍失败 | kubectl logs看退出日志;检查启动命令依赖的环境变量、挂载文件是否就位;本地先跑通容器再上集群 |
| Pod 创建成功但 Service 访问不到 | 就绪探针失败,Pod 未加入 Endpoints | kubectl get endpoints看是否有地址;kubectl describe pod看 Readiness 探针探测结果;调整探针路径或加大initialDelaySeconds |
| 滚动更新卡住不完成 | 新 Pod 一直不就绪,maxSurge/maxUnavailable被占满 | kubectl rollout status观察进度;kubectl describe rs/<new-rs>看新 ReplicaSet 的 Events;等旧版本稳定后回滚 |
| 回滚后还是坏版本 | 没有理解 REVISION 的递增机制 | kubectl rollout history确认 REVISION 和内容对应关系;回滚后立刻kubectl rollout status确认完成 |
| OutOfcpu 或 OOMKilled | limits 设置过低,或 requests 与实际负载不匹配 | 检查kubectl top pod看实际资源占用;适当调高 limits;对突发流量场景设置合理的requests,不要用极限值压测数据 |
| 多个服务相互影响,Pod 调度不均 | 没有配置反亲和性 | 给 Deployment 加podAntiAffinity软约束,配合 HPA 让负载分散到多个节点 |
| 镜像更新了但 Pod 没动 | imagePullPolicy 是 IfNotPresent,且本地已有同 tag 镜像 | 换唯一 tag(推荐);或临时kubectl set image触发一次滚动;或手动删除 Pod 让它重新创建 |
5.1 我最想强调的两条经验
第一,探针配置一定要区分“能启动”和“能提供服务”。我见过一个项目把就绪探针指向/根路径,结果首页静态资源加载慢,探针超时,Pod 被摘流量,用户全部 502。后来把探针改成/healthz并引入独立的健康检查接口后,问题彻底消失。健康检查接口要“轻”:不查数据库、不查第三方依赖,只返回进程存活状态,否则一次数据库抖动就能把整个服务全部重启,那场面太酸爽了。
第二,Deployment YAML 里不要写 Secret 明文。我演示的 YAML 里用了valueFrom.secretKeyRef,这才是正确姿势。Secret 单独管理,Deployment 引用。万一 YAML 被人提交到 Git 仓库,里面没有密钥,不至于直接泄露。配置管理上,ConfigMap 管非敏感配置、Secret 管敏感信息,二者都可以通过挂载或者环境变量注入容器。敏感信息的保存建议再用外部方案加密,不要在集群里裸奔。
5.2 关于 selector 的一个实际操作细节
前面反复强调selector和模板标签要匹配,但我再补充一个实际场景:在给 Deployment 加新标签时,不要直接改 selector 里的 matchLabels。比如想把标签从app: demo-web加一个version: stable,如果你把matchLabels改成{app: demo-web, version: stable},Pod 模板里的 labels 也要同步加version: stable,否则 apply 会报错。更安全的做法是:如果 Pod 模板标签变了,旧的 ReplicaSet 里的 Pod 会被留存,而新的 ReplicaSet 会创建新标签的 Pod,资源会有短暂的双版本并存。想清楚再改 selector,最好在维护窗口做。
K8s 中 selector 是不可以更新的,只能删除 Deployment 重新创建。所以尽可能在最初设计标签时就想好,不要频繁依赖修改 selector 来区分版本。版本区分交给业务镜像和 ReplicaSet 的天然版本机制就够了。
6. 写在最后的实际体会
折腾 Deployment YAML 这几年,最大的感受是:它不是一个“写完就完事”的配置,而是一份持续演进的运维契约。刚开始你可能只需要让 Pod 跑起来,但越到后面,你越会在探针、资源、调度策略、滚动更新节奏这些“软性配置”上花时间。这些配置都不会让你的 YAML 看起来更炫酷,但它们决定了业务在故障、高峰期、发版窗口能不能稳得住。
我个人建议每个团队都维护一套标准的 Deployment 模板,并结合“生产就要按生产标准来”的原则把探针、资源限制、反亲和、terminationGracePeriodSeconds 这些字段都先在测试环境严格验证。terminationGracePeriodSeconds默认 30 秒,如果你的服务优雅退出需要更久(比如要消费完消息再退出),要调大它。这个字段常被忽略,但线上平滑发版、避免请求中断很依赖它。
另外一个小技巧:每次apply之前先kubectl diff -f demo-web.yaml看变更内容,对于生产环境这是成本极低的“戴头盔操作”。我吃过几次直接 apply 的亏之后,这个习惯就再也没断过。
Deployment YAML 是 Kubernetes 世界里最基础也最核心的一块敲门砖。把它吃透,后面 Service、Ingress、Istio、ArgoCD 这些再复杂的东西,本质上也是在“声明状态、持续调和”这条主线上继续叠加。先把这个地基打牢,你会走得很稳。