滚动更新卡在 2/5 二十分钟:配额、maxUnavailable 与 PDB
Events 一片空白,五个旧 Pod 一个不退,两个新的 Ready 却不增加。真因在 status.conditions 里:Pod 配额满了,而 maxUnavailable: 0 要求先扩后缩。
周三下午发版,订单服务推了个小改动,就改了一行日志格式。流水线跑完,kubectl rollout status 卡在 Waiting for deployment "order-service" rollout to finish: 2 out of 5 new replicas have been updated... 这句话上,整整二十分钟没动过。
我先确认了一堆常规项:新镜像能拉、Pod 是 Running、日志没有异常、健康检查接口手动 curl 也通。但 kubectl get pods 里五个旧 Pod 一个都没退,两个新 Pod 变成了 Ready 1/1 却也没增加。
最诡异的是 kubectl describe deployment order-service 里 Events 一片空白,没有 FailedCreate、没有 Unhealthy、没有任何 warn。Available 一直是 5,Up-to-date 是 2,Updated Replicas 卡在 2。
这篇记录这次「什么都不报错、但就是不动」的排查,最后发现是两个本来都很正确的配置互相抵消了。
一、Deployment 的状态字段先说清楚
kubectl get deploy 那一行有四个数字,卡住的时候它们长这样:
NAME READY UP-TO-DATE AVAILABLE AGE
order-service 5/5 2 5 218d
这四个字段的含义经常被混着理解,但排查时它们是判断卡在哪一环的关键:
| 字段 | 含义 | 卡住时说明什么 |
|---|---|---|
READY |
Ready 的 Pod 数 / 期望副本数 | 5/5 说明现有服务能力是够的 |
UP-TO-DATE |
已经是新版本的副本数 | 2 说明只有 2 个被更新 |
AVAILABLE |
处于 Available 状态的副本数 | 5 说明没掉可用性 |
Replicas(describe 里的) |
实际存在的 Pod 总数 | 如果等于期望数,说明没有多创建 |
我们这个组合说明:控制器想创建新 Pod,但创建不出来;同时旧 Pod 也不能删。而它既不报错也不推进,说明是被某个约束挡住了,不是被错误挡住了。
二、把控制器的心路历程打印出来
Deployment 的所有决策都会写到 status.conditions 里,rollout status 只显示其中一句话,完整信息得自己读:
kubectl get deploy order-service -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'
输出:
Available True MinimumReplicasAvailable Deployment has minimum availability.
Progressing False ProgressDeadlineExceeded ReplicaSet "order-service-7d9f" has timed out progressing.
ReplicaFailure True FailedCreate ...
ReplicaFailure=True、reason=FailedCreate,但 message 被截断了。展开看完整 message:
kubectl get deploy order-service \
-o jsonpath='{.status.conditions[?(@.type=="ReplicaFailure")].message}'
pods "order-service-7d9f8b6c5-" is forbidden:
exceeded quota: compute-quota, requested: pods=1, used: pods=50, hard: pods=50
是 ResourceQuota 挡住了。这个 namespace 的 Pod 配额是 50,当前已经用了 50。
这条信息解释了「为什么不报错」:FailedCreate 在 Deployment 的 conditions 里,不在 Events 里。而 rollout status 卡住的时候不会主动打印 condition 的 message。如果一开始就去读 conditions,这个问题三十秒就能定位。
三、配额满了,为什么旧 Pod 不先删
配额满了通常是「加配额」或者「腾空间」。但这里有个更值得追问的点:滚动更新的正常逻辑是「先扩后缩」——先创建一个新 Pod,等它 Ready,再删一个旧的。如果配额只够当前副本数,这个策略天然就卡住了。
那能不能改成「先缩后扩」?这就是 maxSurge 和 maxUnavailable 的作用:
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 允许临时多出 1 个 Pod(6 个)
maxUnavailable: 0 # 不允许任何时刻少于 5 个可用
我们的配置是 maxSurge: 1、maxUnavailable: 0。这个组合的含义是:任何时刻可用副本不能少于 5,所以必须先创建新 Pod 并等它 Ready,才能删旧的。 于是它必然需要第 6 个 Pod 的位置——而配额正好用完了。
maxUnavailable: 0 是我们当初为了「发版零中断」加的,看起来非常正确。但它有一个隐含前提:集群必须有超出当前副本数的余量。 没有余量时,这个配置就从「保证可用性」变成了「保证不动」。
四个组合的实际行为:
maxSurge |
maxUnavailable |
峰值 Pod 数 | 最低可用数 | 需要额外配额 |
|---|---|---|---|---|
| 1 | 0 | 6 | 5 | 是,至少 1 个 |
| 1 | 1 | 6 | 4 | 是,至少 1 个 |
| 0 | 1 | 5 | 4 | 否 |
| 0 | 0 | 不允许(会被 API 拒绝) | — | — |
maxSurge: 0 + maxUnavailable: 0 是非法组合,API Server 会直接拒绝,因为无法在不增加也不减少的前提下完成替换。这条规则本身就是对这个矛盾的承认。
我们的处境是:maxSurge: 1 要额外 1 个 Pod,配额没有;想改成 maxSurge: 0 + maxUnavailable: 1,但这是降可用性的操作,而且——这里才是真正的问题——我们的 PDB 不允许。
四、PDB 又插了一脚
查一下 PDB:
kubectl get pdb -n order
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
order-service-pdb 5 N/A 0 194d
minAvailable: 5,ALLOWED DISRUPTIONS: 0。
PDB 的语义是:自愿中断(eviction,比如节点排空、kubectl drain)时,被中断后剩余的可用副本不能低于 minAvailable。滚动更新删除旧 Pod 算不算自愿中断?
严格说,Deployment 控制器删除 Pod 走的是 API 的 delete,不经过 eviction 子资源,所以 PDB 不直接约束滚动更新。这也是很多人误以为 PDB 只影响 drain 的原因。
但 PDB 在这里仍然间接生效了,通过另一条路:我们的集群装了 descheduler 和一个自研的「容量均衡器」,它们会定期做 eviction 来平衡节点负载。当 ALLOWED DISRUPTIONS: 0 时,这些组件被完全挡住,节点负载开始倾斜。而倾斜导致的后果是:新 Pod 被调度到的节点资源碎片化,Pending 时间变长。
那天的情况更直接一些:配额满 + PDB 允许中断数为 0,让运维同学在下意识的操作里也不敢手动删旧 Pod——因为一删就会触发告警「PDB violation」。于是三件事叠加成了一个死锁:
maxUnavailable: 0要求先扩后缩- 配额满了,扩不出来
- PDB
minAvailable: 5让「手动删一个旧的」变成告警事件
三个单独看都正确的配置,凑在一起就是「谁都不许动」。
五、解开死锁的正确顺序
那天我做的操作顺序是这样的,值得记下来:
# 1. 先确认到底是配额还是调度问题
kubectl describe resourcequota -n order
# 2. 临时提高配额(这是唯一能立刻解开的方式)
kubectl patch resourcequota compute-quota -n order \
--type=merge -p '{"spec":{"hard":{"pods":"60"}}}'
# 3. 观察 rollout 是否恢复推进
kubectl rollout status deploy/order-service --timeout=180s
# 4. 恢复发版策略:允许 1 个不可用,峰值不再超配额
kubectl patch deploy order-service --type=merge -p '{
"spec":{"strategy":{"rollingUpdate":{"maxSurge":0,"maxUnavailable":1}}}
}'
# 5. 配额回落到原值(留 5 个余量给未来发版)
kubectl patch resourcequota compute-quota -n order \
--type=merge -p '{"spec":{"hard":{"pods":"55"}}}'
第 4 步是关键的长久修复:maxSurge: 0 + maxUnavailable: 1 意味着峰值 Pod 数等于副本数,不再需要额外配额;代价是发版过程中可用副本会短暂变成 4 个。
对我们的订单查询服务,4/5 的容量完全够用(平时 CPU 峰值 40%)。而对那些必须保持满员的场景(比如有状态的选主服务),就不能这么改,得反过来保证配额余量。
ALLOWED DISRUPTIONS: 0 这件事单独处理:把 PDB 从 minAvailable: 5 改成 minAvailable: 4(或者用 maxUnavailable: 1 的写法,语义更直观),让 descheduler 有腾挪空间,同时仍然保证 80% 的可用性。
maxUnavailable: 1 比 minAvailable: 5 更好的地方在于:它跟着副本数走。以后副本数从 5 改到 8,minAvailable: 5 会让允许中断数变成 3(太宽松),而 maxUnavailable: 1 永远是「最多少 1 个」,不需要跟着调。
六、让这类问题下次自己暴露
排查花的时间是二十分钟,但真正可惜的是这二十分钟里没有任何告警。我们的监控只盯 Pod 的 Running 状态和接口错误率,而这次 Pod 全都 Running、接口全都正常,从 SLO 角度看服务是健康的——只是新版没上去。
补了三类检查。
第一类,发版超时告警。流水线里原本有 kubectl rollout status --timeout=600s,超时会失败,但那天的流水线恰好把这一步的超时设成了 1800 秒。改成 300 秒,并且失败时自动把 conditions 打出来:
if ! kubectl rollout status deploy/order-service --timeout=300s; then
echo "=== rollout 卡住,dump 诊断信息 ==="
kubectl get deploy order-service -o jsonpath='{range .status.conditions[*]}{.type}={.status} {.reason}: {.message}{"\n"}{end}'
kubectl describe rs -l app=order-service | tail -30
kubectl describe resourcequota -n order
kubectl get pdb -n order
exit 1
fi
这几行把这次排查的所有信息在失败瞬间就打全了,比事后回忆快得多。
第二类,ReplicaFailure 直接告警。这是个被严重低估的信号,它出现就说明控制器想创建 Pod 但失败了,原因可能是配额、可能是 LimitRange 拒绝、可能是准入 webhook 拦截:
- alert: DeploymentReplicaFailure
expr: kube_deployment_status_condition{condition="ReplicaFailure",status="true"} == 1
for: 2m
annotations:
summary: "Deployment {{ $labels.deployment }} 无法创建 Pod"
第三类,配额水位预警。kubectl describe resourcequota 的 used/hard 应该被采集,超过 90% 就提示。配额满了不是突发事件,它通常是慢慢涨上来的——我们那个 namespace 半年前就 46/50 了,一直没人管,直到发版需要第 51 个。
小结
- Deployment 卡住但
Events空白时,先读status.conditions;ReplicaFailure的message里才有真实原因(这次是 Pod 配额超限)。 maxSurge: 1+maxUnavailable: 0要求必须有额外一个 Pod 的位置,配额按「副本数」配的集群会在第一次发版就死锁。- 没有配额余量时改用
maxSurge: 0+maxUnavailable: 1,峰值不超配额,代价是发版期间可用副本短暂少一个;必须满员的场景则要反过来保证配额余量。 maxSurge与maxUnavailable同时为 0 是非法组合,API 会直接拒绝。- PDB 约束的是 eviction 而不是滚动更新,但
ALLOWED DISRUPTIONS: 0会让 drain、descheduler 停摆,间接影响调度;用maxUnavailable写法比minAvailable固定值更能跟随副本数变化。 - 配额水位要按 90% 预警,发版流水线的
rollout status超时要短(300s)并在失败时自动 dump conditions、RS、配额、PDB 四份信息。
正在加载评论…