AI时代的Kubernetes教程36招-第32招-Cordon、Drain、Node维护和故障域怎么保证业务连续性?
节点维护不中断:cordon、drain 与 PDB 的工程边界
排水一个节点,为什么业务会抖三分钟?
[读者] 平台/SRE 工程师,负责多可用区 K8s 集群
[痛点] 内核升级、机型替换都要排水,一 drain 就掉副本
[现在读] 有维护窗口,但不想靠半夜停机加祈祷
[读完] 用 Failure Domain 把单节点维护做成日常操作
[旧方案] 直接删 Pod 或重启节点,靠控制器重建
|
v
[新需求] 节点要规范化下线(内核升级 / 机型迁移),业务必须连续
|
v
[冲突] 驱逐是自愿中断,PDB 会拦;而反亲和又把副本绑在少数节点上
|
v
[后果] drain 卡死、窗口超时,只能 --force 硬删,PDB 形同虚设
我是老李,写业务代码出身,现在带一个小团队维护一个跑在线交易的 Kubernetes 集群:三十来个节点铺在三个可用区,日常交付、扩容、灰度都在上面。集群跑顺之后,真正的工作量其实不在上线,而在存量运维——换机型、打内核补丁、处理磁盘告警,这些动作有一个共同前提:先把这台节点上的工作负载挪走。
一开始我们把这件事当成「重启一下就行」。做了几轮才发现,这是整个集群里最容易翻车的一类操作。原因不在命令本身,而在命令之外:集群有没有为「少一台机器」这件事做过设计。
我们把流程固定成三步:cordon 阻止新 Pod 上来,drain 把现有 Pod 驱逐走,维护完 uncordon 放回调度池。三条命令背下来只要一分钟,但真正决定成败的是另外三个问题:谁会被驱逐、驱逐之后能不能在别处站起来、以及站起来的这段时间里,业务还剩几个健康副本。
那么,当 drain 因为 PDB 卡住不动的时候,是 PDB 配错了,还是放置策略从一开始就没给维护留出余地?
那么,是用 --force 和 --disable-eviction 把节点尽快腾空,还是先停下来把拓扑约束补齐再说?
那么,如果选错了,代价只是维护窗口超时、变更回滚,还是某一次真实发生的可用性下降?
01、故事
任务是:在三个可用区、十二台待换机型的节点上,完成一轮不影响在线交易的节点轮换维护。
现场是这样的。集群里有一条核心交易链路,三个副本,PDB 一开始写的是 minAvailable: 3——当时想法很简单,「三个副本必须都活着」,看起来最安全。
我们按官方文档的标准动作开工:
kubectl cordon worker-1
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
第一条秒回,节点状态变成 SchedulingDisabled。第二条打出一行 evicting pod,然后就没有然后了。十分钟后我按下 Ctrl+C,drain 退出,节点还卡在 cordon,Pod 一个都没走。
第一反应是「PDB 配得太死」。把 minAvailable 从 3 改成 2 再试,这次终于走掉一个 Pod,然后在第二个 Pod 上再次停住——因为三个副本里有两个本来就落在 worker-1 上,另一个在别的节点,maxUnavailable 的额度用完了。
真正让我后背发凉的是一次跨节点操作:同一个可用区里的另一台机器也在维护窗口内,两台同时被 cordon 之后,调度器找不到足够的合规位置把副本重建出来,出现了短暂的空窗。那一刻我才意识到,问题从来不是 PDB 这两个数字,而是副本分布这张图。
于是摆在面前的选择很具体:删掉 PDB、加上 --force,用最快的速度把节点腾空;还是先停手,把「副本应该放在哪儿」这件事重新设计一遍。
一条命令容易
一次可用性很难
卡住的不是节点
是没想清的边界
02、问题
旧方案失效的地方很清晰:我们把「节点维护」当成了一次单纯的命令执行,而系统把它当成了一次自愿中断——两边的假设根本对不上。
业务影响分三层。第一层是维护窗口失效:drain 卡住不是失败,而是一直挂着,运维只能靠 Ctrl+C 手动终止,窗口时间被静默吃掉。第二层是变更节奏被打乱:为了赶窗口,团队开始倾向于跳过预检、直接用更激进的手段,PDB 从保护变成了障碍。第三层才是真正的风险:某一次激进操作恰好撞上真实故障,可用副本数跌破安全线。
技术表现可以归为四类:
- drain 长时间不返回,或反复打印同一条驱逐重试日志。
- PDB 的 ALLOWED 列为 0,describe 里 DisruptionsAllowed 是 0。
- 驱逐之后副本重建到别的节点,但新 Pod 长时间 Pending。
- 节点 drain 成功,但维护完忘了 uncordon,这台机器长期空转。
要判断「问题是不是真的解决了」,需要一组可验证的完成标准,而不是「感觉还行」:
- 单节点 drain 期间,核心链路的 Ready 副本数始终不低于 PDB 允许的下限。
- drain 命令能在设定时间内以退出码 0 正常结束,不需要人工 Ctrl+C。
- drain 结束后所有副本 Ready,且不出现长时间 Pending 的副本。
- 同一可用区内,任意时刻最多只有一台节点处于维护状态。
- 全流程不依赖 --force,也不依赖 --disable-eviction。
旧办法靠手快
新要求靠设计
能验证才算数
说得清才敢做
03、原理
这一篇只需要三个概念,但它们之间的关系和大多数人的直觉不一样。
cordon 只对调度器说话。 它改的是 Node 对象的 .spec.unschedulable,已经在节点上运行的 Pod 完全不受影响,也不会被重启。这里有个容易忽略的细节:DaemonSet 创建的 Pod 会带一个针对 node.kubernetes.io/unschedulable 污点的容忍,所以 cordon 之后 DaemonSet 仍然可能往这台机器上落新 Pod——它并不会因为你标了不可调度就停下。
drain 不等于删 Pod。 drain 做的事情是「先 cordon,再对每个 Pod 发起一次 Eviction 请求」。Eviction 走的是 policy/v1 的 eviction 子资源,它本质上是一次申请,而不是一条删除命令。如果 PDB 判定当前不允许再少一个健康副本,API Server 会拒绝这次申请(HTTP 429),kubectl drain 拿到 429 就重试,重试到成功,或者重试到你把它 kill 掉。这就是「卡住不动」的全部原理——它不是死锁,它是在等一个永远不会到来的许可。
反直觉判断:drain 不搬运任何东西。 它只负责把 Pod 请走,真正让工作负载「在别处出现」的是上层控制器。Deployment 会重建 ReplicaSet 的 Pod,StatefulSet 会重建同名 Pod,而没有任何 ownerReference 的裸 Pod 被驱逐后就是消失了,不会再回来。这解释了一个常见现象:drain 命令报错拒绝执行,提示存在不受控制器管理的 Pod,需要用 --force——不是 kubectl 多事,而是它在提醒你,这些 Pod 没有被驱逐的语义。
反直觉判断:PDB 只在自愿中断时生效。 节点宕机、kubelet 失联、宿主机断电、OOM Killer,这些都是非自愿中断,PDB 一点作用都没有——事件发生得太快,根本轮不到准入控制说话。所以「维护期间业务连续」这件事,其实被拆成了两半:一半靠 PDB 约束自愿中断的速度,另一半必须靠副本数和拓扑分布扛住非自愿中断。只做前一半,等于只做了一半。
还有一条容易忘的语义:nodeAffinity 和 podAntiAffinity 都带 IgnoredDuringExecution。它们只在调度那一刻计算一次,已经跑起来的 Pod 不会因为节点状态变化而重新计算亲和性。所以「我配了反亲和,副本一定分散」是一种错觉——preferred 类型只是建议,required 类型才是硬约束,而在调度资源紧张时,硬约束的另一面就是 Pending。
驱逐不是搬运
许可先于动作
拓扑先于预算
冷静先于快刀
04、架构
[输入] kubectl drain <node>
|
v
[模块] Eviction API + PDB 准入 + kube-scheduler
|
v
[数据/状态] Node.unschedulable / PDB.disruptionsAllowed / Pod.ownerReferences
|
v
[处理] 逐 Pod 发起 eviction -> 429 则重试 -> 控制器重建副本
|
v
[输出] 节点空载 / 新副本落在其他 Failure Domain
这张图里,真正决定成败的是中间那条链路:Eviction 请求 → PDB 准入 → 控制器重建 → 调度器选址。四步里任何一步的输入不足,整个流程就会停在原地。
边界划在哪里。drain 的职责边界只到「让节点上没有非 DaemonSet 的 Pod」,它不负责保证重建成功,也不负责保证重建过程中容量足够。PDB 的职责边界只到「自愿中断的速率」,它不负责节点故障。调度器的职责边界只到「在当前约束下找一个合规位置」,它不负责你的业务能不能扛住这台机器的缺失。把这三件事混在一起谈,就会出现「PDB 都配了为什么还抖」这种讨论。
收益是什么。以 Node 为最小维护单元、以 Zone 为最小容错单元,可以把「换机型」这种低频重操作变成一个有既定节奏的日常流程:一次只动一台,一次只影响一个 Zone 的一小部分容量,出问题可以立刻回滚。
代价是什么。强制性的拓扑约束会牺牲调度灵活性。当可用区容量不均、或者某个 Zone 临时缩容时,required 约束会让副本直接 Pending——它把「不均匀」从运行时问题提前成了调度时问题。这是有意的:宁可现在就 Pending 让你看见,也不要等到维护时才发现副本全挤在一起。
适用条件。这套结构适用于副本数大于等于故障域数量的无状态工作负载。副本数少于可用区数量时,强约束会导致永久 Pending;有本地存储的有状态工作负载需要单独流程,因为它们的重建目标节点是被数据位置钉死的。
边界划在调度
状态落在预算
动作交给重试
结果交给拓扑
05、实战一次
环境与版本。 Kubernetes v1.29 及以上(PDB 使用 policy/v1,v1beta1 已在较新版本中移除,具体版本门槛需按当前官方文档核验),kubectl 与服务端同版本,至少三个可调度的 worker 节点。kind、minikube 或自建集群都可以。
依赖。 不需要额外组件。PDB 由 kube-controller-manager 内置控制器提供,驱逐接口由 API Server 直接提供。
给节点打故障域标签。 云托管集群一般会由云控制器自动注入 zone 标签,自建集群需要手动打:
kubectl label node worker-1 topology.kubernetes.io/zone=zone-a --overwrite
kubectl label node worker-2 topology.kubernetes.io/zone=zone-b --overwrite
kubectl label node worker-3 topology.kubernetes.io/zone=zone-c --overwrite
核心实现。 先建命名空间,再落一个三副本工作负载,用 topologySpreadConstraints 同时约束可用区和节点两个维度:
apiVersion: v1
kind: Namespace
metadata:
name: maint-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
namespace: maint-demo
spec:
replicas: 3
selector:
matchLabels:
app: checkout-api
template:
metadata:
labels:
app: checkout-api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: checkout-api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: checkout-api
containers:
- name: app
image: registry.k8s.io/pause:3.9
resources:
requests:
cpu: 100m
memory: 64Mi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: checkout-api-pdb
namespace: maint-demo
spec:
maxUnavailable: 1
selector:
matchLabels:
app: checkout-api
注意这里的 PDB 用的是 maxUnavailable: 1,而不是 minAvailable: 3。理由在第 07 章展开。
启动。
kubectl apply -f maint-demo.yaml
kubectl -n maint-demo rollout status deployment/checkout-api --timeout=120s
请求与观察。 先确认副本落在不同可用区:
kubectl -n maint-demo get pods -o wide -l app=checkout-api
kubectl -n maint-demo get pdb checkout-api-pdb
get pdb 的输出重点关注 ALLOWED 列,它等于当前允许的中断数。三副本、maxUnavailable 为 1 时,它应当是 1。
预检与执行 drain。 先干跑一遍,看清会驱逐哪些 Pod,再真正执行:
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data --dry-run=client
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
验证。 在另一个终端挂着 PDB 观察:
kubectl -n maint-demo get pdb checkout-api-pdb -w
kubectl -n maint-demo get pods -o wide -l app=checkout-api -w
预期结果(未在当前环境实测,以下为预期结果):被驱逐的 Pod 进入 Terminating,Deployment 在另外两个 Zone 重建副本,ALLOWED 在驱逐窗口内短暂变为 0,随后恢复为 1,drain 命令以退出码 0 结束。
维护完成后把节点放回调度池:
kubectl uncordon worker-1
kubectl get node worker-1
示例输出:
NAME STATUS ROLES AGE VERSION
worker-1 Ready worker 9d v1.29.x
STATUS 回到 Ready 且不再带 SchedulingDisabled,才算这一轮结束。
跑通最小闭环
看清每个参数
留下真实问题
别急着做优化
06、排查
现象一:drain 卡住不返回
怀疑: PDB 不允许中断,drain 在重试。
检查:
kubectl -n maint-demo get pdb checkout-api-pdb
kubectl -n maint-demo describe pdb checkout-api-pdb
kubectl -n maint-demo get pods -o wide -l app=checkout-api
证据: get pdb 的 ALLOWED 列为 0;describe 中 DisruptionsAllowed 为 0、CurrentHealthy 等于 DesiredHealthy;get pods -o wide 显示三个副本里有两个落在同一台节点上。
根因: 两个因素叠加。第一,PDB 的可用预算被配成了 0(minAvailable 在副本数只有 3 的情况下等于全部副本)。第二,副本分布本身就不健康——节点维度的反亲和是 preferred 类型,调度器在资源紧张时完全可以忽略它,于是两个副本落在同一台机器上,一台机器就吃掉了一多半中断预算。
修复: 见第 07 章,先修分布,再修预算。
现象二:drain 直接报错退出
现象: 提示无法排空节点,并列出两类问题——存在 DaemonSet 管理的 Pod、存在使用本地存储的 Pod。
怀疑: 节点上有 DaemonSet 组件,以及用了 emptyDir 或 hostPath 的 Pod。
检查:
kubectl get pods -A -o wide --field-selector spec.nodeName=worker-1
kubectl -n kube-system get pod <pod-name> -o jsonpath='{.metadata.ownerReferences[*].kind}'
kubectl -n maint-demo get pod <pod-name> -o jsonpath='{.spec.volumes[*].emptyDir}'
证据: ownerReferences 的 kind 是 DaemonSet;volumes 里存在 emptyDir 字段。
根因: 这两类 Pod 用标准驱逐方式处理是没有意义的。DaemonSet 的 Pod 被删掉后会被立刻重建,而且它容忍 unschedulable 污点,可能又落回这台正在维护的机器上;emptyDir 的数据生命周期绑定在节点上,Pod 一旦离开,数据就是销毁。
修复: 对 DaemonSet 用 --ignore-daemonsets 明确跳过;对 emptyDir 用 --delete-emptydir-data 明确接受数据丢失。注意后者在较新版本的 kubectl 中才使用这个名字,旧版本的标志名与弃用状态需按当前官方文档核验。
错误尝试: 有同事提出直接 kubectl delete node worker-1,理由是节点对象都没了,Pod 自然会去别的地方。这个做法错在两点:一是删除 Node 对象绕过了 Eviction API 和 PDB,把一次可控的自愿中断变成了不可控的非自愿中断,PDB 提供的一切保证在此刻全部失效;二是节点上的 kubelet 重新注册后节点对象通常又会被建回来,中间产生的孤儿清理、状态不一致都要额外处理,维护流程反而更不可控。
现象三:drain 成功了,但节点一直空着
怀疑: 忘记 uncordon。
检查:
kubectl get nodes
kubectl describe node worker-1 | grep -i schedulable
证据: 节点状态里带 SchedulingDisabled,spec.unschedulable 为 true,但已经没有维护任务在跑。
根因: drain 完成后节点不会自动恢复调度,cordon 状态是持久的,必须显式 uncordon。
修复: 把 uncordon 纳入维护流程的收尾步骤,并在批次巡检里检查长期 unschedulable 的节点。
现象先于猜测
证据先于结论
错误尝试留档
根因才敢动手
07、优化
第 06 章的三条诊断链指向了四个根因,V2 逐条处理。
根因一:节点维度的反亲和是建议性的,副本仍会聚集。
修改:把可用区维度升级为强约束,节点维度作为尽力而为的补充。
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: checkout-api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: checkout-api
原因:维护操作的最小破坏单元是节点,但可用性的最小破坏单元是可用区。先保证可用区之间均匀(这是硬约束),再尽量保证节点之间均匀(这是软约束),顺序不能反——反过来会让调度器为了节点均匀而牺牲可用区均匀。
新行为:调度器在分配副本时必须满足可用区之间 maxSkew 不超过 1,不满足就让 Pod 处于 Pending 而不是硬塞进去。
验证:kubectl get pods -o wide -l app=checkout-api 观察 NODE 列分布在三个不同可用区;再对任意一台节点执行 --dry-run=client 的 drain,确认 PDB 的 ALLOWED 为 1。
根因二:PDB 用 minAvailable 表达,语义随副本数变化而漂移。
修改:改为 maxUnavailable: 1。
原因:维护场景真正关心的是「一次能坏几个」,而不是「必须留几个」。minAvailable 在副本数调整时会悄悄改变可用预算的比例,maxUnavailable 不会——扩容到 6 副本时,允许中断数仍然是 1,语义稳定。
新行为:无论副本数如何变化,单次自愿中断最多影响一个副本。
验证:把 replicas 从 3 改成 6,观察 PDB 的 ALLOWED 列仍为 1,而不是跟着翻倍。
根因三:drain 默认不设超时,卡住时会静默吃掉维护窗口。
修改:预检加超时两道闸门。
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data --dry-run=client
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
原因:--timeout 默认为 0,表示无限等待。这个默认值在交互式操作时很友好,在自动化脚本里就是事故——它会让一次卡住的 drain 悄悄占满整个窗口。
新行为:超时后 drain 主动失败退出,把「卡住」从一个静默状态变成一个可见的失败。
验证:在一个故意配置成阻塞的 PDB 上执行 drain,确认它在设定时间后以非 0 退出码结束。
根因四:本地存储型工作负载需要单独的维护路径。
修改:把工作负载按存储类型分成两类走不同流程。无状态负载走标准 cordon/drain/uncordon;依赖 local PV 的有状态负载不走 drain,而是先扩容或迁移数据,让数据所在的 Pod 不再是唯一副本之后再处理节点。
原因:local PV 的数据绑定在特定节点上,Pod 被驱逐后没有别的合规节点可去,只会一直 Pending。这不是调度策略能解决的问题,它是数据位置的物理约束。
新行为:维护计划生成时,先按存储类型分组,避免把有状态负载和无状态负载混在同一批次里。
验证:对一台挂载了 local PV 的节点执行 --dry-run=client 的 drain,确认预检阶段就能看到受影响的 Pod,而不是等到执行时才卡住。
改动跟着证据
验证跟着改动
不编漂亮数字
只写真实验证
08、演进
[同一输入]
|
+--[V1] preferred 反亲和 + minAvailable=3
| 代价:drain 卡死,维护窗口静默超时,只能 --force 硬删
|
+--[V2] required 可用区拓扑分布 + maxUnavailable=1 + 预检与超时
| 代价:调度灵活性下降,容量不均时副本可能 Pending
|
[Trade-off]
得到:单节点维护不再依赖人工判断,也不需要绕过 PDB
失去:调度器在容量紧张时的自由度
边界:副本数不少于可用区数量,且工作负载为无状态或有独立数据迁移路径
正确性。 V1 的正确性依赖运气:只要副本恰好分散,drain 就能过;一旦聚集,流程就停在 PDB 上。V2 把分散变成了进入调度的前置条件,不满足就不调度,正确性从偶然变成必然。
稳定性。 V1 的失败模式是静默的——drain 挂着不动,没有日志、没有告警,只有运维盯着屏幕。V2 的失败模式是显式的:要么 drain 超时以非 0 退出,要么副本 Pending 触发调度告警。显式失败比静默挂起更容易处理,这是稳定性上的实质提升。
复杂度。 V1 只有两条命令,新人当天就能上手。V2 多了拓扑约束、PDB 语义选择、预检和超时,还要按存储类型分流。复杂度确实上升了,但上升的部分是文档化和可评审的,而不是藏在人的经验里。
成本。 强约束会降低装箱率。可用区容量不均时,V2 可能为了满足 skew 而拒绝调度,导致资源利用率下降。这是用资源换可运维性,是否划算取决于集群规模和资产成本,需要按实际情况判断。
适用范围。 V2 适用于副本数不少于故障域数量的无状态服务。副本数太少的服务,强约束会导致永久 Pending,这类服务更适合用 PDB 的软约束配合人工判断。带本地存储的有状态服务需要独立流程,本方案只能覆盖其中的无状态部分。
遗留问题。 一是拓扑分布只约束了调度时刻,Pod 运行期间节点标签变化不会触发重平衡;二是跨可用区的 PDB 语义仍然是全局的,无法表达「同一可用区最多中断一个」这样的细粒度预算,这类约束目前主要靠变更流程本身来保证。
同输入不同果
得与失要同框
边界写在纸上
才知道能不能用
09、洞见
9.1 维护流程的瓶颈通常在调度,不在驱逐
所有人第一眼盯的都是 drain 这条命令,但真正让流程卡住的从来是「驱逐之后副本去哪」。驱逐是几秒钟的事,重建和选址才决定了整个窗口的长度。反直觉判断: 想让 drain 更快,应该优化的是调度约束,而不是驱逐参数。把 --force 加进去只是让卡住的步骤不卡了,代价是把问题推到了可用性上。
推论:维护预案的第一步不是写 drain 命令,而是把副本分布图拉出来看一眼。
9.2 PDB 是节流阀,不是可用性保证
PDB 只约束自愿中断,它不知道节点宕机、不知道宿主机断电、也不知道 kubelet 失联。把「业务连续」这件事全部押在 PDB 上,等于只覆盖了一半的风险。另一半来自副本数和拓扑分布——它们才是在非自愿中断发生时真正兜底的东西。
反直觉判断: PDB 配得越严格,系统的实际可用性未必越高。把 minAvailable 提到等于副本数,得到的是一个永远无法执行维护的集群,而不是一个更可靠的集群。
9.3 故障域的粒度决定了维护的粒度
以 Node 为维护单元、以 Zone 为容错单元、以 Region 为灾难恢复单元,这三者必须对齐。一旦错位,就会出现「只维护一台机器却打掉了一整个可用区的容量」这种事故——问题不在维护动作本身,而在副本分布从来没按这个粒度设计过。
推论:Failure Domain 不是架构评审文档里的名词,它是维护窗口能不能排下去的物理依据。
9.4 显式失败比静默挂起更值钱
默认不超时的 drain、ALLOWED 为 0 的 PDB、长期 unschedulable 的节点,这三件事的共同点是它们都不报错,只是静静地不正常。运维系统里最贵的从来不是错误,而是没有被表达出来的错误。加一个 --timeout、加一条 PDB 巡检、加一条节点状态告警,成本几乎为零,收益是把问题从「人肉发现」变成「系统发现」。
判断来自现场
直觉需要校准
结论必须可复现
经验才可复用
10、系统落地
原来有什么。一套靠人和经验驱动的节点维护流程:cordon、drain、uncordon 三条命令,加上一份写在本子上的操作顺序。PDB 配了,拓扑约束也配了,但两者都是「配了就行」的状态,没有人验证过它们组合起来会怎样。
本篇新增什么。四个可以被机器检查的约束:可用区维度的强制拓扑分布、以 maxUnavailable 表达的 PDB 预算、drain 的预检加超时、按存储类型分流的维护批次。它们都不是新组件,只是把原来散落在线上的经验写成了声明式配置和命令参数。
现在能做什么。在副本数足够、节点带故障域标签的集群上,可以做到:单台节点维护不需要人工盯屏;drain 卡住会在设定时间内主动失败而不是静默挂起;维护过程中的副本下降幅度是配置里写死的,不依赖当班工程师的判断;同一可用区不会同时出现两台维护中的节点(由流程约束保证)。
还缺什么。第一,缺自动化闸门——目前的批次串行还靠人工排期,没有控制器保证「同 Zone 最多一台」这个不变量。第二,缺拓扑约束的持续合规检查,节点标签变更后没有机制重新评估分布。第三,缺对 local PV 工作负载的标准化迁移路径,目前只能靠个案处理。
下一步如何演进。把「同可用区最多一台维护中」写成一个准入策略或运维控制器,让不变量从流程约定变成系统强制;把 PDB 的 ALLOWED 与副本分布做成常态化的巡检指标,在维护窗口开始前就暴露风险,而不是在 drain 卡住之后才发现。
原来只有命令
现在有了流程
还缺自动化闸门
交给控制器补上
11、小结
Q1 -> PDB 卡住是 PDB 配错了吗
不一定。更常见的是副本分布本来就不健康,PDB 只是把这个事实暴露出来了
Q2 -> 该用 --force 尽快腾空,还是先补齐拓扑约束
先补齐约束。--force 解决的是命令卡住,代价是把可控中断变成不可控中断
Q3 -> 选错了会付出什么代价
轻则维护窗口超时、流程回滚;重则某次激进操作撞上真实故障,可用副本跌破安全线
状态 -> 一套以可用区为容错单元、不依赖 --force 的单节点维护能力
本篇最终可交付的能力是:在副本数不少于故障域数量、节点带故障域标签的集群上,做到单节点 cordon/drain/uncordon 全流程不需要绕过 PDB,不需要人工盯屏,并且副本分布的均匀性在调度阶段就被强制保证,而不是等到维护时才被发现。
问题回到开头
答案落在证据
状态说清能力
边界标在图上
12、作业
12.1 理解题:cordon 之后,为什么 DaemonSet 仍然可能往这台节点上落新 Pod?
参考答案:cordon 改的是 Node 的 .spec.unschedulable,调度器据此拒绝普通 Pod。但 DaemonSet 控制器创建的 Pod 带有针对 node.kubernetes.io/unschedulable 污点的容忍,因此这类污点拦不住它们。这也是 drain 默认要求显式声明 --ignore-daemonsets 的原因:对 DaemonSet Pod 发起驱逐没有意义,它会被立刻重建,而且可能回到同一台机器上。
12.2 实战题:给一个三副本、PDB 为 maxUnavailable: 1 的服务,写出一份可在维护窗口开始前执行的预检脚本,要求在不做任何实际变更的前提下,判断当前集群能不能安全排空某一台节点。
参考答案:核心是三件事。第一,用 kubectl -n <ns> get pods -o wide -l <selector> 确认副本分布在几个不同的故障域上,是否存在单节点承载多个副本;第二,用 kubectl -n <ns> describe pdb <name> 确认 DisruptionsAllowed 大于 0,并核对 CurrentHealthy 与 DesiredHealthy;第三,用 kubectl drain <node> --ignore-daemonsets --delete-emptydir-data --dry-run=client 列出将被驱逐的 Pod,检查其中是否有裸 Pod、local PV 挂载或无控制器管理的资源。三项都通过,才允许在窗口内真正执行 drain。整个预检过程不做任何变更。
12.3 排障题:某次维护中 drain 命令在打出第一条驱逐日志后长时间无输出,Ctrl+C 退出后节点仍是 SchedulingDisabled 状态但 Pod 一个没走。请给出排查步骤。
参考答案:先确认是准入阻塞还是别的阻塞。第一步 kubectl -n <ns> get pdb 看 ALLOWED 列,若为 0 则说明可用中断预算耗尽;第二步 kubectl -n <ns> describe pdb <name> 看 DisruptionsAllowed、CurrentHealthy、DesiredHealthy 三个字段,判断是预算配置问题还是健康副本不足;第三步 kubectl get pods -o wide -l <selector> 看副本实际分布,确认是否存在单节点承载多个副本。节点停留在 SchedulingDisabled 是 cordon 状态未被撤销,属于 normal 行为,维护结束或放弃维护时都要显式 uncordon。
12.4 架构判断题:某团队把 PDB 的 minAvailable 设置为等于副本数,理由是「这样最安全,任何情况都不允许少副本」。请评价这个设计。
参考答案:这个配置在语义上等于把可用中断预算设为 0,任何驱逐请求都会被拒绝,所有依赖 Eviction API 的操作——包括标准 drain、节点池自动缩容、集群自动伸缩器的缩容——都会永久卡住。它并没有提高系统的实际可用性,因为节点宕机、宿主机故障这些非自愿中断根本不经过 PDB,PDB 拦不住也保护不了。真正的可用性来自副本数和跨故障域的拓扑分布。合理做法是让允许中断数大于 0 且小于副本数,例如副本数为 3 时使用 maxUnavailable: 1,同时用拓扑约束保证副本分散在不同故障域上。
会做还要会答
会答还要会判
四道题闭环
一次真正掌握
13、思考
这一篇的核心冲突其实只有一句话:我们在做节点维护的时候,到底是在执行一条命令,还是在做一次有边界的可用性权衡。
如果是执行命令,那么所有注意力都会落在 drain 的输出上,卡住了就想办法让它不卡——加 --force、加 --disable-eviction,甚至绕过 PDB 直接删 Pod。这条路每一步都很快,但每快一步,可用性的保证就少一层。
如果是做权衡,那么第一件要做的事就不在命令上,而在分布上:副本散落在哪些节点、哪些可用区;故障域之间是不是真的互相独立;预算应该表达成「必须留几个」还是「最多坏几个」。这些问题在维护窗口之前就有答案,窗口内只是按既定计划执行。
可复用的工程判断是:把运维操作的阻塞点当作设计缺陷的显式暴露,而不是当作流程障碍。drain 卡住不是 PDB 在捣乱,它在告诉你这个工作负载从来没有为「少一台机器」这件事做过准备。修好分布,卡点会自己消失;绕过卡点,问题只会在下一次非自愿中断时以更贵的代价回来。
一念辨边界
一证定根因
一改知取舍
一役见真章

浙公网安备 33010602011771号