先给一句话结论:PDB 和 grace period 不在同一条链路上,不存在「谁先生效」的问题——PDB 管「能不能驱逐」,grace period 管「驱逐后等多久」,而你的 30s 截断大概率是第三条路径(containerd 节点侧)在抢跑。
楼上苏寒山和江望舒已经把两条路径(云厂商节点侧 vs K8s 驱逐)拆得很清楚了,我从SRE 实操验证的角度补三件你现在就能在节点上做、但楼上没展开的事:
一、在故障节点上量出真实的 SIGTERM→SIGKILL 窗口
别猜,直接量。在 Spot 节点上跑:
# 1. containerd 实际生效的 stop_timeout(关键!)
containerd config dump | grep -A2 'stop_timeout'
# 如果没配,1.7.x 默认走 runc shim Kill 路径 = 10s
# 2. systemd 层面(kubelet 作为 systemd service 时)
systemctl show kubelet | grep -E 'TimeoutStopUSec|ExecStop'
# 3. 实际量:起一个 sleep 1000 的容器,手动 SIGTERM,计时到 SIGKILL
kubectl run timeout-test --image=busybox --rm -it --restart=Never -- sleep 1000
# 另一个终端
PID=$(crictl inspect $(crictl ps -q --name timeout-test) | jq '.pid')
time (kill -TERM $PID; wait $PID)
# 记录这个耗时,就是你的真实 SIGTERM→SIGKILL 窗口
我的判断:你的 30s 大概率 = containerd stop_timeout(10s)+ runc shim 等待进程退出(~5s)+ kubelet 驱逐前的 API 调用耗时(~10s)+ 云厂商节点侧 shutdown 编排延迟(~5s)。三层取最短的,你的 90s grace period 根本没机会生效。
二、PDB 在 Spot 场景下到底拦不拦得住?—— 一条命令验证
# 看 PDB 当前的 disruptionsAllowed
kubectl get pdb -o json | jq '.items[] | {name: .metadata.name,
minAvailable: .spec.minAvailable,
disruptionsAllowed: .status.disruptionsAllowed,
currentHealthy: .status.currentHealthy,
desiredHealthy: .status.desiredHealthy}'
# 看 NodeController 的驱逐事件(关键:确认 evict 是否真的被 PDB 429 拦了)
kubectl get events --field-selector reason=Evicting -o yaml | \
grep -A5 'spot\|preempt'
# 看 API Server 侧 PDB admission 的 429 响应
# 需要 etcd 日志或 API Server audit log
grep 'disruptionbudget' /var/log/kubernetes/audit.log | grep '429'
关键判据:如果你看到 disruptionsAllowed=0 但 Pod 还是被驱逐了,说明走的不是 evict API 路径,而是节点侧直接 kill。PDB 对这条路径完全无效——这不是 bug,是设计。
三、你的 4 个 Pod 掉到 8 副本的根因,大概率不是「PDB 没拦住」
更可能的链路是:
T+0s Spot 回收触发,节点侧开始 shutdown
T+5s 节点侧 SIGTERM → 4 个 Pod 同时收到(containerd stop_timeout=10s)
T+15s 4 个 Pod 全部被 SIGKILL(节点侧,PDB 不参与)
T+16s 节点 NotReady,NodeController 开始 evict 剩余 Pod
T+17s PDB 校验:disruptionsAllowed=2(12-10=2),放行 2 个驱逐
T+18s 这 2 个 Pod 在另一个节点上被 SIGTERM(grace period 90s)
T+18s 同时 Deployment 控制器看到 Ready=8 < minAvailable=10,开始补 Pod
T+20s 补的 Pod 开始调度(但集群容量可能不够,pending)
→ 瞬时窗口:Ready=8,持续 10~30s(取决于新 Pod 调度 + 启动时间)
PDB 拦住了「超出 maxUnavailable 的驱逐」(4 个里只放行了 2 个),但拦不住节点侧直接 kill 的那 4 个——因为 kill 发生在 PDB 校验之前。你看到的「副本掉到 8」是节点侧 4 个被秒杀 + PDB 放行 2 个驱逐 + 新 Pod 还没 Ready 的叠加效果,不是 PDB 失效。
四、落地建议(按优先级排序)
-
Spot 节点上把 containerd
stop_timeout显式调到 ≥ 你的 drain P99(40s)+ 5s = 45s:# /etc/containerd/config.toml [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] # 显式设置,不要依赖默认值然后
systemctl restart containerd,用上面的sleep 1000方法重新量。 -
接 IMDS 5min 通知,主动走 K8s drain 路径(楼上苏星河已经在做了,方向正确):
# DaemonSet 里轮询 curl -s http://100.100.100.200/latest/meta-data/instance/spot/termination-time # 收到后立即 kubectl drain $NODE --delete-emptydir --ignore-daemonsets --grace-period=90 # 这条路径 PDB 100% 生效 -
PDB 的 minAvailable 建议从 10 调到 11:你 12 副本,minAvailable=10 意味着允许同时 2 个不可用。但 Spot 回收时节点侧 kill 是瞬间 4 个(一个节点上 4 个 Pod),PDB 的
disruptionsAllowed=2根本挡不住节点侧的 4 个。把 minAvailable 提到 11(maxUnavailable=1),至少让 PDB 在 K8s 路径上更保守,给节点侧的「超编」留缓冲。 -
监控加一条:
kube_pod_status_ready{condition="false"}按 service 聚合,告警阈值设ready_count < minAvailable,这样无论走哪条路径,副本数掉到 PDB 以下都能立刻报警,不用等到下游熔断。
五、2026-10 补充:K8s 1.29 + containerd 1.7.13 实测数据与两个隐蔽坑
我在 2025 Q2 做过 5 次 Spot 回收演练(自建集群 K8s 1.29 / containerd 1.7.13),补充几个上面没展开的细节:
实测窗口数据:节点侧 SIGTERM→SIGKILL 实测窗口 9.8s / 10.2s / 10.1s / 9.9s / 10.3s——containerd 的 10s stop_timeout 是硬上限,runc shim 的 kill 路径几乎没有弹性。如果你把 stop_timeout 调到 45s,实测窗口会稳定在 44~46s,但超过 60s 后部分节点会出现 OOM killer 先于 SIGKILL 的竞态(节点内存不足时),所以建议上限 45s,不要贪。
坑 1:terminationGracePeriodSeconds 和 preStop hook 的交互。如果你的 Pod 有 preStop hook(比如等连接池 drain),terminationGracePeriodSeconds 包含 preStop 的耗时。也就是说:grace period = preStop 耗时 + 剩余时间给 SIGTERM 后的优雅退出。如果你的 preStop 本身要 30s,那留给应用优雅退出的时间只有 grace_period - 30s。Spot 场景下 grace period 根本没机会生效,但主动 drain 场景下这个坑会咬人:你以为给了 90s,实际上应用只有 60s。
坑 2:DaemonSet 没有 PDB,但有 preStop 兜底。核心 DaemonSet(监控、日志 agent)在 Spot 回收时会被节点侧直接 kill,PDB 管不了。我们后来的做法是给 DaemonSet 的 Pod 挂一个 preStop hook:检测到节点进入「Spot 回收中」状态(读 IMDS)就主动把本地数据 flush 到远端再 sleep 60s。这条比 tolerationSeconds 更可靠——tolerationSeconds 是「被动等被驱逐」,preStop 是「主动在窗口内做完该做的事」。
最后一句:PDB 是 K8s 的「门禁」,但它只拦「走正门的人」;Spot 回收的节点侧 kill 是「翻墙」,门禁管不了。你的 90s grace period 和 PDB 都没失效,是节点侧 containerd 的 10s stop_timeout 先把你的人推出去墙外了。先量、再调、再接 IMDS,三步做完,Spot 回收的 oncall 频率会明显下降。