有状态服务的 K8s 编排层治理(防漂移 + 节点崩溃恢复)
nodeAffinity / topologySpreadConstraints / PDB · Taint-Based Eviction · StatefulSet 保守语义 · NonGracefulShutdown · 脑裂 Fencing
一句话结论
K8s 编排层治理有两大动作:防漂移——用调度硬约束(nodeAffinity / 拓扑约束 / PDB / priorityClass / descheduler)把 Pod 钉在该在的位置;节点崩溃恢复——kubelet 心跳失联 → NodeController 打 NoExecute taint → tolerationSeconds 熬完 → StatefulSet 默认不会自动重建(防脑裂),靠 K8s 1.28 GA 的 NonGracefulShutdown(out-of-service taint)触发自动 Force-Detach PVC + 重调度,或人工介入。业务态数据回放交给 stateful-recovery。
场景问题
先摆清定位:K8s 编排层的治理是Pod / Node / PVC 层面的问题,业务态数据层的迁移与恢复由 stateful-migration 与 stateful-recovery 承担。两轴四象限:
| 计划内主动变更 | 故障 | |
|---|---|---|
| 业务态(内存/连接/数据) | stateful-migration:快照 + 增量 + 短冻结 + 切流 | stateful-recovery:checkpoint + WAL + 副本接管 |
| 编排态(Pod / Node / PVC) | 本篇 · 段落 A:防漂移(调度约束 + descheduler) | 本篇 · 段落 B:节点崩溃恢复(Eviction + Force-Detach + Fencing) |
本篇的红线:不复述业务态
本篇不复述 checkpoint / WAL / 切流的机制,只在段落 B 结尾以一句话接续到 stateful-recovery——编排层拉起空 Pod 之后,业务态回放才是让服务真正可用的一步。K8s 网络原理详见 k8s-network。
真实的游戏区服在 K8s 上会撞两类事故:
事故 A:Pod 漂到不该去的节点
- 某个战斗服 StatefulSet 期望"每台 GPU 节点只跑一个",但重启后被 scheduler 排到普通 CPU 节点上(
nodeAffinity写错了 required vs preferred); - 或者多个副本被塞到同一台机器(
topologySpreadConstraints没配),一次机器故障把整个 shard 端了; - 或者 HPA 缩容把关键副本缩没了(没有 PDB),触发 StatefulSet 重排导致大批 Pod 漂移。
事故 B:节点整机崩溃后 Pod 卡住
- 某台节点掉电或内核死锁,
kubelet心跳断了 40 秒; - NodeController 打上
node.kubernetes.io/unreachable:NoExecutetaint,等 300 秒(tolerationSeconds); - 结果 StatefulSet Pod 一直卡在 Terminating 状态——K8s 默认不会自动在别处重建它;
- 值班人硬着头皮
kubectl delete pod --grace-period=0 --force强删——但原节点其实还没死透,两个 Pod 同时挂载 PVC 写数据,脑裂!
这两类事故对应的动作分工:
- 防漂移——事前把"想跑在哪 / 不能跑在哪"用硬约束钉死;
- 崩溃恢复——事后把"卡住的 Pod 安全地拉到新节点",且必须防脑裂。
实现方案
段落 A:防漂移
概念对齐:K8s 语义里的"漂"
社区没有"anti-drift"官方术语——"漂"= Pod 被调度到、或被驱逐后重调度到非预期节点。触发路径三条:
- 首次调度:Scheduler 按约束找候选节点,约束写弱了就飘。
- 被驱逐 + 重调度:
kubelet内存压力驱逐、NodeControllerNotReady 驱逐、descheduler主动重平衡、抢占(preemption)。 - 重启:Pod OOM/Crash 后由 controller(Deployment/StatefulSet)重建,落到 scheduler 手里再选一次。
任何一环约束不硬,Pod 就可能漂。
硬约束:把 Pod 钉在该在的位置
apiVersion: v1
kind: Pod
spec:
# ① nodeSelector:最简单,标签精确匹配(够用就用它)
nodeSelector:
node-type: game-server
# ② nodeAffinity:更强大,支持表达式与 required vs preferred
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬约束,不满足绝不调度
nodeSelectorTerms:
- matchExpressions:
- key: workload
operator: In
values: ["game-battle"]
preferredDuringSchedulingIgnoredDuringExecution: # 软约束,尽量满足
- weight: 100
preference:
matchExpressions:
- key: zone
operator: In
values: ["cn-shanghai-a"]
# ③ Toleration:允许 Pod 容忍某类 Taint(配合污点使用)
tolerations:
- key: dedicated-for
operator: Equal
value: game-battle
effect: NoSchedule
required vs preferred:一字之差
- required(硬约束):不满足 = 不调度(Pod 一直 Pending)。
- preferred(软约束):不满足 = 降低偏好但仍会调度。
真实事故八成出在把 preferred 当 required 用——写完发现"怎么飘了",其实调度器只是"倾向于",从没承诺过。关键位置必须 required。
Taint + Toleration 的对偶:给节点打污点,只允许有对应 toleration 的 Pod 上;用于专用节点(GPU 池 / 战斗服池 / 大内存池)。
NoSchedule:新 Pod 不来(老的不动);PreferNoSchedule:软性不来;NoExecute:新 Pod 不来 + 老 Pod 被驱逐(注意 tolerationSeconds)。
拓扑约束:把副本分散开
topologySpreadConstraints 是 1.19 GA、专门为"跨拓扑域均匀分布"设计的:
spec:
topologySpreadConstraints:
- maxSkew: 1 # 任何两个拓扑域之间 Pod 数差不超过 1
topologyKey: topology.kubernetes.io/zone # 按 zone 分散
whenUnsatisfiable: DoNotSchedule # 无法满足就 Pending(vs ScheduleAnyway)
labelSelector:
matchLabels:
app: game-battle
- maxSkew: 1
topologyKey: kubernetes.io/hostname # 也按 node 分散
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: game-battle
与 PodAntiAffinity 的区别:
| 特性 | topologySpreadConstraints | PodAntiAffinity |
|---|---|---|
| 语义 | 定量约束(差多少个不许) | 定性约束(一起 / 不能一起) |
| 表达力 | 精细(maxSkew 可调) | 二元 |
| 性能 | 好(1.19+ 专门优化) | Pod 多时调度慢 |
| 用法 | 拓扑域均匀分散 | 一 Node / Zone 只能一个 |
优先用 topologySpreadConstraints,PodAntiAffinity 只在"绝对不能同宿主"这种二元需求上用。
保护旋钮
PDB(PodDisruptionBudget)——限制自愿驱逐:
apiVersion: policy/v1
kind: PodDisruptionBudget
spec:
minAvailable: 2 # 至少保留 2 个可用(或用 maxUnavailable)
selector:
matchLabels:
app: game-battle
- 自愿驱逐(voluntary disruption):
kubectl drain、node upgrade、cluster autoscaler 缩容——受 PDB 限制。 - 非自愿驱逐(involuntary disruption):节点掉电、内核 panic、
kubelet心跳丢失——PDB 管不住。
priorityClass + preemptionPolicy——防被抢占:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: game-battle-priority
value: 100000
globalDefault: false
preemptionPolicy: PreemptLowerPriority # 或 Never(不抢占别人)
description: "Battle servers - never preempted by lower priority"
节点冷冻(kubectl cordon):把节点标 unschedulable: true,新 Pod 不来;配合 kubectl drain 优雅腾空节点做维护。
反漂移动作:descheduler + HPA 冲突治理
descheduler 是 sig-scheduling 的社区项目(不是 K8s 核心组件),定期扫描已调度 Pod 并驱逐"位置不合适"的:
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: default
pluginConfig:
- name: "RemoveDuplicates" # 同 Node 上同 Deployment 的重复 Pod
- name: "LowNodeUtilization" # 均衡低利用率节点
args:
thresholds:
cpu: 20
memory: 20
targetThresholds:
cpu: 50
memory: 50
- name: "RemovePodsViolatingTopologySpreadConstraint" # 违反拓扑约束
- name: "RemovePodsViolatingNodeAffinity" # 违反 nodeAffinity
HPA 与 topologySpreadConstraints 的冲突
HPA 缩容时,K8s 挑"最不重要"的 Pod 干掉;但如果 topologySpreadConstraints 是 DoNotSchedule,缩下去的下一个再扩上来的 Pod 可能永远 Pending(拓扑约束在扩容时也生效)。
治理姿势:
- 扩容时用
ScheduleAnyway允许违反; - 或用 HPA + PDB 组合,
minAvailable与 topology 副本数对齐; - 或干脆不给关键有状态服务上 HPA——它们本来就不该自动伸缩。
有状态特化:StatefulSet + volumeClaimTemplates 的每 Pod 独立 PVC
apiVersion: apps/v1
kind: StatefulSet
spec:
serviceName: game-battle
replicas: 3
volumeClaimTemplates: # 每个 Pod 自动创建一份独立 PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
关键:volumeClaimTemplates 为每个 Pod(game-battle-0/1/2)生成独立 PVC(data-game-battle-0/1/2)。Pod 被删/重建时,PVC 保留、绑定关系不变。这是有状态服务能"重启后拿回数据"的基础。
StorageClass.volumeBindingMode: WaitForFirstConsumer 与拓扑感知:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度完再创建 volume
allowedTopologies:
- matchLabelExpressions:
- key: topology.kubernetes.io/zone
values: ["cn-shanghai-a", "cn-shanghai-b"]
为什么用 WaitForFirstConsumer:若一开始 Immediate 创建 volume,volume 落在 zone-a,之后 Pod 因 topologySpreadConstraints 被调度到 zone-b,就出现"volume 和 Pod 跨 AZ"的悲剧(跨 AZ 挂载不了或性能巨差)。WaitForFirstConsumer = "等 Pod 定了再创建 volume",从根本上避免了这个坑。
段落 B:节点崩溃恢复
检测链路:从心跳失联到 Taint
Node Lease(K8s 1.14+):每个节点在 kube-node-lease namespace 有一份 Lease 对象,kubelet 每 10 秒续期。相比早期"心跳=更新整个 Node 对象",Lease 便宜得多,让 API Server 承压能力大幅上升。
关键参数:
| 参数 | 默认 | 意义 |
|---|---|---|
--node-status-update-frequency (kubelet) | 10s | kubelet 汇报频率 |
--node-lease-duration-seconds | 40s | Lease 有效期 |
--node-monitor-grace-period (kube-controller-manager) | 40s | 判定 NotReady 的容忍窗口 |
tolerationSeconds (Pod 上) | 300s | Pod 能容忍多久 NoExecute taint |
驱逐时序:Taint-Based Eviction
K8s 1.13+ 后驱逐机制全面走 Taint-Based Eviction:
- 节点 NotReady → 打
node.kubernetes.io/unreachable:NoExecutetaint - Pod 上默认有
tolerationSeconds: 300→ 熬 300 秒后被驱逐 - 熬的过程可以自定义 tolerationSeconds(长于 300s 更保守,短于则更激进)
K8s 1.27+ 新增 unhealthy-pod-eviction-policy(PDB Beta 字段):
apiVersion: policy/v1
kind: PodDisruptionBudget
spec:
minAvailable: 2
selector: { matchLabels: { app: game-battle } }
unhealthyPodEvictionPolicy: AlwaysAllow # 或 IfHealthyBudget (默认)
IfHealthyBudget(默认):只有健康副本 ≥minAvailable时才允许驱逐任何 Pod(连不健康的都要保)——保守。AlwaysAllow:不健康的 Pod 随时可驱逐,只保健康副本满足 PDB——关键:正是这个新特性让"节点崩了但 PDB 卡死不能强删卡死 Pod"的老问题得到缓解。
StatefulSet 的保守语义:默认不会自动重建
这里是最坑的一点:
- Deployment / ReplicaSet 的 Pod:控制器会自动在别处重建(因为它们是"无身份"的,一个是一个)。
- StatefulSet 的 Pod:Pod 有唯一名字(
game-battle-0)、绑定独立 PVC(data-game-battle-0)、可能绑定独立 hostname/网络身份。K8s 默认不敢自动在别处重建——因为原节点其实可能只是失联,不代表原 Pod 已死;如果原 Pod 还在写 PVC,新 Pod 也起来写 PVC,就是脑裂(详见下节)。
于是 StatefulSet Pod 在节点失联后表现为:
NAME READY STATUS RESTARTS AGE
game-battle-0 0/1 Terminating 0 3d
game-battle-1 1/1 Running 0 3d
game-battle-2 1/1 Running 0 3d
game-battle-0 永远卡在 Terminating,直到有人明确告诉 K8s 原节点已死——这就是恢复动作的核心。
恢复动作谱系
从完全自动到完全人工三档:
① K8s 1.28 GA 的 NonGracefulShutdown——推荐路径
给节点手动或程序化打 node.kubernetes.io/out-of-service:NoExecute taint:
kubectl taint nodes <node-name> node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
这个 taint 是运维层的明确宣告:"这个节点我确认已经死透了,K8s 可以按'节点非优雅关机'处理"。K8s 会自动:
- 强制删除该节点上所有 Pod(不等
terminationGracePeriodSeconds); - Force-Detach 所有 PVC(跨 CSI 主动调用
ControllerUnpublishVolume); - StatefulSet controller 在别处重建 Pod;
- 新 Pod attach 相同的 PVC,绑定关系不变。
版本时间线:
| 特性 | 版本 |
|---|---|
NonGracefulShutdown | 1.26 Beta / 1.28 GA |
unhealthy-pod-eviction-policy | 1.27 Beta / 1.28 Beta |
| Node Lease | 1.14 GA |
| Taint-Based Eviction | 1.13 GA |
topologySpreadConstraints | 1.19 GA |
② 完全人工:kubectl delete pod --grace-period=0 --force
kubectl delete pod game-battle-0 --grace-period=0 --force
- 强删该 Pod 的 API 对象(不管
terminationGracePeriodSeconds); - 但:K8s 不会自动 Force-Detach PVC(这是 controller 的保守设计——它不知道原节点真死没死);
- 需要人工确认 + 手动 Force-Detach(
kubectl patch pvc <pvc> -p '{"metadata":{"finalizers":null}}'之类的补救); - 强删的风险:如果原节点其实还活着(网络分区导致失联),你强删了 API 对象但原 Pod 进程仍在写 PVC——脑裂!
③ 完全人工兜底
一切自动化都不行,值班人 SSH 到原节点、关电源、拔存储、确认后再让 K8s 重调度。适合"最后一根救命稻草",不适合日常。
三档如何选
- 一等 SOP:
NonGracefulShutdown——但必须有能"外部确认节点死透"的信号(IPMI / API / 电源探针)才能自动打 out-of-service taint,否则也是脑裂。 - 二等 SOP:手工 Force-Delete + Force-Detach PVC,且必须先 Fencing(下一节)。
- 三等 SOP:物理断电确认后再动 K8s——出事概率最低,但 RTO 最长。
脑裂风险与三层 Fencing
脑裂:原节点其实还活着(只是网络失联),你强删 K8s Pod 对象并在别处重建;两个 Pod 现在同时:
- 挂载 PVC → 两个进程同时写同一块盘(数据分叉、文件系统损坏);
- 监听端口 → 两个进程接同一份客户端请求(业务逻辑双写、状态分叉)。
Fencing(隔离/围栏)是"确保原实例不再有影响力"的机制:
三层 Fencing 一般组合使用:
- 存储侧 Fencing(最直接):CSI 主动
ControllerUnpublishVolume/ SAN 强制 detach / iSCSI reset。原节点即使还活着,也写不进 PVC了。 - 电源侧 Fencing(最彻底):通过 IPMI / API / PDU 直接关掉原节点电源。原节点整个不存在了。
- API 侧 Fencing(K8s 层):
out-of-servicetaint 是 K8s 层的"我宣告死透";配合驱逐前置健康校验。
Kubernetes 社区的最佳实践:至少存储侧 + API 侧双层(有条件加电源侧最稳)。
存储侧配合
ReadWriteOnce (RWO) PVC 的重挂延迟:
RWO 的语义是"同时只能被一个 Node attach"。原 Pod 卡在 Terminating 时,PVC 还 attach 在原节点上;新 Pod 想 attach → 报 Multi-Attach error:
Warning FailedAttachVolume attachdetach-controller
Multi-Attach error for volume "pvc-abcd..." Volume is already exclusively
attached to one node and can't be attached to another
这个错误正是"K8s 的保守设计在保护你"——它不敢主动强剪原节点的 attach,除非你明确告诉它"原节点已死"(用 out-of-service taint 或人工 Force-Detach)。
CSI Node Plugin 崩溃后的清理:如果 CSI 驱动本身也在挂掉的节点上,K8s 有时会执行不了 NodeUnstageVolume / NodeUnpublishVolume。此时需要 CSI Controller 侧的 ControllerUnpublishVolume 强制解绑,甚至存储阵列侧手动清理。
接续到业务态
编排层拉起空 Pod(新节点 + 新进程)之后,业务态还是空的——内存态、连接态、进度都归零。这时接续到 stateful-recovery:从 PVC 里加载最近 checkpoint 作为基线、回放 WAL 追到最后一条落盘记录、防脑裂 fencing(业务层再做一次 term/lease 校验)、恢复后自检不变量再对外服务。
编排层完成到"Pod 起来了、PVC 挂上了";业务层完成到"内存态回到了 T0 时刻"——两层各司其职,不复述。
为什么这么做
- 为什么编排层与业务层要分家:
stateful-migration/stateful-recovery讲的是业务态(内存、连接、数据),K8s 讲的是编排态(Pod、Node、PVC)。二者的 SLI 指标(一个是 RPO/RTO,一个是"Pod 何时可再调度")、修复主体(一个是应用程序、一个是 K8s 控制平面)、失败模式(一个是数据分叉、一个是脑裂/Multi-Attach)都不同。合起来讲会稀释规范;拆开讲各自打透。 - 为什么防漂移必须用
required硬约束:preferred是"调度器倾向于",从来不保证。真实事故 80% 是把preferred当required用——直到事故那天才知道调度器只是"倾向于"。 - 为什么 StatefulSet 默认不自动重建:因为 K8s 无法从远处判断原节点是"死透了"还是"网络失联但进程还在"。默认"保守"= 宁可 RTO 长一点,也不冒脑裂风险。K8s 的设计哲学是:不确定就不做。
- 为什么
NonGracefulShutdown(out-of-service taint)是关键突破:把"我宣告节点死透"这个运维层信号通过 taint 显式表达给 K8s,K8s 才敢自动 Force-Detach + 重调度。之前的十年里,社区一直缺这个信号,全靠人工。 - 为什么 Fencing 是脑裂唯一解:分布式共识本身无法在异步网络下同时保证 Safety 和 Liveness(FLP 定理),K8s 的心跳不能可靠区分"死"与"慢"。唯一的解法是外部真源——存储侧强制 detach、电源侧强制关机、API 侧显式宣告——把不确定性从"猜"变成"确认"。
为什么别的选择不行
- 不设 required 硬约束、只靠 preferred:Pod 会漂到不该去的节点。
- 只用
PodAntiAffinity而不用topologySpreadConstraints:PodAntiAffinity只能表达"不能一起",无法定量控制"最多差几个",且大集群下调度慢。 - 给 StatefulSet 直接开 HPA 自动缩扩:有状态服务的副本数应该是"根据分片规模严肃决定",不是"根据 CPU 自动伸缩"。滥用 HPA 会让 PVC 频繁 attach/detach,触发一堆边缘 bug。
- 节点故障后直接 Force-Delete 而不 Fencing:只是运气好没脑裂。运气用完的那天就是一次数据事故。
- 用
kubectl drain替代NonGracefulShutdown:drain是优雅驱逐(等 grace period),要求 kubelet 还活着;节点心跳都失联了,drain根本执行不下去。 - 只做业务态恢复不做编排层治理:
stateful-recovery假设"进程还在、只是状态丢了";如果连 Pod 都没起来,stateful-recovery无从下手。反过来只做编排层不做业务恢复:Pod 起来了但状态归零,用户仍无服务。 - 给关键业务开
NoExecutetaint 且不设 tolerationSeconds:一次瞬时抖动就把关键 Pod 驱逐了。
三个绝对不做的动作
- 在原节点没确认死透前 Force-Detach PVC:脑裂即数据事故;
- 对关键 StatefulSet 用 preferred 代替 required:Pod 一定会漂;
- 绕过 PDB 强制 drain 节点:正是 PDB 存在的意义就是防这个。
沉淀结论
- 两轴模型:编排层 × 业务层 × 计划 vs 故障——本篇负责"编排层×两者",业务层交给 stateful-migration / stateful-recovery。
- 防漂移四把旋钮:
nodeAffinityrequired 硬约束 +topologySpreadConstraints拓扑均匀 +PodDisruptionBudget限自愿驱逐 +priorityClass防被抢占;再加descheduler定期回收 +WaitForFirstConsumer存储拓扑感知。 - 崩溃检测链:kubelet Lease →
node-monitor-grace-period40s →NoExecutetaint →tolerationSeconds300s → 驱逐。 - StatefulSet 保守语义:Pod 唯一名字 + PVC 绑定不释放,默认不自动重建——防脑裂。
- 恢复三档:
NonGracefulShutdown(1.28 GA,最推荐) > 手工 Force-Delete + Force-Detach + Fencing > 物理断电确认后再动。 - 脑裂三层 Fencing:存储侧(CSI ControllerUnpublish / SAN detach)+ 电源侧(IPMI / API 关电)+ API 侧(out-of-service taint + 前置校验)。
- RWO PVC 的 Multi-Attach Error 是保护而不是障碍——不要绕过它。
- 编排层完成到"Pod + PVC 就绪",业务层负责"内存态回到 T0"——两层各司其职。
记忆口诀
两轴四象限:计划/故障 × 业务态/编排态——本篇=编排态两象限
防漂四旋钮:nodeAffinity required / topologySpreadConstraints / PDB / priorityClass
+两利器:descheduler 定期回收 / WaitForFirstConsumer 拓扑感知
required vs preferred:required=不满足不调度 / preferred=倾向但不保证——关键位置一律 required
崩溃检测链:kubelet Lease → 40s grace → NoExecute taint → 300s toleration → 驱逐
StatefulSet 保守:Pod 唯一名 + PVC 绑定 → 默认不自动重建 / 防脑裂
恢复三档:NonGracefulShutdown(out-of-service taint, 1.28 GA)→ Force-Delete + Force-Detach → 物理断电
三层 Fencing:存储(CSI detach)+ 电源(IPMI 关电)+ API(out-of-service)
Multi-Attach = 保护:K8s 不敢强剪原 attach,靠 out-of-service 或 Fencing 明确宣告
版本关键:NonGracefulShutdown 1.26 Beta / 1.28 GA · unhealthy-pod-eviction-policy 1.27+ · Node Lease 1.14 GA · topologySpread 1.19 GA · descheduler 是社区项目
三不做:强剪 PVC 前不 Fencing / 关键 StatefulSet 用 preferred / 绕过 PDB 强 drain
内容来源
综合整理。参考方向:Kubernetes 官方文档(kube-controller-manager 参数、Taint-Based Eviction、StatefulSet 保守语义、NonGracefulShutdown KEP-2268、unhealthy-pod-eviction-policy KEP-3017、topologySpreadConstraints KEP-895)、sig-scheduling 的 descheduler 项目、CSI 规范(ControllerUnpublishVolume 语义)、存储 Fencing 实践(Ceph RBD watcher、iSCSI reset、SAN detach)、以及站内 stateful-migration / stateful-recovery / k8s-network 的边界对齐。
自测:合上资料能说清楚吗?
Q1: `stateful-migration` / `stateful-recovery` / `k8s-stateful-ops` 三者的边界是什么?
两轴模型:
- 业务态 × 计划内 =
stateful-migration:主动搬迁,有活源,快照 + 增量 + 短冻结 + 切流。 - 业务态 × 故障 =
stateful-recovery:无活源,checkpoint + WAL 回放 + 副本接管。 - 编排态 × 计划内 =
k8s-stateful-ops段落 A(本篇):防漂移,nodeAffinity / 拓扑约束 / PDB。 - 编排态 × 故障 =
k8s-stateful-ops段落 B(本篇):节点崩溃恢复,NonGracefulShutdown+ 三层 Fencing。
编排层完成到"Pod 起来 + PVC 挂上",业务层完成到"内存态回到 T0",两层各司其职、互相接续。
Q2: 节点整机 NotReady 后,StatefulSet 的 Pod 为什么会卡在 Terminating?
因为 StatefulSet 语义"保守":Pod 有唯一名字(game-battle-0)、绑定独立 PVC(data-game-battle-0)。K8s 无法从远处判断原节点是"死透"还是"网络失联但进程还在"。如果贸然在别处重建 Pod:
- 两个 Pod 同时挂载同一 PVC → 双写数据分叉;
- 两个 Pod 同时监听同一网络身份 → 双写业务状态。
即脑裂。所以默认"宁可 RTO 长一点也不冒脑裂风险"——除非有明确的外部信号(out-of-service taint 或人工介入)告诉 K8s 原节点已死透。
Q3: K8s 1.28 的 `NonGracefulShutdown` 具体做什么?为什么它是关键突破?
给节点打 node.kubernetes.io/out-of-service:NoExecute taint。这个 taint 是运维层的明确宣告:"我确认这个节点死透了,K8s 可以按'节点非优雅关机'处理"。之后 K8s 自动:
- 强制删除该节点所有 Pod(不等 grace period);
- 跨 CSI 主动
ControllerUnpublishVolume(Force-Detach 所有 PVC); - StatefulSet controller 在别处重建 Pod;
- 新 Pod attach 相同 PVC,绑定关系不变。
为什么是关键突破:之前十年里,社区一直缺这个"运维层信号"通道,导致节点崩溃后 StatefulSet 恢复必须人工介入(kubectl delete pod --force + 手动 Force-Detach)。NonGracefulShutdown 让这个流程可以自动化——只要有能确认节点死透的外部信号(IPMI / API / 电源探针),就能自动打这个 taint 触发恢复。
Q4: 三层 Fencing 各做什么?为什么至少要两层?
- 存储侧 Fencing:CSI 主动
ControllerUnpublishVolume/ SAN 强制 detach / iSCSI reset。原节点即使还活着,也写不进 PVC。 - 电源侧 Fencing:IPMI / API / PDU 直接关掉原节点电源。原节点整个不存在了。
- API 侧 Fencing:K8s 层的
out-of-servicetaint / 驱逐前置健康校验。
至少存储侧 + API 侧双层的原因:单层可能失效(存储侧的 detach 可能因 CSI Bug 卡住;API 侧的 taint 可能被误标);两层互相验证形成证据链。有条件加电源侧最稳(直接消除物理源)。
Q5: `topologySpreadConstraints` 与 `PodAntiAffinity` 有什么区别?什么时候用哪个?
| 特性 | topologySpreadConstraints | PodAntiAffinity |
|---|---|---|
| 语义 | 定量(差多少个不许,maxSkew) | 定性(一起 / 不能一起) |
| 表达力 | 精细 | 二元 |
| 性能 | 好(1.19+ 专门优化) | Pod 多时调度慢 |
- 用
topologySpreadConstraints:大多数场景——跨 AZ 均匀、跨 Node 均匀。 - 用
PodAntiAffinity:绝对不能同宿主这种二元硬约束(比如"两个副本必须在两台不同机器")。
优先用 topologySpreadConstraints。
Q6: HPA + `topologySpreadConstraints DoNotSchedule` 为什么会冲突?怎么治理?
冲突场景:HPA 缩容后再扩容,若 topology 已经违反 maxSkew(比如某个 zone 副本数已经比其他 zone 多),扩容出的新 Pod 会永远 Pending(拓扑约束在扩容时也生效)。
治理三条路:
- 允许违反:改为
whenUnsatisfiable: ScheduleAnyway; - PDB 对齐:
minAvailable与 topology 副本数对齐,防止缩容缩到冲突; - 不给关键有状态服务上 HPA:它们本来就不该"根据 CPU 自动伸缩"——分片规模应该是严肃决定的,不是自动伸缩的。
结论:HPA 是无状态服务的工具,别硬套在有状态服务上。
Q7: `ReadWriteOnce` PVC 的 Multi-Attach Error 是障碍还是保护?
是保护。RWO 的语义是"同时只能被一个 Node attach",Multi-Attach Error 正是 K8s 在保护你不脑裂——它不敢主动强剪原节点的 attach,除非你明确宣告原节点已死。
面对这个错误的正确做法:
- 首选:打
out-of-servicetaint 让 K8s 自动 Force-Detach(1.28 GA); - 备选:先做存储侧 Fencing(CSI ControllerUnpublish),再人工 Force-Detach;
- 错误做法:绕过它、直接 patch PVC finalizer——脑裂风险高。