笃行
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
首页
个人 & 心法
互联网/硬件后台
游戏基础架构
UE 引擎
游戏业务
AI / 大模型
数据结构与算法
机器学习数学
通用基础
GitHub
  • 接入与通信

    • 游戏基础架构与工具
    • 接入网关(长连接接入层)
    • 消息总线(共享内存 IPC)
    • 帧同步(Lockstep)
    • 全栈极限低延迟(物理层→应用层)
    • KCP 与 QUIC
  • 网络与服务网格

    • CNI 与 K8s 网络插件
    • 容器运行时:Docker 与 containerd
    • Istio 与 Cilium 服务网格
    • 服务网格:中心化 vs 去中心化
    • eBPF 原理与在网络/可观测/安全的落地
    • 自研 Mesh 服务网格 × K8s 部署
    • 自研 Mesh × K8s · 数据面深水区
    • Tco 协程框架 · 运行时深水区
    • K8s 异构 & 网络插件
  • 有状态服务

    • 分布式游戏存储(分布式 KV)
    • 有状态服务的数据迁移
    • 有状态服务的数据恢复与容灾
    • 有状态服务的 K8s 编排层治理(防漂移 + 节点崩溃恢复)
    • 内存配置热刷新与更新机制
  • 算法与协程

    • 一致性哈希四算法实现(Ring Hash / Ketama / Maglev / Jump Hash)
    • 蓄水池抽样(Reservoir Sampling)与加权扩展
    • C++ 协程:有栈 vs 无栈 vs C++20 原生(函数染色与 libco)
    • Raft 与 Gossip:CP 强一致共识 vs AP 最终一致传播
  • 流量与承载

    • 游戏秒杀场景承载
    • 令牌桶与漏桶
    • 限流与熔断
    • 限流:互联网 vs 游戏
    • 灰度发布 · Canary / BlueGreen / A-B / Shadow
    • 服务器日常系统开发与维护 SOP
    • 极限发压工具设计(多长连接 + 发压任务)
    • 性能分析与优化方法论(六条排查线)
  • 编译与排查

    • 编译优化与 LLVM/Clang/GCC
    • Sanitizer 工具链与内存泄漏定位

有状态服务的 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:NoExecute taint,等 300 秒(tolerationSeconds);
  • 结果 StatefulSet Pod 一直卡在 Terminating 状态——K8s 默认不会自动在别处重建它;
  • 值班人硬着头皮 kubectl delete pod --grace-period=0 --force 强删——但原节点其实还没死透,两个 Pod 同时挂载 PVC 写数据,脑裂!

这两类事故对应的动作分工:

  • 防漂移——事前把"想跑在哪 / 不能跑在哪"用硬约束钉死;
  • 崩溃恢复——事后把"卡住的 Pod 安全地拉到新节点",且必须防脑裂。

实现方案

段落 A:防漂移

概念对齐:K8s 语义里的"漂"

社区没有"anti-drift"官方术语——"漂"= Pod 被调度到、或被驱逐后重调度到非预期节点。触发路径三条:

  1. 首次调度:Scheduler 按约束找候选节点,约束写弱了就飘。
  2. 被驱逐 + 重调度:kubelet 内存压力驱逐、NodeController NotReady 驱逐、descheduler 主动重平衡、抢占(preemption)。
  3. 重启: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 的区别:

特性topologySpreadConstraintsPodAntiAffinity
语义定量约束(差多少个不许)定性约束(一起 / 不能一起)
表达力精细(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(拓扑约束在扩容时也生效)。

治理姿势:

  1. 扩容时用 ScheduleAnyway 允许违反;
  2. 或用 HPA + PDB 组合,minAvailable 与 topology 副本数对齐;
  3. 或干脆不给关键有状态服务上 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)10skubelet 汇报频率
--node-lease-duration-seconds40sLease 有效期
--node-monitor-grace-period (kube-controller-manager)40s判定 NotReady 的容忍窗口
tolerationSeconds (Pod 上)300sPod 能容忍多久 NoExecute taint

驱逐时序:Taint-Based Eviction

K8s 1.13+ 后驱逐机制全面走 Taint-Based Eviction:

  • 节点 NotReady → 打 node.kubernetes.io/unreachable:NoExecute taint
  • 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 会自动:

  1. 强制删除该节点上所有 Pod(不等 terminationGracePeriodSeconds);
  2. Force-Detach 所有 PVC(跨 CSI 主动调用 ControllerUnpublishVolume);
  3. StatefulSet controller 在别处重建 Pod;
  4. 新 Pod attach 相同的 PVC,绑定关系不变。

版本时间线:

特性版本
NonGracefulShutdown1.26 Beta / 1.28 GA
unhealthy-pod-eviction-policy1.27 Beta / 1.28 Beta
Node Lease1.14 GA
Taint-Based Eviction1.13 GA
topologySpreadConstraints1.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 一般组合使用:

  1. 存储侧 Fencing(最直接):CSI 主动 ControllerUnpublishVolume / SAN 强制 detach / iSCSI reset。原节点即使还活着,也写不进 PVC了。
  2. 电源侧 Fencing(最彻底):通过 IPMI / API / PDU 直接关掉原节点电源。原节点整个不存在了。
  3. API 侧 Fencing(K8s 层):out-of-service taint 是 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 起来了但状态归零,用户仍无服务。
  • 给关键业务开 NoExecute taint 且不设 tolerationSeconds:一次瞬时抖动就把关键 Pod 驱逐了。

三个绝对不做的动作

  1. 在原节点没确认死透前 Force-Detach PVC:脑裂即数据事故;
  2. 对关键 StatefulSet 用 preferred 代替 required:Pod 一定会漂;
  3. 绕过 PDB 强制 drain 节点:正是 PDB 存在的意义就是防这个。

沉淀结论

  • 两轴模型:编排层 × 业务层 × 计划 vs 故障——本篇负责"编排层×两者",业务层交给 stateful-migration / stateful-recovery。
  • 防漂移四把旋钮:nodeAffinity required 硬约束 + topologySpreadConstraints 拓扑均匀 + PodDisruptionBudget 限自愿驱逐 + priorityClass 防被抢占;再加 descheduler 定期回收 + WaitForFirstConsumer 存储拓扑感知。
  • 崩溃检测链:kubelet Lease → node-monitor-grace-period 40s → NoExecute taint → tolerationSeconds 300s → 驱逐。
  • 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 自动:

  1. 强制删除该节点所有 Pod(不等 grace period);
  2. 跨 CSI 主动 ControllerUnpublishVolume(Force-Detach 所有 PVC);
  3. StatefulSet controller 在别处重建 Pod;
  4. 新 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-service taint / 驱逐前置健康校验。

至少存储侧 + API 侧双层的原因:单层可能失效(存储侧的 detach 可能因 CSI Bug 卡住;API 侧的 taint 可能被误标);两层互相验证形成证据链。有条件加电源侧最稳(直接消除物理源)。

Q5: `topologySpreadConstraints` 与 `PodAntiAffinity` 有什么区别?什么时候用哪个?
特性topologySpreadConstraintsPodAntiAffinity
语义定量(差多少个不许,maxSkew)定性(一起 / 不能一起)
表达力精细二元
性能好(1.19+ 专门优化)Pod 多时调度慢
  • 用 topologySpreadConstraints:大多数场景——跨 AZ 均匀、跨 Node 均匀。
  • 用 PodAntiAffinity:绝对不能同宿主这种二元硬约束(比如"两个副本必须在两台不同机器")。

优先用 topologySpreadConstraints。

Q6: HPA + `topologySpreadConstraints DoNotSchedule` 为什么会冲突?怎么治理?

冲突场景:HPA 缩容后再扩容,若 topology 已经违反 maxSkew(比如某个 zone 副本数已经比其他 zone 多),扩容出的新 Pod 会永远 Pending(拓扑约束在扩容时也生效)。

治理三条路:

  1. 允许违反:改为 whenUnsatisfiable: ScheduleAnyway;
  2. PDB 对齐:minAvailable 与 topology 副本数对齐,防止缩容缩到冲突;
  3. 不给关键有状态服务上 HPA:它们本来就不该"根据 CPU 自动伸缩"——分片规模应该是严肃决定的,不是自动伸缩的。

结论:HPA 是无状态服务的工具,别硬套在有状态服务上。

Q7: `ReadWriteOnce` PVC 的 Multi-Attach Error 是障碍还是保护?

是保护。RWO 的语义是"同时只能被一个 Node attach",Multi-Attach Error 正是 K8s 在保护你不脑裂——它不敢主动强剪原节点的 attach,除非你明确宣告原节点已死。

面对这个错误的正确做法:

  1. 首选:打 out-of-service taint 让 K8s 自动 Force-Detach(1.28 GA);
  2. 备选:先做存储侧 Fencing(CSI ControllerUnpublish),再人工 Force-Detach;
  3. 错误做法:绕过它、直接 patch PVC finalizer——脑裂风险高。
最近更新: 2026/9/10 11:38
Prev
有状态服务的数据恢复与容灾
Next
内存配置热刷新与更新机制