☰
Kubernetes Deployment实战:滚动更新、回滚与排障
2026/10/22 5:09:38 网站建设 项目流程

我最早系统性地使用 Kubernetes Deployment,是在做某个跨平台系统的容器化改造时。当时团队还在用脚本批量管理容器,每次发版都要盯着屏幕数 Pod,一有节点出问题就手忙脚乱。后来把无状态应用全部换成 Kubernetes Deployment 管理,整个发布流程才真正稳定下来。

很多人把 Deployment 当成“启动一堆 Pod”的工具,其实它背后是一整套状态控制机制。它负责保证指定数量的 Pod 一直运行,支持滚动更新、一键回滚、水平扩缩容。如果你在维护网站后端、API 服务、消息消费者这类无状态应用,Deployment 基本就是最常用的工作负载类型。这篇内容我就从实际运维视角,把 Deployment 的选型逻辑、字段细节、发布动作和常见坑一次说清楚。

1. Deployment 到底解决了什么问题

1.1 没有 Deployment 时的苦日子

刚开始做容器化的时候,不少团队的做法是每个节点上手动跑几个容器,或者直接用裸 Pod 来启动应用。Pod 本身是有生命周期的,节点重启、内存被挤爆、容器被 OOM Kill、镜像拉取失败,任何一个环节出问题都会导致服务不可用。更麻烦的是,没有控制器的情况下,Pod 被删了就是删了,不会有人替你在别的地方重新拉起来。经常是半夜收到告警,爬起来一台一台检查节点,手动补容器。

更新应用就更痛苦了。传统做法是先停掉旧容器,再启动新容器,这个过程中服务会出现一段空窗期。如果新版本启动失败,还得靠人工把旧镜像重新拉回来。回滚没有历史记录,全靠脑子记“上一次用的是哪个镜像”,一旦记错,问题就扩大了。我记得最狼狈的一次,是某个内部系统升级后连接池配置写错,所有 Pod 都在启动几秒后退出,团队花了快一个小时才定位到问题,又把旧版本重新部署回去。

Deployment 的出现,本质上就是把“期望状态”和“实际状态”之间的偏差交给控制器去自动纠正。你声明“我要 3 个副本”,集群就会努力维持 3 个副本。某个 Pod 挂了,控制器会重新创建;节点挂了,Pod 会被调度到其他可用节点。更新的时候,Deployment 按照你定义的策略逐步替换 Pod,整个过程不会一次全部停掉。回滚也只需要一条命令,因为控制器保存了版本历史和对应的模板。

1.2 Deployment、StatefulSet、DaemonSet 怎么选

不是所有应用都适合用 Deployment。Kubernetes 里工作负载类型有好几种,选错了后面会很难受。我在实际项目中见过有人把数据库管理端也塞进 Deployment,结果因为 Pod 被随机重建导致数据丢失,这个教训很典型。下面这张表是我常用的选择参考:

工作负载类型适合场景特点典型例子
Deployment无状态应用任意扩缩容、滚动更新、Pod 可替换API 服务、Web 前端、消息消费者
StatefulSet有状态应用稳定网络标识、稳定存储、有序部署数据库、缓存、ZooKeeper 类协调服务
DaemonSet节点级守护每个节点运行一个副本日志采集、监控 Agent
Job / CronJob任务型应用一次性执行或定时执行数据迁移、报表生成、定时清理

Deployment 的核心假设是“任何一个副本都是等价的,可以被随时替换”。如果你的应用需要稳定的主机名、需要独立的存储卷、需要按顺序启动,那就不应该选 Deployment。无状态应用换成 Deployment 之后,扩缩容几乎没有任何成本,因为副本之间没有共享状态,流量分发完全靠 Service 层去做。这也是为什么大多数 Web 后端应用,默认就是从 Deployment 开始。

1.3 一次发布背后的控制逻辑

Deployment 本身并不直接管理 Pod,它通过 ReplicaSet 来间接管理。创建一个 Deployment 时,控制器会生成一个 ReplicaSet,这个 ReplicaSet 负责维护指定数量的 Pod 副本。你可以把它理解成三层结构:Deployment 管版本,ReplicaSet 管副本,Pod 管实例。

当你想发布一个新版本时,Deployment 会创建一个新的 ReplicaSet,然后按照滚动更新策略,逐步扩大新 ReplicaSet 的副本数,同时缩小旧 ReplicaSet 的副本数。整个过程是渐进的,每次替换都会等待新 Pod 进入就绪状态。如果新 Pod 一直不就绪,更新就会卡住,不会无脑把旧副本全删掉。这就把发布风险控制在了可接受的范围内。

控制器的工作方式叫“控制循环”,不断比较当前实际状态和期望状态,然后通过创建、删除、更新资源来缩小差距。这也是 Kubernetes 声明式 API 的核心思想。你不需要告诉它“先在节点A上启动两个Pod,再去节点B上启动一个”,你只需要说“我要3个副本”,剩下的事情由控制器根据集群现状决定。

2. 手写第一个 Deployment:字段逐个拆解

2.1 最简 YAML 长什么样

很多人喜欢用kubectl create deployment快速生成资源,但生产环境我强烈建议用 YAML 文件管理,因为关键参数、标签、探针都需要显式声明。一个最简的 Deployment 长这样:

apiVersion: apps/v1 kind: Deployment metadata: name: demo-api namespace: default spec: replicas: 3 selector: matchLabels: app: demo-api template: metadata: labels: app: demo-api spec: containers: - name: api image: registry.example.com/demo-api:v1.2.3 ports: - containerPort: 8080

这里面有两个metadata,外层的是 Deployment 自己的元数据,内层的是 Pod 模板的元数据。新手最容易搞混的就是这个结构。apiVersion现在必须是apps/v1,网上有些老教程还在用extensions/v1beta1,那个版本早就废弃了,直接写新版本就好。

spec.selector是 Deployment 用来管理 Pod 的关键。它只认带有matchLabels指定标签的 Pod。这里有个很隐蔽的细节:selector一旦创建,就不能再修改匹配的标签键,因为控制器会靠着它去关联 ReplicaSet 和 Pod。如果你需要改标签,宁可新建一个 Deployment 再删掉旧的,也不要去改 selector。

2.2 真正决定行为的关键字段

如果只填replicas和image,Deployment 能跑,但完全不够稳。真正决定发布行为的是strategy字段,它会直接影响滚动更新的速度和可用性。

spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25%

maxSurge表示更新过程中最多可以比期望副本数多出多少个 Pod,maxUnavailable表示最多允许多少个 Pod 不可用。默认都是 25%,这个组合对我们常见的服务来说是比较安全的。举个例子,如果你的副本数是 10,maxSurge: 25%会向上取整到 3,意味着滚动期间最多可以有 13 个 Pod 同时存在;maxUnavailable: 25%会向下取整到 2,意味着至少有 8 个 Pod 处于可用状态。这样既保证了发布速度,又不会让服务容量掉得太低。

如果改成type: Recreate,Deployment 会先把所有旧 Pod 删掉,再创建新 Pod。这个策略很干脆,但会带来服务中断,通常只适合离线任务或者可以接受短暂停服的场景。我曾在一个数据上报服务上误用过 Recreate,发布窗口内直接丢失了一部分请求,后来再也不敢对在线服务用这个策略。

还有两个容易忽略的字段。progressDeadlineSeconds表示更新在多少秒内没有进展就被标记为失败,默认 600 秒。如果应用启动很慢,建议调大一点。revisionHistoryLimit表示保留多少份历史版本,默认 10,如果磁盘和资源紧张,可以调成 3 到 5。但它调得太小会减少可回滚的版本数,需要平衡。

2.3 Pod 模板里的隐藏细节

Deployment 真正运行的是模板里的 Pod,这个模板里的细节比 Deployment 自己的字段还重要。我一般会在模板里至少配置三样东西:探针、资源限制、优雅停机时间。

spec: template: spec: containers: - name: api image: registry.example.com/demo-api:v1.2.3 ports: - containerPort: 8080 readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 15 periodSeconds: 20 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi terminationGracePeriodSeconds: 30

readinessProbe负责告诉 Service 这个 Pod 是否已经准备好接收流量。只有就绪探针通过的 Pod 才会被加入 Service 的 Endpoints 列表。livenessProbe负责检查应用是否存活,如果失败,kubelet 会重启容器。两者路径虽然可以一样,但语义完全不同。我曾经见过一个应用把数据库连接状态写进了存活探针,结果数据库短暂抖动时,所有 Pod 都被反复重启,反而把故障放大了。存活探针应该只是判断进程能不能响应基础请求,不要耦合外部依赖。

资源限制这块容易被忽视。不设置requests和limits,调度器做 Pod 分配时就很“盲目”,节点上其他应用也可能把内存吃满。设置了之后,调度器会根据 requests 选择合适的节点,kubelet 也会根据 limits 限制容器用量。至于terminationGracePeriodSeconds,它决定了 Pod 被删除时能有多少时间处理存量请求,对优雅下线非常重要。我一般设置 30 秒以上,配合 preStop 钩子做健康检查摘除。

3. 从创建到发布:完整实操流程

3.1 创建 Deployment 并核对状态

假设你写好了上面那个 YAML,保存为demo-api.yaml,创建命令很简单:

kubectl apply -f demo-api.yaml

建议永远用kubectl apply而不是kubectl create。apply具有声明式特性,后续你再修改 YAML 文件,然后重新执行apply,它会自动计算差异并更新资源,不会像create那样报“已存在”的错误。

创建完别急着走,先看状态:

kubectl get deploy demo-api kubectl get rs -l app=demo-api kubectl get pods -l app=demo-api -o wide kubectl describe deploy demo-api

第一眼要看的不是 Pod 是否 Running,而是 DESIRED、CURRENT、READY 这三列数字是否一致。如果 READY 比 DESIRED 少,说明有一部分副本还没有通过就绪探针。再配合kubectl describe看事件,能发现镜像拉取失败、调度失败、探针未通过等具体原因。这个习惯我保持了很久,能省下大量排查时间。

3.2 更新镜像与触发滚动更新

日常发布最常见的就是换镜像版本,命令有两种。一种是用kubectl set image:

kubectl set image deployment/demo-api api=registry.example.com/demo-api:v1.3.0

这里的api是容器名,不是 Deployment 名,写错会直接报错。执行后,Deployment 会自动生成一个新的 ReplicaSet,然后开始滚动。你可以用kubectl rollout status实时跟进:

kubectl rollout status deployment/demo-api

这个命令会阻塞到滚动完成或超时。滚动过程中,你可以再看一眼 Pod 的变化:

kubectl get pods -l app=demo-api -w

应该能看到一部分 Pod 先进入 Terminating,同时新的 Pod 被创建并慢慢变成 Running 和 Ready。整个过程是交替的,不会瞬间全部切换。

另一种改 YAML 的方式是kubectl edit deployment/demo-api,它会打开一个编辑器。不过我更推荐先改本地文件,再apply,这样版本历史和代码仓库能对应起来,方便审计。

这里要特别注意:如果新版本的启动探针没有配置好,滚动更新可能会一直卡住。假设新旧版本交替时,新 Pod 始终不能 Ready,Deployment 会保持旧 ReplicaSet 副本不减少,让你有时间排查。它不会自己帮你回滚,这是好事,因为有些时候你需要先看日志再决定。

3.3 扩缩容与暴露服务

需要扩容时,直接改 YAML 里的replicas再apply,或者用命令:

kubectl scale deployment/demo-api --replicas=5

假如你的集群装了 metrics-server,还能配置 HPA(HorizontalPodAutoscaler),让副本数根据 CPU 使用率自动变化:

kubectl autoscale deployment/demo-api --min=2 --max=10 --cpu-percent=60

HPA 会周期性获取 Pod 的 CPU 指标,然后调整 Deployment 的副本数。但这里需要提醒一下:HPA 在流量突然上涨时响应有延迟,如果应用启动需要很长时间,最好提前用压力测试验证扩容速度。

Deployment 创建之后,默认只有集群内部能访问,外部无法直接访问。通常我们通过 Service 暴露:

kubectl expose deployment/demo-api --port=80 --target-port=8080 --type=ClusterIP

这条命令会生成一个 Service,port是 Service 对外端口,target-port是容器端口。ClusterIP只在集群内部访问,想要外部访问,一般换成NodePort或者接入 Ingress 网关。生产环境我更推荐 Ingress,因为它能统一管理域名、TLS 和路由规则,而不是把端口直接暴露到节点上。

4. 回滚与版本管理

4.1 为什么回滚要单独拎出来说

发布不是每次都能成功,有时候镜像比对了,测试也过了,但到了生产环境就是有问题。比如某个接口返回格式变了,下游服务没跟上;或者内存占用突然翻倍,节点压力变大。这时候如果手动改镜像重新发布,动作慢,还容易出错。Deployment 的版本管理机制,就是为了让你在几分钟内快速恢复到一个已知可用的版本。

Deployment 默认会保留最近 10 条历史版本。这里的“版本”不是镜像 tag,而是 Deployment 的完整 Pod 模板快照。每一次触发 ReplicaSet 变化的发布,都会生成一个新的 RS,历史版本就是这些 RS。所以你不是直接删掉当前 RS,而是通过rollout命令在历史 RS 之间切换。

4.2 回滚实战

先看历史版本:

kubectl rollout history deployment/demo-api

每一行会显示 revision 号和对应的变更原因。如果CHANGE-CAUSE为空,说明你创建和更新时没有加--record,现在的版本其实默认会记录元数据,最好还是养成标注变更原因的习惯。想查看某个具体版本包含的镜像和模板:

kubectl rollout history deployment/demo-api --revision=3

要回滚到上一个版本,最简单:

kubectl rollout undo deployment/demo-api

如果想回到指定版本:

kubectl rollout undo deployment/demo-api --to-revision=2

执行之后,Deployment 会创建一个新的 ReplicaSet,把模板换成指定版本的内容,然后执行滚动更新。也就是说,回滚动作本身也是一次发布,它会遵循同样的滚动策略,而不是瞬间切换。如果你希望回滚得快一点,可以临时把maxUnavailable调大,回滚完再改回来。

这里有个经验:不要频繁在多个版本之间来回undo。因为每次undo都会生成新 RS,历史列表会不断增加。如果发现版本 A 和版本 B 都不行,需要仔细看回滚后的 Pod 日志,而不是盲目再回滚一次。我见过有人连续 undo 三次,结果因为日志被覆盖,更难定位根因。

5. 常见问题与排查记录

5.1 镜像拉取失败:ImagePullBackOff 与 ErrImagePull

这个应该是最常见的日常问题。执行kubectl get pods时,如果看到ImagePullBackOff或ErrImagePull,基本就是镜像相关原因。先用kubectl describe pod看事件,它会给出更具体的错误。常见原因有这么几类:

一是镜像名或者 tag 写错,比如多写了空格、少写了仓库地址,这种通过仔细对比 YAML 就能发现。二是私有仓库需要认证,但 Pod 没有配置imagePullSecrets。很多团队把镜像放在自建仓库里,创建 Deployment 时忘了把拉取凭据关联进去,就会一直拉取失败。三是镜像部署到了不兼容的架构,比如在 ARM 节点上拉取了一个只有 amd64 的镜像,也会报 manifest 问题。四是网络层面拉取超时,特别是镜像很大或仓库带宽不足,适当增加imagePullPolicy相关的重试等待时间,或者优化仓库镜像大小。

处理时不要盲目删除 Pod。Deployment 会自动重建,重试是有限的,频率会越来越低。先把根因纠正,再手动删除当前异常 Pod 触发一次重建,比反复等退避要快。

5.2 Pod 一直重启:CrashLoopBackOff

如果 Pod 状态显示CrashLoopBackOff,说明容器启动后马上崩溃,或者存活探针失败导致反复重启。不要慌,先看日志:

kubectl logs demo-api-xxxxx kubectl logs demo-api-xxxxx --previous

--previous参数很关键,它能看到上一次容器退出的日志。很多应用在启动失败时会把错误写到 stderr,但如果只在崩溃前那一刻才输出,当前日志可能查不到。看完日志一般能定位到配置文件错误、依赖连接不上、启动命令参数不对等问题。

还有一种情况是启动很慢,刚启动到一半存活探针就开始探测,连续几次失败导致重启。这时候需要调整livenessProbe的initialDelaySeconds和failureThreshold,或者加startupProbe让慢启动应用有更长的初始化时间。启动探针配置好之后,失败处理会更平滑,不会和存活探针“打架”。

5.3 更新卡住:ProgressDeadlineExceeded 和就绪探针

有时执行完更新后,kubectl rollout status迟迟不结束,事件里出现ProgressDeadlineExceeded。这意味着更新在指定时间内没有完成,通常不是因为控制器停了,而是因为某个新 Pod 一直未就绪。最常被忽略的是就绪探针的失败,新 Pod 虽然进程在跑,但健康检查不通过,Service 不会把流量放进去。

看新 ReplicaSet 的情况:

kubectl get rs kubectl describe rs <new-rs-name>

重点看Events里有没有Readiness probe failed。如果新 Pod 一直没有 Ready,旧 Pod 就不会被大量替换,整体容量稍微下降,但服务不至于完全挂掉。这种设计其实是保护机制,避免你发布一个有问题的版本导致雪崩。定位到探针问题后,修正配置,重新发布就好。

还有另一种情况是maxUnavailable: 0且maxSurge过小,比如副本只有 2 个,maxSurge设置成 1,每次只能多一个 Pod,滚动速度会很慢。如果你希望发布更激进,可以适当提升maxSurge的比例,但要保证集群资源足够。

5.4 Pod 一直 Pending:调度不成功

如果 Pod 一直是Pending,说明调度器没能把它放到某个节点上。describe pod能看到原因。最常见的是资源不足:节点上 CPU 或内存余量不够,无法满足requests。这种情况要么扩容节点,要么调整资源 requests,让 Pod 能塞进现有节点。

另外还有几种可能:节点有污点(Taint),而 Pod 没有对应的容忍(Toleration)。或者 Deployment 的 Pod 模板里定义了nodeSelector,但集群里没有匹配的节点。再有就是节点本身处于NotReady或SchedulingDisabled状态。排查思路很简单:先看节点状态,再看调度器事件,最后看 Pod 的调度约束。

有时候 Pod Pending 不是因为资源,而是因为持久卷调度没有完成。虽然无状态应用一般不用 PV,但如果 Deployment 的模板里意外挂载了一个 PVC,且 PVC 一直是 Pending,Pod 也会被卡住。检查 PVC 状态和存储类是否匹配,这种问题很隐蔽。

5.5 排查工具总结与实用技巧

我把平时排查问题的一套流程总结一下。第一步永远先看kubectl get events --sort-by=.lastTimestamp,这是最快了解集群发生了什么的方式。第二步看 Deployment 和 ReplicaSet 的kubectl describe,它会给出控制器的判断,包括镜像拉取、探针失败、调度错误等。第三步看 Pod 日志,需要区分当前日志和上一次日志。

还有一个用处很大但很多人不用的参数是kubectl get pods -o yaml,能直接看到 Pod 的完整状态、容器状态、终止原因码和最后探针状态。有时候CrashLoopBackOff的容器退出码是 137,这个信号要格外小心。137 通常是因为容器收到了 SIGKILL,有可能是 OOM 被杀,也可能是节点内存压力触发。结合事件里的OOMKilled才能判断,不能一概而论。

如果 Service 访问不到,先检查 Service 的 Endpoints:

kubectl get endpoints demo-api kubectl describe svc demo-api

如果 Endpoints 为空,说明 Pod 的标签和 Service 的 selector 不匹配,或者 Pod 没有 Ready。很多人会盯着服务配置看半天,结果发现是 Deployment 的标签写错了。Service 的 selector 要和 Pod 模板里的labels一致,不是和 Deployment 的metadata.labels一致。

我在实际运维中的体会是,Deployment 本身不难,难的是把细节形成肌肉记忆。滚动更新参数、探针设置、资源限制、标签一致性,这些点分开看都很简单,但合在一起,就决定了发布是顺滑还是事故。尤其是探针,它在滚动更新和故障自愈里扮演的角色比很多人想象中大得多。只要把这个细节吃透,再配合rollout命令做回滚,绝大部分发布故障都能在几分钟内控制住。最后再分享一个小技巧:生产环境发布前,先在测试环境用同一份 YAML 跑一遍kubectl rollout status,然后把测试环境的 Deployment 历史清空,再上生产。这样你至少能确保 YAML 语法和探针配置是有效的,能省掉不少临时救火的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询