【Kubernetes 滚动更新逼坑手册】3 个核心参数控制 Pod 发布节奏,从 Deployment 到 StatefulSet
你是否经历过——改了一行镜像 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-app
2.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,每次等上一个 Pod Running 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。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)