资讯动态

K8s Deployment 从入门到实战:YAML、滚动更新与回滚全解析

发布时间:2026/10/21 6:02:48 来源:尧图企业网站定制
这个系列写到第九篇终于轮到被问得最多的资源对象之一K8s deployment。前几篇聊过 Pod、Service、Namespace但说实话生产环境里很少会直接操作一个裸 Pod真正天天打交道的基本都是 Deployment。这也是新同学最容易困惑的地方——明明 Pod 也能跑容器为什么要多套一层这篇把 deployment 相关操作讲透从 YAML 编写到滚动更新、扩缩容、回滚外加我在真实集群里踩过的一堆坑。无论你是刚跨进 K8s 门槛的开发还是已经在维护业务集群的运维按这篇的思路完整过一遍比自己瞎试几个月管用得多。1. 先搞清楚 Deployment 到底在管什么1.1 三层资源的关系很多教程一上来就让你写 YAML结果写着写着就懵了Deployment、ReplicaSet、Pod 到底什么关系我习惯用一个比喻来解释Deployment 是项目经理只负责说我要 3 个副本镜像版本是 1.25ReplicaSet 是组长负责盯着手下有没有人离职、有没有人划水数量不够就立刻补Pod 才是真正干活的员工里面跑着你的容器。三者是层层管理的关系Deployment 控制 ReplicaSetReplicaSet 控制 Pod。你平时执行kubectl get deploy看到的只是一层壳下面每个 Deployment 都对应着至少一个 ReplicaSet而每个 ReplicaSet 又对应着若干 Pod。这个层级关系理解透了后面看滚动更新和回滚才会觉得顺理成章。1.2 控制器循环与声明式设计的价值K8s 这东西的核心思想叫声明式你告诉它期望状态它自己去想办法达到。Deployment 控制器内部其实就是一个无限循环不断做三件事从 etcd 里读期望状态从集群里看实际状态然后执行操作让实际状态向期望状态靠拢。举个例子你改了 Deployment 的副本数期望是 5 个但当前只有 3 个控制器就会调用 kube-apiserver 创建 2 个新 Pod如果某个 Pod 所在的节点挂了控制器发现实际状态少于 5 个又会自动在其他节点上补一个新的。这个自动修复能力是 Deployment 存在的最根本原因。1.3 为什么生产环境不会用裸 Pod我见过有同学图省事用kubectl run直接起一个 Pod 来跑测试服务。自己本地玩玩没问题但放到生产环境就会遇到三个很现实的问题第一裸 Pod 没有自愈能力。Pod 挂了你得手工重建凌晨三点报警起来kubectl get pods然后手动删掉痛苦不痛苦。第二裸 Pod 没法滚动更新。你想换镜像版本只能删了重建中间必然断服。第三裸 Pod 没有回滚能力。新版本出问题你想切回旧版本还是只能手动搞。Deployment 就是为这三个问题而生的。它给你自愈、滚动更新、版本回滚三条保障这也是为什么生产环境里 90% 以上的工作负载最终都会以 Deployment 的形式存在。2. 从零创建一个 DeploymentYAML 字段逐个拆2.1 一个最小可用的完整示例先给一个我在测试环境经常用的最小 YAML它麻雀虽小五脏俱全足够跑一个单副本的 Nginx 服务apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: default labels: app: nginx spec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80把这个文件保存成nginx-deploy.yaml然后执行kubectl apply -f nginx-deploy.yaml一个 Deployment 就起来了。注意我现在统一用apply而不是create原因后面会专门讲。2.2 关键字段选型背后的原因这几个字段看起来简单但每个都有讲究。apiVersion必须用apps/v1。早年间有extensions/v1beta1、apps/v1beta1这些老版本现在早就废弃了新写的 YAML 一律用apps/v1它才是 Deployment 的正规稳定版本。metadata.name就是 Deployment 的名字命名要符合 DNS 规范。这个名字在集群命名空间内唯一重名会直接报错。spec.replicas是期望副本数。这里有一个新手必踩的大坑selector.matchLabels里的标签必须和template.metadata.labels里的标签保持一致。我这句是app: nginx那么 Pod 模板里的 labels 也必须包含app: nginx否则kubectl apply会直接报 selector 不匹配的校验错误。K8s 靠这个标签去圈定自己管理的 Pod不一致就等于你告诉它我要管这些 Pod但 Pod 上没贴这个标签它自然没法管理。spec.template就是 Pod 模板Deployment 创建 Pod 时完全照这个模板来。模板里的spec.containers[].image是镜像地址注意不只是镜像名还包括 tag。生产环境强烈建议写具体版本号比如nginx:1.25而不是写nginx:latest。latest 的问题是每次重新部署可能拉到不同版本而且当集群里多个节点已经有这个镜像时拉取策略可能导致新旧版本混跑排查问题的时候会非常痛苦。containerPort只是声明性的信息它不会自动创建 Service。真正让外界访问服务还得靠 Service 或 Ingress那是另一个话题但这行写上没坏处方便kubectl describe时查看端口信息。2.3 两种创建方式的区别create 与 apply同样一个 YAMLkubectl create和kubectl apply行为完全不同很多人一开始分不清。kubectl create -f xxx.yaml是命令式操作它的逻辑是创建这个资源。如果资源已经存在直接报错如果你只改了 YAML 中的一小部分再执行 create它不会去更新已有资源而是告诉你AlreadyExists。kubectl apply -f xxx.yaml是声明式操作它的逻辑是让集群里的这份配置和文件里一致。文件里写了什么集群最终就会变成什么。新增字段就补字段修改字段就更新字段资源不存在就先创建。这种以文件为唯一事实来源的方式才符合声明式的设计哲学也更适合配置管理。所以我的建议是从一开始就只用apply把它当成标配。后面做 CI/CD 流水线时kubectl apply也是绝对的主流。还有一个实用小技巧如果你拿到一个 Deployment YAML或者想快速了解某个 Deployment 的完整配置可以执行kubectl get deployment nginx-deploy -o yaml它会输出这个 Deployment 在集群里的实际状态和配置比网上找模板靠谱得多。如果只想看关键字段可以加-o jsonpath或者直接用kubectl describe。3. 滚动更新生产变更不断流的核心机制3.1 默认策略与两个关键参数的计算滚动更新是 Deployment 最核心的价值之一。它解决的核心痛点是我在不中断服务的情况下把 A 版本平滑替换成 B 版本。Deployment 默认的更新策略是RollingUpdate还有另一个策略叫Recreate就是先把旧 Pod 全部删掉再创建新 Pod中间必然有一段服务不可用生产环境基本不会用它。滚动更新有两个关键参数都写在strategy.rollingUpdate下strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25%maxSurge表示更新过程中最多可以超出期望副本数多少个或者百分之多少。maxUnavailable表示更新过程中最多允许有多少个 Pod 处于不可用状态。这两个参数看似简单但真正计算的时候埋了不少雷。我拿一个实际场景算给大家看假设当前副本数是 4maxSurge是 25%maxUnavailable是 25%。maxSurge的百分比是向上取整4 的 25% 是 1向上取整就是 1所以更新期间最多允许有 5 个 Pod 同时存在。这多出来的 1 个 Pod 是临时加塞的用于先启动新版本。maxUnavailable的百分比是向下取整4 的 25% 是 1向下取整也是 1所以更新期间最少必须保证有 3 个 Pod 处于可用状态。换句话说删旧 Pod 的时候最多只能同时删 1 个。所以整个滚动更新过程大概是这样的先创建 1 个新版本 Pod等它变得 Ready再删除 1 个旧版本 Pod然后再创建一个新版本 Pod再删 1 个旧版本 Pod直到所有 Pod 都变成新版本。整个过程可用副本数始终不低于 3临时总副本数不超过 5。如果副本数换成 3百分比就会产生差异3 的 25% 是 0.75maxSurge向上取整为 1maxUnavailable向下取整为 0。这意味着更新期间不允许任何 Pod 不可用如果其中某个旧 Pod 恰好出了问题滚动更新就可能卡住。这也是很多同学发现更新怎么一直不完成的根源之一。3.2 触发滚动更新的三种常用方式滚动更新不是靠重新 apply 整个 Deployment才触发的它只在 Pod 模板发生变化时才触发。所以实际操作中有三种常见方式方式一修改 YAML 后重新 apply。比如把image: nginx:1.25改成nginx:1.26然后kubectl apply -f nginx-deploy.yaml。这是我最推荐的方式因为集群状态和文件保持一致方便追踪。方式二直接用命令更新镜像最常用的就是kubectl set image deployment/nginx-deploy nginxnginx:1.26这里要注意格式容器名新镜像。容器名是你在 YAML 里containers[].name指定的名字不是 Deployment 名也不是 Pod 名。如果写错容器名命令会直接报错而且不会做任何修改。如果 Deployment 里只有一个容器也可以省略容器名前缀直接写镜像kubectl set image deployment/nginx-deploy nginx:1.26但为了明确我建议还是写上容器名。方式三直接编辑线上资源kubectl edit deployment nginx-deploy这会打开一个 YAML 编辑器改完保存后立即生效。这种方式适合临时调整但它会跳过代码仓库的 review 流程生产环境用起来要谨慎。3.3 暂停与恢复多步骤变更的保险丝有一个命令在实战中非常有用但很多人不知道rollout pause和rollout resume。kubectl rollout pause deployment/nginx-deploy暂停后你对 Deployment 做的任何修改比如改了镜像、改了环境变量都不会立即触发滚动更新而只是更新了 Deployment 的期望配置。等你把一系列变更都准备好后再执行kubectl rollout resume deployment/nginx-deploy这时才会一次性把所有变更应用到滚动更新中。这个机制非常适合我要同时改镜像、调资源配额、追加环境变量这种多步操作。如果不暂停每改一步就会触发一次滚动更新旧 Pod 反复重建非常浪费资源还容易造成混乱。需要提醒的是rollout pause暂停的是变更触发不是把整个集群状态冻结了。已经处于更新中的 Pod 不会停还会走完当前流程。另外还有一个命令kubectl rollout restart deployment/nginx-deploy它跟滚动更新没关系它是强制触发一次 Pod 重建用来让所有 Pod 重新调度。当你改了 ConfigMapPod 里的配置不会自动生效想快速让所有副本重新读取配置时这个命令就派上用场了。3.4 就绪探针滚动更新真正不中断的保证滚动更新本身只是机制它保证的不是业务不中断而是发布流程不中断。真正保证流量不中断的是你的探针配置尤其是就绪探针。很多人在没有配置readinessProbe的情况下直接做滚动更新会出现一个经典现象新版本 Pod 已经Running了但 Service 马上就把流量切过去结果新版本还没初始化完成直接 502、504。原因就是 Deployment 默认认为 Pod 进入 Running 状态就是可用但 Running 只是容器进程起来了不代表应用能处理请求。所以只要你的应用需要连接数据库、加载缓存、初始化配置就应该配置就绪探针比如readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 5有了就绪探针新 Pod 只有通过健康检查后才会被标记为 ReadyService 的 Endpoint 才会把它纳入转发列表Deployment 也才会继续下一个批次的新增和删除。这个探针同时也是滚动更新进度的判据——只有新 Pod Ready更新才会继续走。我踩过最惨的一次事故就是线上服务缩容后扩回来发现 Pod 一直 CrashLoopBackOff查下来是因为我把livenessProbe和readinessProbe混用了把读外部系统的超时连到了存活探针上外部系统一抖动所有 Pod 都被判定不健康然后被杀掉。探针的路径、超时、容错逻辑一定要设计得比业务更保守。4. 扩缩容从手动 scale 到自动伸缩4.1 手动扩缩容的常用方式扩缩容是 Deployment 日常用得最多的操作之一。比如大促前要扩容流量波峰过了要缩容省资源。最简单的方式是kubectl scale deployment nginx-deploy --replicas5这条命令直接把副本数改成 5Deployment 控制器看到期望状态变了就会创建相应数量的 Pod。缩容时也一样kubectl scale deployment nginx-deploy --replicas2除了命令方式你也可以直接改 YAML 里的spec.replicas再kubectl apply效果一样。我推荐在生产环境使用后者因为走代码仓库评审变更可追踪临时应急调容量才用kubectl scale。4.2 缩容时 Pod 的删除顺序缩容不是随机删 Pod 的它有一套排序逻辑。K8s 会优先删除调度最不安全的 Pod比如在多节点环境下会优先删除副本数最多节点上的 Pod如果多个节点副本数相同会优先删除最老的 Pod。但这个排序不是绝对的不同版本行为有差异。这里有一个非常重要的运维常识不要在任何需要保证数据一致性的场景下依赖 Pod 名字来承接业务状态。因为缩放、重建、滚动更新都会导致 Pod 名字和 IP 完全变化。如果想要稳定的访问入口必须依赖 Service如果想要持久状态必须挂 PVC。4.3 通过 HPA 自动伸缩需要注意的限制手动扩缩容适合有规律、可预判的场景但流量是动态的手动操作往往跟不上。这时候可以上 HPAHorizontalPodAutoscaler让集群根据 CPU、内存或自定义指标自动调整 Deployment 的副本数。一个最简单的 HPA 例子apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-deploy minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60创建之后HPA 控制器会定期拉取指标数据如果 CPU 平均使用率持续超过 60%就调大副本数低于某个阈值且持续一段时间再逐步缩回来。但用 HPA 有三个必须注意的限制第一scaleTargetRef指向的必须是支持缩放的资源Deployment 天然支持。但你绝对不能同时手动执行kubectl scale去修改副本数因为 HPA 会把它改回来两者会互相打架。第二HPA 判断的是所有 Pod 的平均指标如果某个 Pod 因为流量热点 CPU 打满而其他 Pod 很闲它可能不会触发扩容。对于这种情况需要基于自定义指标或者 PDB 进行更细粒度的控制。第三minReplicas和maxReplicas之间的副本数范围要合理。我见过有人把 min 设为 1结果流量低谷时只剩 1 个 Pod节点故障时服务就直接不可用了。一般建议 min 至少 2 个跨节点分布。5. 回滚操作生产事故后的标准救援流程5.1 查看发布历史再完善的发布流程也有出问题的时候。Deployment 的回滚能力就是事故现场最后一道保险。首先要看这个 Deployment 的历史版本kubectl rollout history deployment/nginx-deploy输出会类似REVISION CHANGE-CAUSE 1 none 2 none 3 none这个 REVISION 就是滚动更新的版本号每触发一次 Pod 模板变更Deployment 就会生成一个新的 ReplicaSet同时 REVISION 加 1。如果想看某个版本详细改了什么可以加--revision参数kubectl rollout history deployment/nginx-deploy --revision2这里要提醒一点如果你希望CHANGE-CAUSE列有内容传统做法是在kubectl apply/run时加上--record参数。不过在比较新的 K8s 版本里--record已经被移除了替代方案是直接在 Deployment 的 annotation 上记录变更原因。我在实际工作中更推荐走 GitOps把 YAML 变更全部通过代码仓库提交历史自然可追溯比依赖 K8s 自带的 history 记录可靠得多。5.2 全量回滚与指定版本回滚确认问题后执行全量回滚也就是回滚到上一个版本kubectl rollout undo deployment/nginx-deploy执行后Deployment 控制器会把 Pod 模板恢复成 REVISION 为 1 时的配置然后触发滚动更新。如果你明确知道是哪个版本出的问题或者想回滚到某个指定版本可以用kubectl rollout undo deployment/nginx-deploy --to-revision2比如你发布了 3 个版本第 3 版有问题但第 2 版是稳定的用这条命令直接回 2 版不用先回 1 再升 2。回滚后一定要用下面这条命令确认状态kubectl rollout status deployment/nginx-deploy它会一直输出当前滚动更新的进度直到所有 Pod 变为 Ready 或者超时。我见过只执行rollout undo不看结果的还以为是回滚成功了实际卡在了某个探针上业务一直没恢复。5.3 回滚后的 ReplicaSet 取舍一个很重要但常常被忽略的细节是回滚操作不会删除旧的 ReplicaSet。回滚实际上相当于把 Pod 模板改回旧配置因此会触发一次新的滚动更新生成一个新的 ReplicaSet。旧的历史 ReplicaSet 依然保留这保证了你可以继续在版本之间来回切换。这个设计带来的一个实际影响是每回滚一次ReplicaSet 数量就会增加etcd 的存储也会跟着涨一点。如果发布极频繁历史版本累积太多可以用revisionHistoryLimit限制保留数量。5.4 控制历史版本数量Deployment 默认保留 10 个历史 ReplicaSet可以通过这个字段调整spec: revisionHistoryLimit: 5这个值不是越大越好。历史版本多回滚选择余地大但每个 ReplicaSet 都要占 etcd 存储大量无用历史版本会影响 apiserver 性能。我的经验是生产环境保留 3 到 5 个就够了如果真的需要回滚到很远之前的版本走 GitOps 重新部署才是正路。回滚还有一个容易被忽略的点如果你通过kubectl edit直接修改了 Deployment 的字段然后又执行rollout undoundo 只会把 Pod 模板回滚到历史 REVISION 的配置而不是把你 edit 的内容回滚掉。所以回滚前最好先保存一份当前配置的备份比如kubectl get deployment xxx -o yaml backup.yaml万一回滚后发现需要恢复某些非模板字段还有个后手。6. 高频故障排查实录与经验清单6.1 镜像拉取失败ImagePullBackOff这是新手遇到最多的错误kubectl get pods看到状态是ImagePullBackOff或者ErrImagePull。原因通常有三个镜像地址写错、镜像不存在、私有仓库认证失败。排查步骤很简单kubectl describe pod pod-name在 Events 里会看到具体失败原因比如Failed to pull image nginx:xxx或者pull access denied。前者大概率是 tag 写错了后者需要你检查 imagePullSecrets 是否配置正确。这里有一个很多人不知道的坑如果你用的是 Docker Hub 的镜像频繁发布时可能触发 Docker Hub 的匿名拉取限流尤其在工作节点 IP 比较少的情况下也会报ImagePullBackOff但 Events 里写的不是认证失败而是too many requests。解决办法是配置镜像仓库凭据或者把镜像同步到内网私有仓库。6.2 容器反复崩溃CrashLoopBackOffCrashLoopBackOff 意味着容器启动后立刻崩溃K8s 会不断重启它而且重启间隔指数增长。排查思路是先看日志kubectl logs pod-name --previous--previous参数非常关键因为容器已经崩溃重启了默认看的是当前活着实际上可能又死了的日志--previous才能看到上一次崩溃时的输出。很多进程是启动时读取配置出错直接 panic日志里会写得清清楚楚。如果日志是空的就要怀疑livenessProbe配置有误应用其实还活着但探针启动检查失败K8s 认为容器不健康杀了重启。这种看起来一直重启但日志没有异常的情况优先检查探针的路径、端口、initialDelaySeconds这几个参数。6.3 滚动更新卡住滚动更新卡住不是报错而是kubectl rollout status一直转圈或者新版本 Pod 一直创建不出来。常见的三种原因第一种是maxUnavailable设置得太严格比如副本数是 3maxUnavailable: 0%如果此时集群资源不足新 Pod 调度不上去旧 Pod 又不会被删更新就永远卡住。这就是我前面说的百分比取整问题在生产场景的真实体现。第二种原因是新版本 Pod 一直达不到 Ready要么是就绪探针失败要么是镜像启动崩溃。看kubectl get pods就能发现新 Pod 状态不健康。第三种是 PVC 或存储相关问题比如新 Pod 调度到了另一个节点而那个节点上挂载的存储不可用。排查滚动更新卡住最有效的工具是kubectl describe rs new-rs-name它会告诉你新 ReplicaSet 创建 Pod 时遇到的具体事件比盲目看 Deployment 状态直观得多。6.4 调度失败一直 PendingPod 一直Pending说明它没有被调度到任何节点上。常见原因有节点资源不足、有污点不匹配、PVC 没有绑定、节点亲和性规则没有满足。先用kubectl describe pod看 Events事件信息基本会告诉你原因。如果是因为资源不足可以执行kubectl top nodes看节点负载再决定是否扩容节点如果是因为污点要么调整 Deployment 加 tolerations要么取消节点污点。这里提醒一个我踩过的坑Deployment 设置了nodeSelector要求 Pod 调度到有特定标签的节点但后来这几个节点下线了标签一直没清理结果所有新 Pod 全部 Pending而老 Pod 慢慢被驱逐服务直接雪崩。所以nodeSelector和节点标签一定要定期梳理。6.5 六条经过验证的操作习惯最后分享六条我在生产环境里长期执行的操作习惯谈不上惊天动地但每条都是从事故里换来的第一Deployment 变更永远走 YAML 文件 kubectl apply不要用kubectl edit直接改线上配置。文件可以进 Git出问题能 review、能回滚。第二所有容器必须显式配置resources的 request 和 limit。有 limit 能防止单容器把节点打爆有 request 能帮助调度器更合理分配资源。我见过没写 limit 的服务突然遭遇流量峰值直接把节点内存耗尽整个节点上的 Pod 全被 OOM 杀掉。第三就绪探针和存活探针分开配置探针的检查路径要做接口级别的健康检查不要只检查 TCP 端口。TCP 通不代表业务可用。第四每次滚动更新前先看一眼当前副本数和策略参数尤其是百分比设置的取整逻辑确认不会卡更新。第五缩容前要确认下游流量已经切走或者有 Service 兜底缩容后要观察一段时间再继续缩不要一缩到底。第六把kubectl rollout status做成发布流程的必选动作脚本里不检查 rollout 状态就不允许标记发布成功。个人体会最深的一条是Deployment 这套机制只有在充分理解期望状态这几个字以后才算真正入门。它包括整个 K8s一切的出发点都是声明期望状态而不是执行具体命令。理解了这一点再去写 YAML、排查问题思路会顺很多。最后再送一个小技巧用kubectl explain deployment.spec可以随时查看 Deployment 的字段说明比翻文档还快遇到不认识的字段先敲这条命令能省不少时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑