Kubernetes 基础体系 · 第 25/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Pod 拓扑设计:Zone、Node、故障域、反亲和和数据局部性
Pod 拓扑设计解决的不是“Pod 应该放在哪台机器上”这么简单的问题,而是要同时回答以下问题:
- 一个 Pod 与另一个 Pod 放在同一台 Node、同一个 Zone,还是不同 Zone?
- 某个 Zone 整体故障时,副本是否仍然可用?
- 节点故障、机架故障、可用区故障分别会破坏哪些副本?
- Pod 的计算位置是否接近它访问的数据?
- 为了高可用而跨 Zone 分布,是否会增加网络延迟和跨区流量?
- 调度约束之间冲突时,Pod 是被调度到次优位置,还是直接变成 Pending?
这些问题分别由 Node、拓扑域、故障域、亲和与反亲和、Topology Spread Constraints,以及存储拓扑共同决定。
一、先建立几个不同层次的概念
1. Node 是 Kubernetes 的调度单位
Node 是 Kubernetes 调度器可以选择的基本计算节点。一个 Pod 通常会被绑定到一个 Node,并由该节点上的 kubelet 创建和管理。
可以查看节点及其标签:
kubectl get nodes --show-labels
更适合阅读的方式是只显示常用拓扑标签:
kubectl get nodes \
-L kubernetes.io/hostname \
-L topology.kubernetes.io/zone \
-L topology.kubernetes.io/region
典型输出可能类似:
NAME STATUS ROLES AGE VERSION HOSTNAME ZONE REGION
worker-a1 Ready <none> 20d v1.31.2 worker-a1 zone-a region-1
worker-a2 Ready <none> 20d v1.31.2 worker-a2 zone-a region-1
worker-b1 Ready <none> 20d v1.31.2 worker-b1 zone-b region-1
worker-c1 Ready <none> 20d v1.31.2 worker-c1 zone-c region-1
Kubernetes 通常使用以下标准标签描述拓扑:
kubernetes.io/hostname
topology.kubernetes.io/zone
topology.kubernetes.io/region
其中:
kubernetes.io/hostname通常表示节点主机名;topology.kubernetes.io/zone表示 Zone;topology.kubernetes.io/region表示 Region。
这些标签是 Kubernetes 识别拓扑的输入,但标签本身并不自动保证物理含义。具体 Zone 是否对应独立供电、独立网络设备、独立机房,取决于云厂商或集群运营方的实现。裸机环境中如果管理员错误地给两个物理机房打上相同的 Zone 标签,Kubernetes 也无法自行纠正。
2. Zone 是一种拓扑域,不等于任意一种“机房”
拓扑域是由某个 topologyKey 划分出来的一组节点。
例如:
topologyKey = kubernetes.io/hostname
表示按 Node 划分拓扑域:
worker-a1
worker-a2
worker-b1
worker-c1
而:
topologyKey = topology.kubernetes.io/zone
表示按 Zone 划分拓扑域:
zone-a: worker-a1, worker-a2
zone-b: worker-b1
zone-c: worker-c1
因此,Zone 不是调度器中的特殊魔法对象,而是节点标签值形成的逻辑分组。调度器关心的是:
- 哪些节点存在;
- 这些节点有哪些拓扑标签;
- 某个约束使用哪个
topologyKey; - 每个拓扑域中已有多少匹配 Pod。
Region 通常包含多个 Zone。Zone 通常包含多个 Node,但这不是 Kubernetes API 层面的强制关系,而是基础设施常见的组织方式。
3. 故障域描述“哪些组件可能一起失败”
故障域是具有相近故障原因或故障传播路径的一组资源。
常见层次如下:
Region
└── Zone
└── 机架 / 电源域 / 网络故障域
└── Node
└── Pod
这不是 Kubernetes 规定的唯一层级,而是一种设计模型。不同基础设施中,故障域可能是:
- 单个 Node;
- 一个机架;
- 一组共享电源的服务器;
- 一个网络交换机下的节点;
- 一个 Zone;
- 一个 Region;
- 一个存储系统的副本组。
如果两个副本放在不同 Node,但这些 Node 共享同一个机架和电源,那么它们只实现了 Node 级分散,没有实现机架级故障隔离。
因此,“副本分散”必须先说明分散到哪一级:
| 目标 | 常用 topologyKey |
|---|---|
| 不在同一台 Node | kubernetes.io/hostname |
| 不在同一个 Zone | topology.kubernetes.io/zone |
| 不在同一个 Region | topology.kubernetes.io/region |
| 不在同一个机架 | 使用集群实际提供的机架标签 |
| 不在同一个电源或网络故障域 | 使用基础设施实际提供的标签 |
如果不存在真实可靠的标签,Kubernetes 不能凭空推导故障域。
二、调度约束的共同基础:硬约束、软约束和候选节点
一个 Pod 的调度可以抽象为两个阶段:
- 过滤(Filter):排除不满足硬约束的 Node;
- 评分(Score):在剩余 Node 中选择更符合软约束的 Node。
可以把最终候选节点集合写成:
其中:
- :状态可调度的节点;
- :满足 CPU、内存等资源请求的节点;
- :Pod 的 toleration 允许使用的节点;
- :满足节点亲和约束的节点;
- :满足 Pod 亲和、反亲和和拓扑分布约束的节点;
- :满足存储卷拓扑和节点关联的节点。
如果某个硬约束导致 ,Pod 就无法调度。软约束不会把节点从候选集合中删除,而是影响评分。
nodeSelector 和节点亲和
最简单的硬约束是 nodeSelector:
spec:
nodeSelector:
topology.kubernetes.io/zone: zone-a
这表示 Pod 只能调度到具有该标签且值为 zone-a 的 Node。
节点亲和可以表达更复杂的条件:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
- zone-b
requiredDuringSchedulingIgnoredDuringExecution 的含义是:
- 调度时必须满足;
- Pod 调度完成后,如果节点标签后来发生变化,Kubernetes 不会仅因为这个变化主动驱逐 Pod。
软约束使用:
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- zone-a
weight 只参与评分,不是保证。若 zone-a 没有资源,而 zone-b 可以运行 Pod,Pod 仍可能进入 zone-b。
Taint 和 toleration 不是拓扑分布机制
Taint 通常用于表达“某类 Pod 不应进入这个节点”,例如专用节点、故障节点或基础设施节点:
kubectl taint nodes worker-a1 dedicated=storage:NoSchedule
只有声明对应 toleration 的 Pod 才能被调度到该节点:
spec:
tolerations:
- key: dedicated
operator: Equal
value: storage
effect: NoSchedule
Taint 解决的是节点准入,不是“副本之间应该如何分散”。一个 Pod 即使能够进入某个节点,也不代表它应该和同应用的其他副本位于同一个 Zone。
三、Pod 亲和与反亲和:按已有 Pod 进行约束
Pod 亲和和反亲和根据其他 Pod 的标签决定放置位置。
- Pod affinity:希望与匹配 Pod 靠近;
- Pod anti-affinity:希望与匹配 Pod 分开。
下面的示例表示:这个 Pod 不得与具有 app=web 标签的 Pod 位于同一个 Node:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname
这里的 topologyKey 非常关键。相同的 labelSelector,换一个 topologyKey,含义就完全不同:
topologyKey: kubernetes.io/hostname
表示不在同一台 Node。
topologyKey: topology.kubernetes.io/zone
表示不在同一个 Zone。
反亲和的完整推导
假设当前有三个节点:
zone-a / node-a1
zone-a / node-a2
zone-b / node-b1
已有两个 app=web Pod:
web-0 -> node-a1
web-1 -> node-b1
新 Pod 使用如下硬反亲和:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: topology.kubernetes.io/zone
检查每个候选节点:
node-a1属于zone-a,该 Zone 已有web-0,排除;node-a2也属于zone-a,虽然本节点没有 Pod,但同 Zone 已有web-0,排除;node-b1属于zone-b,已有web-1,排除;- 如果不存在第三个 Zone,则没有候选节点。
这说明“反亲和到 Zone”不是“尽量跨 Zone”,而是强制要求每个匹配副本占据不同 Zone。副本数超过 Zone 数时,硬反亲和必然导致 Pending。
反亲和的常见误解
误解一:Node 反亲和能防止同一故障域故障
如果两个 Node 位于同一个机架或 Zone,使用:
topologyKey: kubernetes.io/hostname
只能避免单 Node 故障,不能避免机架或 Zone 故障。
误解二:反亲和会重新平衡已经运行的 Pod
IgnoredDuringExecution 表示约束主要在调度时检查。集群扩容、标签变化或新增 Zone 后,已有 Pod 不会自动为了满足更理想的分布而迁移。
误解三:软反亲和是严格保证
preferredDuringSchedulingIgnoredDuringExecution:
只是评分偏好。当资源不足、节点污点、存储拓扑或其他约束冲突时,Pod 仍可能与匹配 Pod 放在一起。
四、Topology Spread Constraints:表达“尽量均匀分布”
Pod 反亲和更适合表达“不能共处”,而 topologySpreadConstraints 更适合表达“在多个拓扑域之间尽量均匀”。
一个典型 Deployment 如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 6
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
这段配置同时要求:
app=web的 Pod 在 Zone 之间最多相差 1 个;app=web的 Pod 在 Node 之间最多相差 1 个;- 如果无法满足,Pod 不调度,而不是接受更差的分布。
maxSkew 的数学含义
对每个拓扑域 ,定义:
令:
则某个拓扑域的偏斜度为:
当新 Pod 放入某个候选域后,若:
才满足该约束。
假设有三个 Zone,当前副本分布为:
zone-a: 2
zone-b: 1
zone-c: 0
如果 maxSkew: 1:
- 放入
zone-a后:3,1,0,最小值为 0,最大偏斜为 3,不满足; - 放入
zone-b后:2,2,0,最大偏斜为 2,不满足; - 放入
zone-c后:2,1,1,最大偏斜为 1,满足。
因此调度器会优先或只能选择 zone-c。
如果当前分布为:
zone-a: 1
zone-b: 1
zone-c: 0
新 Pod 放入 zone-c 后得到:
zone-a: 1
zone-b: 1
zone-c: 1
这正是最均匀的结果。
whenUnsatisfiable 的两种语义
whenUnsatisfiable: DoNotSchedule
表示这是硬约束。无法满足时,Pod 保持 Pending。
whenUnsatisfiable: ScheduleAnyway
表示这是软约束。调度器会尽量改善分布,但在资源或其他硬约束限制下仍可接受偏斜。
生产系统通常需要根据业务选择:
- 无法接受单 Zone 集中时,使用
DoNotSchedule; - 更重视始终保持可调度,允许暂时不均匀时,使用
ScheduleAnyway; - 副本数小于拓扑域数时,硬约束可能让部分域为空,但这不一定是错误。
拓扑域必须有一致的标签
如果部分 Node 没有 topology.kubernetes.io/zone,调度器无法可靠地把它们归入 Zone。此时可能出现:
- Pod 被调度到不具备预期 Zone 标签的节点;
- Spread 计算的候选域不符合运维人员直觉;
- 使用 Zone 相关存储时,Pod 无法找到兼容节点;
- 节点标签变化后,新旧 Pod 的分布语义不一致。
因此,应把拓扑标签纳入节点注册、云控制器或基础设施自动化流程,而不是在故障发生时手工临时补标签。
五、反亲和与拓扑分布应该如何选择
设有 3 个 Zone 和 6 个副本。
方案一:Zone 级硬反亲和
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: topology.kubernetes.io/zone
这个约束要求同一 Zone 最多有一个匹配 Pod。因此最多只能调度 3 个副本,剩余 3 个会 Pending。
方案二:Zone 级拓扑分布
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
6 个副本可以分布为:
zone-a: 2
zone-b: 2
zone-c: 2
这符合均匀分布要求。
方案三:同时做 Zone 和 Node 分布
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: web
这表达了两个独立目标:
- Zone 之间均衡;
- 同一 Node 上不集中多个副本。
但这并不保证每个 Zone 中的每台 Node 都恰好只有一个副本。它只保证每个约束定义的计数偏斜不超过指定值。
约束冲突时的结果
假设只有两个 Zone:
zone-a: 2 个可用节点
zone-b: 1 个可用节点
Deployment 需要 6 个副本,同时要求:
- Zone
maxSkew: 1; - Node
maxSkew: 1; - 每个 Pod 请求 4 CPU;
zone-b的唯一节点只有 2 CPU 可用。
即使 Zone 级分布从逻辑上可行,资源约束也会使 zone-b 无法承载副本。此时:
- 若 Zone Spread 是
DoNotSchedule,Pod 可能 Pending; - 若是
ScheduleAnyway,可能在zone-a集中; - 若另有硬反亲和,候选集合可能直接为空。
拓扑设计不是独立的装饰性配置,而是资源、污点、节点亲和、存储拓扑等约束的交集。
六、调度器不会为“高可用”自动定义你的业务目标
Kubernetes 只执行声明的约束。它不知道:
- 两个 Zone 是否真的独立;
- 跨 Zone 流量是否昂贵;
- 应用是否支持跨 Zone 复制;
- 一个副本是否拥有独立数据;
- 某个 Node 上运行的多个 Pod 是否共享同一物理故障点;
- 业务需要“低延迟”还是“跨故障域容灾”。
例如,只写:
replicas: 3
并不能保证:
zone-a: 1
zone-b: 1
zone-c: 1
三个 Pod 可能都被调度到同一个 Zone 的不同 Node,甚至在资源充足时位于同一 Node。
同样,写了 Zone Spread 也不代表应用数据已经跨 Zone 复制。它只控制 Pod 的计算位置;数据是否分布、是否可恢复,取决于存储系统。
七、数据局部性:计算位置和数据位置必须一起分析
数据局部性是指计算 Pod 与其访问的数据尽可能位于接近的拓扑域中。
“接近”可能意味着:
- 同一 Node:本地磁盘或本地缓存;
- 同一 Zone:区域内网络访问;
- 同一 Region:跨 Zone 但仍在区域内;
- 同一存储故障域:数据副本和计算副本具有可接受的故障关系。
数据局部性通常降低网络路径和延迟,但可能减少高可用性。例如:
- Pod 与 Zone 内的云盘靠近,访问延迟较低;
- 如果整个 Zone 故障,Pod 可能无法在其他 Zone 使用该云盘;
- 把无状态副本跨 Zone 分布可以提高可用性;
- 把每个有状态副本和它的卷强行跨 Zone 迁移,可能反而不可行。
因此,局部性和高可用不是同一个优化目标。
八、PVC、PV 和存储拓扑如何参与调度
1. PV 可能带有节点或拓扑限制
一个 PersistentVolume 可以通过 nodeAffinity 声明它只能被哪些节点使用。下面是一个静态 Local PV 示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-a1
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /var/lib/local-data/app-1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-a1
这个 PV 只能在 worker-a1 上使用。它不是一个可以被任意节点挂载的远程卷。
对应的 StorageClass:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
其中:
kubernetes.io/no-provisioner表示没有动态 provisioner;- Local PV 通常由管理员预先创建;
WaitForFirstConsumer表示不要在 PVC 创建时立刻绑定,而要等到有 Pod 使用它时,再结合 Pod 的调度条件选择合适的 PV。
创建 PVC:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-storage
resources:
requests:
storage: 100Gi
创建使用该 PVC 的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx:1.27
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
2. 延迟绑定的调度过程
使用 WaitForFirstConsumer 时,过程大致如下:
sequenceDiagram
participant U as 用户
participant API as API Server
participant S as Scheduler
participant B as PV/PVC 绑定控制器
participant K as kubelet
participant N as Node
U->>API: 创建 PVC
API-->>U: PVC Pending
U->>API: 创建使用 PVC 的 Pod
API-->>S: Pod 进入待调度队列
S->>S: 检查资源、亲和、拓扑和可用 PV
S->>B: 为 PVC 选择并绑定兼容 PV
B-->>API: 更新 PVC/PV 绑定状态
S->>API: 将 Pod 绑定到 Node
API-->>K: Pod 期望状态
K->>N: 挂载或准备卷
K-->>API: Pod Running 或报告挂载错误
关键点是:存储约束会反过来影响 Pod 的候选节点。
例如:
- PV 只能在
worker-a1; - Pod 的节点亲和只允许
zone-b; worker-a1位于zone-a。
那么候选集合为空,Pod 会 Pending,而不是“先调度再尝试挂载”。
3. 云盘的 Zone 局部性
许多云厂商的块存储卷带有 Zone 属性。一个卷可能只能挂载到同 Zone 的节点,或者支持有限的跨 Zone 附加能力。具体行为由 CSI 驱动和云厂商实现决定,不能把某一家云的行为当成 Kubernetes 规范。
使用 CSI 动态存储时,通常需要确认:
- StorageClass 的
volumeBindingMode; - CSI 驱动是否正确发布拓扑信息;
- 卷是否具有 Zone 或 Region 限制;
ReadWriteOnce是否限制为单节点挂载;- 节点故障后卷能否重新附加;
- 跨 Zone 重新调度时是否需要复制数据或恢复卷。
WaitForFirstConsumer 能避免卷在错误 Zone 提前创建,但它不能把单 Zone 卷变成跨 Zone 高可用存储。
九、Local PV 的局部性与故障恢复边界
Local PV 的数据直接位于某个 Node 的本地路径。它具有很强的数据局部性,但也暴露了明确的故障边界:
Pod -> Node 本地路径
当 Node 故障时,通常同时发生:
- Pod 无法继续在该 Node 上运行;
- Local PV 也无法在其他 Node 上直接访问;
- Deployment 控制器可能创建替代 Pod;
- 替代 Pod 找不到可用的同一数据卷;
- PVC 仍然绑定原 PV,业务可能保持 Pending 或等待人工恢复。
因此,Local PV 的“节点故障自动恢复”能力不能与远程复制存储混为一谈。
如果业务数据必须在 Node 故障后恢复,通常需要额外机制,例如:
- 应用自身的数据复制;
- 存储系统的副本和故障转移;
- 定期备份与恢复;
- 将数据放在可跨节点附加的远程存储;
- 预先设计卷替换和数据重建流程。
Local PV 适合对本地 I/O 或数据局部性有强需求、且应用或存储层已经处理复制和恢复的场景。仅仅因为它“快”,就把唯一数据放在 Local PV 上,会把 Node 故障直接扩大为数据不可用风险。
十、StatefulSet 中的拓扑和数据关系
StatefulSet 为每个副本提供稳定身份和通常独立的 PVC。假设有 3 个副本:
db-0 -> data-db-0
db-1 -> data-db-1
db-2 -> data-db-2
这与 Deployment 的共享 PVC 语义不同。每个 Pod 的存储绑定关系可能具有独立拓扑限制。
一个简化的 StatefulSet 示例:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 3
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: db
containers:
- name: db
image: postgres:17
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: zonal-block
resources:
requests:
storage: 100Gi
这段配置只表达了 Pod 的 Zone 分布和每个副本的 PVC 模板。它并不保证:
- PostgreSQL 已经配置了跨 Zone 复制;
- 每个 PVC 都一定创建在不同 Zone;
- 主库故障后应用能够自动选主;
- 卷中的数据能够跨 Zone 恢复。
如果 StorageClass 的动态卷创建在某个 Zone,而 StatefulSet 又要求 Pod 分散到多个 Zone,必须确认 CSI 驱动和延迟绑定能让两者共同成立。否则某个副本可能因为卷拓扑与 Pod 拓扑冲突而 Pending。
十一、一个可验证的无状态应用拓扑示例
下面的 Deployment 同时表达 Zone 和 Node 两级分布:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 6
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: api
containers:
- name: api
image: nginx:1.27
resources:
requests:
cpu: 100m
memory: 128Mi
应用配置:
kubectl apply -f api.yaml
kubectl rollout status deployment/api
kubectl get pods -l app=api -o wide
查看 Pod 到 Zone 的分布:
kubectl get pods -l app=api -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
再结合节点标签检查:
kubectl get pods -l app=api -o wide --no-headers |
while read pod ready status restarts age ip node rest; do
zone=$(kubectl get node "$node" -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
printf '%s\t%s\t%s\n' "$pod" "$node" "$zone"
done
预期可以看到类似:
api-xxx-1 worker-a1 zone-a
api-xxx-2 worker-a2 zone-a
api-xxx-3 worker-b1 zone-b
api-xxx-4 worker-b2 zone-b
api-xxx-5 worker-c1 zone-c
api-xxx-6 worker-c2 zone-c
实际分布可能不同,但在资源和标签条件满足时,Zone 计数应符合 maxSkew: 1。
如果 Pod Pending,先查看事件:
kubectl describe pod <pod-name>
常见事件包括:
0/6 nodes are available:
2 node(s) didn't match pod topology spread constraints,
2 node(s) didn't match pod anti-affinity rules,
2 Insufficient cpu.
这类信息说明问题通常不是“调度器随机选错了节点”,而是多个过滤条件叠加后已经没有合适候选节点。
十二、故障路径:不同拓扑设计会产生不同结果
1. 单 Node 故障
假设 3 个副本分布为:
zone-a / node-a1: api-0
zone-b / node-b1: api-1
zone-c / node-c1: api-2
node-a1 故障后:
api-0所在节点不可用;- Deployment 发现可用副本数下降;
- 创建替代 Pod;
- 调度器依据当前约束选择可用节点;
- 如果
zone-a仍有其他节点且资源足够,替代 Pod 可能回到zone-a; - 如果 Zone 级分布要求不允许继续进入
zone-a,则可能选择其他 Zone; - 如果所有符合约束的节点都没有资源,替代 Pod Pending。
控制器只负责维持副本数,不负责保证每个副本都有可恢复的数据。
2. 整个 Zone 故障
如果原来是:
zone-a: 2
zone-b: 2
zone-c: 2
zone-a 故障后,剩余副本为 4 个。对于无状态应用,只要剩余 Zone 有足够资源,Deployment 可以重建丢失副本。
但如果每个副本使用只能位于 zone-a 的卷,计算 Pod 即使能被重新调度,也无法使用原数据。此时计算层高可用不能抵消存储层的 Zone 限制。
3. 重新调度与卷重新挂载之间的并发
对于远程卷,Node 故障后常见过程是:
- kubelet 不再报告原 Pod 正常运行;
- 控制器创建或确认替代 Pod;
- 卷控制器尝试从旧节点卸载或确认旧节点已失效;
- CSI 驱动尝试把卷附加到新节点;
- kubelet 挂载文件系统;
- 容器启动。
旧节点状态不明确时,卷可能因为仍被认为已附加而暂时无法挂载到新节点。这个过程受 CSI 驱动、云厂商控制面和节点故障类型影响,不能通过 Pod 反亲和解决。
十三、生产中最容易出现的设计错误
错误一:副本数大于 Zone 数,却使用 Zone 硬反亲和
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: topology.kubernetes.io/zone
这意味着同一 Zone 只能有一个匹配 Pod。副本数为 5、Zone 数为 3 时,至少 2 个 Pod 会 Pending。
如果目标是均衡而不是绝对互斥,应考虑:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
错误二:只按 Node 分散,却误以为实现了跨机架高可用
Node 级分散不能抵御共享机架、电源或网络设备故障。需要使用真实的机架或故障域标签。
错误三:只设置 Zone Spread,却忽略 Zone 容量
三个 Zone 的节点数量和可用资源可能完全不同:
zone-a: 20 个节点
zone-b: 3 个节点
zone-c: 1 个节点
maxSkew: 1 仍然试图让 Pod 数量接近均匀,而不是按容量比例分配。如果 zone-c 容量不足,硬约束会造成 Pending。
可以选择:
- 增加各 Zone 容量;
- 使用
ScheduleAnyway接受容量约束下的偏斜; - 按业务容量重新规划故障域;
- 将不同工作负载拆分到不同调度策略。
错误四:以为调度分布等于数据复制
Pod 跨 Zone 只说明计算实例跨 Zone。数据库数据是否安全,取决于数据库复制、卷复制、备份和恢复流程。
错误五:忽略服务流量的拓扑
Pod 分散后,请求可能通过 Service 访问跨 Zone 后端。计算高可用可能提高网络成本和延迟。
Service 的拓扑感知流量分配需要单独配置和验证,例如使用 trafficDistribution 等能力时,必须确认目标 Kubernetes 版本、kube-proxy 或实现组件是否支持。它与 Pod 调度约束是两个不同层面:
调度约束:Pod 放在哪里
Service 流量:请求转发到哪里
前者不会自动改变后者。
十四、如何诊断“分布不符合预期”
第一步:确认节点标签
kubectl get nodes \
-L kubernetes.io/hostname \
-L topology.kubernetes.io/zone \
-L topology.kubernetes.io/region
检查:
- 是否所有目标 Node 都有 Zone 标签;
- Zone 值是否拼写一致;
- 是否存在错误标签或过期标签;
- 标签是否真的对应预期故障域。
第二步:确认 Pod 实际所在位置
kubectl get pods -A -o wide
按应用过滤:
kubectl get pods -l app=api -o wide
调度分布只能通过实际 Pod 和 Node 位置验证,不能只看 Deployment 的 replicas。
第三步:查看调度事件
kubectl describe pod <pod-name>
重点关注:
Insufficient cpu、Insufficient memory;didn't match Pod's node affinity/selector;didn't match pod anti-affinity rules;didn't match pod topology spread constraints;- volume node affinity conflict;
- PVC 未绑定或卷无法挂载。
第四步:检查 PVC、PV 和卷拓扑
kubectl get pvc,pv
kubectl describe pvc <pvc-name>
kubectl describe pv <pv-name>
对于 CSI 卷,还应检查 StorageClass:
kubectl get storageclass <storage-class-name> -o yaml
确认:
volumeBindingMode: WaitForFirstConsumer
是否符合预期,并检查 CSI 驱动是否正常运行:
kubectl get pods -A | grep -i csi
第五步:区分“没有调度”与“调度后启动失败”
- Pod 处于
Pending:优先看调度、PVC 和资源; - Pod 已经绑定 Node 但处于
ContainerCreating:优先看卷挂载、网络、镜像; - Pod 进入
CrashLoopBackOff:拓扑可能已经满足,问题转向应用启动和数据状态; - Pod 被驱逐:检查节点压力、资源请求和
NoExecutetaint。
不能因为 Pod 没有 Running 就笼统归因于“拓扑配置错误”。
十五、版本和实现边界
本文示例使用当前 Kubernetes 稳定 API 组:
apps/v1:Deployment、StatefulSet;v1:Pod、PV、PVC;policy/v1:PodDisruptionBudget;storage.k8s.io/v1:StorageClass;topologySpreadConstraints:Pod 规范中的稳定调度字段。
需要特别区分以下事项:
topology.kubernetes.io/zone和topology.kubernetes.io/region的具体标签值依赖云厂商或集群基础设施;- 云盘是否跨 Zone 可用、是否支持自动重新附加,由 CSI 驱动和云厂商决定;
WaitForFirstConsumer能协调初次绑定与调度,但不保证故障后的数据恢复;- 新版本中可能增加拓扑分布的节点纳入策略、最小域数等字段,使用前应以目标集群版本的 API 文档和服务器 OpenAPI 定义为准;
- 不同调度器配置、默认插件配置和云控制器行为可能影响评分与拓扑标签同步;
- Kubernetes 的调度约束不替代应用层复制、备份、选主和灾备演练。
十六、建立拓扑设计时应先回答的形式化问题
对一个工作负载,先定义:
副本数 R
故障域集合 D
每个故障域的容量 capacity(d)
每个 Pod 的资源需求 request
数据卷允许的拓扑集合 V
业务允许的最大延迟和跨域流量
然后分别判断:
计算可行性
是否存在一个分配函数:
使得每个 Pod:
- Node 资源足够;
- 满足节点亲和与污点条件;
- 满足 Pod 亲和、反亲和;
- 满足拓扑分布的
maxSkew; - 满足卷的节点或 Zone 限制。
故障域可用性
对任意目标故障域 ,故障后剩余容量是否足以运行所需副本:
如果这个条件不成立,调度配置再严格,也无法在该故障后恢复全部副本。
数据恢复性
若某个卷只能在故障域 使用,则必须额外回答:
d 故障时,数据是否有副本?
副本是否位于 d 之外?
恢复需要多长时间?
恢复期间业务是否只读、降级或不可用?
这部分不由 Pod 的 replicas 或反亲和字段自动回答。
结语
Pod 拓扑设计的核心不是简单地给 Deployment 增加一段反亲和配置,而是把以下关系明确建模:
Node 是调度位置
Zone 是标签定义的拓扑域
故障域是共同失败的边界
反亲和限制副本不能共处
Topology Spread 约束副本分布偏斜
存储拓扑限制数据可访问的位置
Node 级分散主要抵御单节点故障,Zone 级分散主要抵御可用区故障,更高层故障域则需要基础设施提供可靠标签。无状态工作负载通常适合使用 Zone 与 Node 两级拓扑分布;有状态工作负载还必须把 PVC、PV、CSI 拓扑、数据复制和恢复路径一起设计。
只有当计算位置、数据位置、资源容量和故障恢复流程彼此一致时,“跨 Zone 高可用”才是实际成立的系统属性,而不是 YAML 中的一组看起来合理的字段。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes Priority 与 Preemption:优先级、抢占、公平和关键服务保护
- 下一篇:Kubernetes Service:ClusterIP、NodePort、LoadBalancer、EndpointSlice 和流量
- 延伸:Kubernetes 调度约束:NodeSelector、Affinity、Taint、Topology 和 Spread
- 延伸:Kubernetes 存储拓扑:Zone、Local PV、延迟绑定和故障恢复
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论