你是否经历过——改了一行镜像 tag,集群直接炸了?旧 Pod 切得太快,新 Pod 还没起来,502 刷屏。或者更坑爹的是,明明新版本代码有问题,但滚动更新照跑不误,把最后一个可用的旧 Pod 也干掉了。
别问我是怎么知道的。这十年运维干下来,这种凌晨 2 点的 Case 我接得太多了。
读完这篇你能有这些收获:
- 搞懂 Pod 从 Pending 到 Terminating 的全过程,知道什么时候该等、什么时候可以强制干
- Deployment 滚动更新的三个核心参数(maxSurge / maxUnavailable / minReadySeconds)到底怎么配
- StatefulSet 的 RollingUpdate 为什么“伪滚动”,以及怎么利用 partition 控制灰度
- 优雅终止的配置套路 + 我踩过的坑
好了,说干就干。
一、Pod 生命周期:你的 Pod 在 5 种状态之间怎么转
我个人更倾向于把 Pod 生命周期理解为 Kubernetes 这座大厦的“地基”——你把滚动更新、自愈、弹性伸缩这些上层能力玩得再花,如果 Pod 状态机没搞懂,一出问题照样抓瞎。
Pod 的生命周期被抽象为一个有限状态机,包含 Pending、Running、Succeeded、Failed、Unknown 五个基本状态。这些状态之间由事件驱动转换——调度、节点故障、健康检查失败、用户删除,都可能触发状态跳转。
各状态的精确语义(这个必须背下来):
状态 | 含义 | 常见原因 |
Pending | Pod 已被 Kubernetes 接受,但还没调度到节点,或镜像拉取/卷挂载没完成 | 节点资源不足(CPU/内存不满足 requests);镜像拉取失败(地址错、私有镜像无 pull secret);节点亲和性/污点不匹配;InitContainer 执行失败 |
Running | 至少有一个容器正在运行 | 正常状态,但注意 Running ≠ 可以接流量 |
Succeeded | 所有容器正常退出(exit 0) | Job/CronJob 执行完毕 |
Failed | 至少一个容器以非零码退出,或 kubelet 判败 | 应用崩溃、配置错误、OOMKilled |
Unknown | 节点失联,Master 不知道 Pod 到底还活不活 | 网络分区、节点宕机 |
这里有个很关键的区分:状态(Pod phase)和条件(Pod conditions)是两回事。比如 Pod 可能在 Running 状态下,但PodScheduled是 True,ContainersReady却是 False——这意味着容器跑起来了,但还没通过就绪探针,不该接流量。
Pending 的诊断三板斧:
# 1. 看事件 kubectl describe pod <pod-name> | grep -A 20 "Events:" # 2. 看调度失败原因 kubectl describe pod <pod-name> | grep -A 10 "FailedScheduling" # 3. 检查节点资源 kubectl top nodes常见 Pending 原因就那么几个:节点资源不足、镜像拉取失败、调度策略不匹配、InitContainer 卡死。排查效率最高的永远是kubectl describe,别去乱猜。
二、Deployment 滚动更新:零停机不是自动的,是配置出来的
2.1 两种升级策略
Kubernetes Deployment 默认就是 RollingUpdate。另一种叫 Recreate——先把旧 Pod 全干掉再创建新的,当然会有服务中断。我个人只在 dev 测试环境用 Recreate,生产环境从来不碰。
2.2 三个核心参数 = 滚动更新的控制中枢
参数 | 作用 | 建议值(生产) |
| 更新过程中超出期望副本数的最大 Pod 数量,可以是整数或百分比 | 25% 或 1 |
| 更新过程中最多有多少个 Pod 可以处于不可用状态 | 0 或 10% |
| 新 Pod 启动后需要稳定运行多少秒才被视为“可用” | 10–60 秒,看应用启动时长 |
这两个maxUnavailable和maxSurge的配合决定了更新速度和服务可用性:
maxUnavailable: 0+maxSurge: 1:典型零停机配法。每次先创建一个新 Pod,等它就绪后,再删一个旧 Pod。- 注意:
maxSurge: 0且maxUnavailable: 0同时出现是不允许的,这样其实根本没有滚动更新的空间,Kubernetes 会返回校验错误。
配一个稳妥的滚动更新策略:
apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 minReadySeconds: 30 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: app image: my-app:v2 ports: - containerPort: 8080 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10(顺便提一嘴,如果你的应用启动需要一分钟,minReadySeconds至少要设到 60 秒,光靠就绪探针还不够——因为探针成功后到服务注册完成之间还有时间窗。)
触发滚动更新(改镜像版或者配置都行):
# 更新镜像 kubectl set image deployment/my-app app=my-app:v3 # 或 apply 新配置 kubectl apply -f deployment.yaml # 看滚动更新进度 kubectl rollout status deployment/my-app # 如果更新异常,立即回滚 kubectl rollout undo deployment/my-app2.3 kubectl rollout 家族 4 条常用命令
# 看状态 kubectl rollout status deployment/<deploy-name> # 回滚(回到上一个版本) kubectl rollout undo deployment/<deploy-name> # 回滚到指定版本 kubectl rollout undo deployment/<deploy-name> --to-revision=2 # 看历史版本 kubectl rollout history deployment/<deploy-name> # 暂停更新(比如发现问题了,先别继续) kubectl rollout pause deployment/<deploy-name> # 恢复更新 kubectl rollout resume deployment/<deploy-name>2.4 滚动更新必须配合健康探针
没有健康探针的滚动更新就是在裸奔。三个探针搞清楚各自管什么:
探针 | 用途 | 失败后果 |
| 检测容器是否还活着 | 重启容器 |
| 检测容器是否可以接收流量 | 从 Service Endpoints 摘掉 |
| 慢启动应用的启动检测 | 启动完成后失败会触发 livenessProbe |
重点是:滚动更新依赖readinessProbe。新 Pod 必须通过 readinessProbe 才会被标记为 Ready,然后 Kubernetes 才会继续更新下一步。
三、StatefulSet 滚动更新:它真的不是 Deployment
StatefulSet 是用来管理有状态应用的,为每个 Pod 提供持久标识符(如mysql-0、mysql-1)和稳定存储。和 Deployment 不一样,StatefulSet 里的 Pod 不能相互替换,每个都有固定的“身份证”。
更新策略:当设置updateStrategy.type: RollingUpdate时,StatefulSet 控制器会从最大序号向最小序号逐个删除并重建 Pod,每次等上一个 PodRunning and Ready后,才处理下一个。
这就是我为什么说它是“伪滚动”——Deployment 还可以新旧同时存在形成“浪涌”,StatefulSet 一次只停一个、建一个。
配置示例:
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: "mysql" replicas: 3 minReadySeconds: 10 updateStrategy: type: RollingUpdate rollingUpdate: partition: 2 # 只更新序号 >= 2 的 Pod selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0彩蛋:用 partition 做灰度更新
partition参数是 StatefulSet 的隐藏王牌。假设replicas=3,partition=2,则只有序号大于等于 2 的 Pod(比如mysql-2)会被更新,小于 2 的(mysql-0、mysql-1)保留旧版本。这就可以实现灰度发布——先更新一个 Pod 观察效果,没问题了再逐步调低 partition 值,直到全部更新完毕。
看更新状态:
kubectl rollout status statefulset/mysql kubectl get pods -l app=mysql -w限制:
- 删除或缩容 StatefulSet不会删关联的 PV/PVC(为了数据安全)
- StatefulSet 需要搭配无头 Headless Service 才能正常工作
(顺便提一嘴,Kubernetes 1.32 新特性:StatefulSet 支持自动删除不再需要的 PVC 了,省得你手动去清理那些遗留的存储卷,开persistentVolumeClaimRetentionPolicy就能用。)
四、Pod 优雅终止:从硬杀到温柔说再见
4.1 不配优雅停机的后果
我曾经帮一个电商团队排查过:他们做版本发布,新 Pod 全跑起来了,但一堆客户端报 502。原因很简单——旧 Pod 被强杀,但 Eureka 注册中心里头还留着这些 Pod 的地址,负载均衡把请求发到了已消失的 Pod 上。
这正是 Kubernetes 默认终止流程不够“温柔”的地方。
4.2 正确的 Pod 终止流程(按顺序)
当你删除 Pod 或滚动更新时:
- Pod 状态变为Terminating
- 从 Service Endpoints 中立即移除该 Pod(没有新请求进来了)
- 执行PreStop 钩子(如果有配置)
- 发送SIGTERM信号给容器主进程
- 等待
terminationGracePeriodSeconds(默认 30 秒) - 超时发送SIGKILL强制终止
关键动作顺序:流量切断发生在 PreStop 之前,这意味着如果没有 PreStop,旧 Pod 切断流量后就直接等 SIGTERM/SIGKILL了,根本没机会主动向注册中心“注销”。
4.3 一套生产级的优雅终止方案
我需要做三件事:
- 实现优雅停机:代码里注册 ShutdownHook,收到 SIGTERM 时处理完现有请求再退出(Spring Boot 默认就支持,不用额外配太多,用
server.shutdown=graceful就行)。 - 配置 PreStop 钩子:注销服务、通知不再接流量、留足时间处理存量请求。
- 调整 terminationGracePeriodSeconds:给 PreStop + 进程退出留够时间。
apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 3 template: spec: containers: - name: app image: my-app:latest lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 15"] # 等待流量切除 + 处理请求 terminationGracePeriodSeconds: 45 # 必须大于 preStop 时间为什么要把terminationGracePeriodSeconds设得比 PreStop 钩子时间长一些?因为预停止钩子需要完成注销或下游任务,而进程退出也需要额外时间。如果TerminationGracePeriodSeconds小于 PreStop 时长 + 进程退出所需时间,SIGKILL 很可能会过早介入,直接打断优雅停机。
实测教训:我之前有次把terminationGracePeriodSeconds设成 32(preStop 是 30),结果注册中心确认发送慢了几秒,Pod 直接被 SIGKILL 了,大量 pending 请求直接超时。
五、我把这些年踩的坑全放这了
5.1 镜像拉取失败导致滚动更新卡死
现象:新镜像不存在或仓库不通,新 Pod 起不来,旧 Pod 也被慢慢删完,业务降级。
为什么坑:默认情况下 Kubernetes 不知道你的镜像有问题,它只看到 Pod 起不来。如果没有 readinessProbe,它会继续替换旧 Pod。
应对:
imagePullPolicy设为IfNotPresent或Always,看场景- 私有仓库配好
imagePullSecrets - 配好 readinessProbe,新 Pod 不 Ready 就不会进入 Endpoints
- 设置合理的
maxUnavailable,保证更新过程中旧 Pod 不会被全部替换 - 紧急情况直接
kubectl rollout undo
5.2 更新策略参数乱配
典型反面教材:maxUnavailable: 100%——更新过程中所有旧 Pod 都会被删掉,新 Pod 一旦起不来,服务就彻底断了。
我推荐的保守配置:maxUnavailable: 0+maxSurge: 1。每次只多一个 Pod 在跑,安全、可控。
5.3 StatefulSet 假死不动
StatefulSet 滚动更新卡住可能的原因:PreStop 钩子执行失败、Pod 始终 Ready 不了、PVC 挂载失败、ControllerRevision 异常等。
排查路径:
kubectl -n <ns> rollout status statefulset/<name> kubectl -n <ns> describe statefulset <name> kubectl -n <ns> get controllerrevision -l app=<app>5.4 资源限制相关
- Pod requests 设太高导致调度失败:小于任何节点的可分配资源,Pod 就会死在 Pending 上。
- OOMKilled:limit 太低了。开个资源监控面板持续观察,别等到凌晨炸了才看。
- requests 和 limits 怎么设:我通常建议 requests = limits / 1.5 左右,用 Guaranteed QoS 防驱逐。但要结合实际压测和监控数据来定,别拍脑袋。
六、几个“隐藏”知识点,知道的人不多
- 滚动更新靠 ControllerRevision 记录 Revision:每次模板变化都会产生新的 ControllerRevision,
kubectl rollout history用的就是这个。 - PostStart 钩子和容器的 ENTRYPOINT 是同时触发的,谁先跑完没保证。别拿它当依赖初始化工具。
- PreStop 钩子失败不会阻止 Pod 终止:Kubernetes 不会因为 PreStop 失败就让 Pod 无限期停留在 Terminating 状态,超时后会强制 SIGKILL。
- StatefulSet 的 Pod 管理策略可以改成 Parallel:需要并行启动/终止 Pod 时,设
.spec.podManagementPolicy: Parallel。 - Pod 原地更新在 1.33 仍然有很多限制:QoS 类不能变,不支持所有字段修改,默认 RBAC 权限也不够。别指望它能完全取代滚动更新。
写到最后,其实滚动更新和 Pod 生命周期管理是 Kubernetes 最基础但也最容易翻车的环节。配置是你写的,集群是你维护的,半夜被叫起来推回滚方案的时候谁也替不了你。
你的更新策略参数是怎么配的?maxUnavailable 设的是 0 还是 25%?欢迎评论区聊聊你遇到过的奇葩 Case。