Kubernetes Pod 亲和性与反亲和性:调度原理与排障实战 开门见山聊调度Kubernetes 里的“亲和性与反亲和性”说白了就是告诉调度器两件事Pod 希望被放到哪些节点上、希望和哪些 Pod 待在一起或者反过来不希望和谁挨得太近。这个能力在部署高可用服务、打散多副本、绑定特殊硬件节点、优化服务间访问延迟这些场景里非常常用。这篇文章面向的是已经被 Pod 部署折磨过几次的运维、开发或 SRE 同学。默认调度器只保证 Pod 能跑起来至于“跑在哪”“和谁一起跑”“是否跨可用区分散”它一概不管。很多线上事故比如所有副本被调度到同一台故障机器上、跨可用区单点、服务调用延迟暴涨根源都出在“调度太随意”上。亲和性与反亲和性就是用来纠正这种随意的。我把实际排查中积累的经验、YAML 写法、以及踩过的坑一起整理出来。没有废话都是可以直接抄的配置和定位思路。1. 为什么需要亲和性与反亲和性1.1 默认调度器的短板默认的调度器在选择节点时只看资源是否满足要求CPU、内存、端口、存储。节点标签它也会看但只看被 nodeSelector 指定的那部分。只要资源够Pod 落在哪个节点上对调度器来说“都一样”。可现实场景里真不一样。举个我实际遇到过的例子我们有一批跑批处理任务的 Pod磁盘 IO 密集只有一部分节点挂了 SSD。如果不对调度做约束这批 Pod 会被随机打散到机械盘节点上任务耗时直接翻倍。还有一次一个核心服务的三个副本被调度到了同一台物理机上当晚那台机器硬件告警整个服务直接不可用而集群里其他节点全都空闲。这些问题靠手动控制节点资源占用不现实。你刚把副本 A 调到 node1下次发布又变回默认随机。所以必须在 Pod 的定义里把调度意图表达出来让调度器每次都能按同样的规则执行。再补一个容易混淆的点很多人以为设置了资源 request 和 limit调度器就会“考虑”负载均衡。并不会。它只保证单 Pod 的资源申请能被满足。两个高负载副本落在同一节点、其他节点空转这种局面它完全接受。所以补充亲和性约束不是锦上添花而是部署策略落地的一种手段。1.2 两个维度节点亲和与 Pod 亲和Kubernetes 的亲和性体系分两层节点亲和nodeAffinity决定 Pod 喜欢去哪些节点上通过节点标签来匹配。Pod 亲和podAffinity与 Pod 反亲和podAntiAffinity根据集群里已经存在的其他 Pod 的特征来决定这个 Pod 应该跟谁待在一起、或者离谁远一点。举个例子帮你区分一下。假设你有一个订单服务和一个用户服务两者需要频繁通信。给订单服务加 nodeAffinity说“我要去带 rolecore 的节点”这是节点维度。给订单服务加 podAffinity说“我要去找运行着 user-service Pod 的节点”这是 Pod 维度。给订单服务加 podAntiAffinity说“不要让我的多个副本落在同一个可用区”这还是 Pod 维度但表达的是“不想靠近”的类型。理解这个维度差异很重要。因为它们的匹配对象完全不同用错维度会得出完全相反的部署效果。2. 亲和性字段拆解从 nodeSelector 到 podAntiAffinity2.1 nodeSelector 与 nodeAffinity 的取舍先说说最容易上手的 nodeSelector。它的写法就是把节点标签直接写在 Pod 里。spec: nodeSelector: disk-type: ssd只支持“等于”这一种匹配关系。你要排除某些标签没法做。要做“与”和“或”的组合没法做。nodeAffinity 是升级版用 matchExpressions 表达更复杂的条件。spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: disk-type operator: In values: - ssd - key: node-group operator: In values: - batch - gpu这段的意思是节点必须同时满足“disk-typessd”和“node-group 属于 batch 或 gpu”里面任意一个调度器才把 Pod 放上去。看到这一长串字段名大概有点压力。拆开看就没那么可怕requiredDuringSchedulingIgnoredDuringExecution硬性要求找不到匹配节点 Pod 就一直 Pending。preferredDuringSchedulingIgnoredDuringExecution软性偏好找不到匹配节点也可以调度但调度器会尽量满足。后面跟一个weight影响打分的权重。英文名字看着很绕翻译成大白话DuringScheduling 调度的时候。Execution 运行阶段。Required 必须满足。Preferred 加分项。IgnoredDuringExecution Pod 运行期间忽略该条件的变化。也就是说调度时严格检查运行后如果节点标签变了不会把已经运行中的 Pod 赶走。这个语义后面排查问题时会再次遇到先记着。2.2 podAffinity 与 podAntiAffinity基于 Pod 的拓扑决策节点亲和解决“Pod 去哪些节点”的问题。但有些场景的出发点是“另一个 Pod 在哪”。比如希望日志采集的 Pod 跟业务 Pod 尽量待在同一节点减少跨节点采集开销——用 podAffinity。希望同一个服务的多副本不要落在同一个节点或同一个可用区——用 podAntiAffinity。它们都依赖labelSelector来挑“参照 Pod”同时用一个topologyKey来定义“怎么算靠近”。spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: redis topologyKey: kubernetes.io/hostname这段表达调度时去找带有appredis标签的 Pod它所在的节点因为 topologyKey 是 hostname就是我倾向选的节点。podAntiAffinity 的写法几乎一样只是把podAffinity换成podAntiAffinity语义反过来。spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: order-service topologyKey: kubernetes.io/hostname这段表达不要把我调度到已经存在apporder-servicePod 的节点上。2.3 operator 与 topologyKey 的含义nodeAffinity 和 podAffinity 的 labelSelector 表达式里都支持一类操作符operator含义类比In值在给定列表内等于其中一个NotIn值不在给定列表内不等于任意一个Exists标签键存在不关心值你带着这个标签就行DoesNotExist标签键不存在你没有这个标签才行Gt / Lt值是数字且大于/小于某个值适用于数字型标签这些操作符跟 SQL 里 WHERE 子句的思路一样。直接查表就行。topologyKey更值得聊。它决定“同一拓扑域”怎么定义。常用的几个值kubernetes.io/hostname每个节点一个值代表物理机维度。topology.kubernetes.io/zone可用区维度。topology.kubernetes.io/region地域维度。如果反亲和用的是topologyKey: kubernetes.io/hostname那么只保证“同一个节点上不重复”。至于同一个可用区里有没有多副本它管不着。如果你想让副本跨可用区分散必须把 topologyKey 换成topology.kubernetes.io/zone。这个细节在排查“全挤在一台机器上”的问题时很关键我们在后面单独说。3. 三个高频场景的实操配置3.1 将特定 Pod 调度到 GPU 或高规格节点跑 AI 训练任务的人应该深有体会。集群里可能混着几台带 GPU 卡的高规格节点日常跑 Web 服务的 Pod 不该占用它们但也没有机制主动拦截。正确做法是给两类节点打标签再把工作负载用 nodeAffinity 指定过去。高规格节点如何打标签通常用下面命令kubectl label node gpu-node-1 acceleratornvidia然后给训练 Pod 加上硬性节点亲和apiVersion: apps/v1 kind: Deployment metadata: name: model-train spec: replicas: 2 selector: matchLabels: app: model-train template: metadata: labels: app: model-train spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: accelerator operator: Exists containers: - name: trainer image: training-image:latest resources: limits: nvidia.com/gpu: 1两个细节值得注意第一用Exists操作符而不是In带values语义是“只要节点有这个标签就行”不用管具体值。将来你新增 GPU 机型即使标签值不同也能生效少改一遍 YAML。第二labelSelector 的匹配对象是节点标签不是 Pod 标签。这一点别搞混了。很多新手把 nodeAffinity 写成 matchLabels appxxx结果一直调度不上去。如果只是优先调度不要硬性要求用 preferred 字段权重写 100affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 preference: matchExpressions: - key: accelerator operator: Exists两种字段可以同时存在。也就是尽量调度到 GPU 节点调剂到普通节点也行但必须有某块磁盘。实际配置中我习惯把硬性条件写成“必须匹配”再把不重要的倾向放进 preferred。3.2 让服务与依赖服务同节点部署微服务架构里网关服务和核心业务服务之间可能每个请求都在互相转发。如果这两个 Pod 落在不同节点每次调用都要跨节点走网络延迟和抖动都会上来。虽然 Service 会做负载均衡内部通信开销在流量高峰期会明显放大。用 podAffinity 把两个服务“绑”到同一节点apiVersion: apps/v1 kind: Deployment metadata: name: gateway spec: replicas: 2 selector: matchLabels: app: gateway template: metadata: labels: app: gateway spec: affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - order-service topologyKey: kubernetes.io/hostname containers: - name: gateway image: gateway-image:latest这里用的是 preferred 而不是 required。原因很现实required 的意思是“找不到 order-service 的节点就不调度”如果 order-service 发布期间所有副本短暂消失网关的新 Pod 会全部卡在 Pending。用 preferred 可以保证“能同节点最好不能同节点也别阻塞发布”。这个取舍线上环境一定要想清楚。还有一个反直觉的点podAffinity 匹配的对象是“已经存在的、符合标签条件的 Pod”。如果目标 Pod 也没调度出来这两个服务都会互相等待形成循环依赖。所以跨服务绑定时至少要保证目标服务有副本在集群里或者用软偏好兜底。顺带提一句如果只是想让调用关系密切的服务之间尽量近一点更省心的方案是使用topologySpreadConstraints把分布规则按拓扑维度展开。但它的控制粒度不同如果明确要求“同节点”还是 podAffinity 更精确。3.3 高可用服务用反亲和跨可用区分散这是我最想强调的场景。多副本部署不等于高可用三副本全挤一台机器一台宕机全挂。生产环境里这种事故还挺常见尤其是副本数比较少、节点标签区分又做得粗糙时调度器会把它们放到同一个可用区。要让同一服务的副本必须跨可用区配置如下apiVersion: apps/v1 kind: Deployment metadata: name: order-service spec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: order-service topologyKey: topology.kubernetes.io/zone containers: - name: order-service image: order-image:latest这段配置的关键是topologyKey: topology.kubernetes.io/zone。调度器会检查“已存在哪些带apporder-service标签的 Pod”把它们所在的可用区都标记出来新 Pod 不能落到这些可用区。也就是说三副本会被强制分布到三个不同可用区挂掉一个可用区也不影响另外两副本继续服务。如果你只想要保证同一节点上不出现多副本改topologyKey: kubernetes.io/hostname就行。两种粒度没有绝对对错取决于你的容忍度。比如数据库类服务要求强可用性通常可以配合 PDBPodDisruptionBudget来避免节点维护时出现断服。这点问题后面一起聊。4. 常见问题与排查技巧调度失败、反亲和失效怎么办4.1 Pod 一直 Pending怎么定位Task 调度不上第一反应先看事件kubectl describe pod pod-name -n namespace输出里最后几行会有 Event最常见的是Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 12s default-scheduler 0/4 nodes are available: 2 Insufficient cpu, 1 node(s) didnt match pod anti-affinity rules, 1 node(s) didnt match node selector.这里面信息量很大。0/4 nodes are available后面列出了每个节点被拒绝的原因链。先看是“资源不足”还是“亲和性不匹配”。如果是亲和性不匹配多半是两类问题节点上标签跟你期望的不一致。用kubectl get nodes --show-labels核对。反亲和把可调度的节点全堵死了。比如副本数等于可用节点数时所有节点上都已经有同样标签的 Pod新副本无处可去。反亲和堵死是特别容易踩的坑。假设集群只有 3 个节点副本数设为 3再用required反亲和按 hostname 打散。三个副本刚好各占一个节点看起来没问题。但如果其中某个节点挂掉状态异常的 Pod 被重新调度这时只剩 2 个可用节点反亲和又要求新 Pod 不能和剩下的副本坐同一节点——直接 Pending永远调度不出来。处理办法有两个一是给副本数留出至少一个节点的富余量二是用 preferred 反亲和而不是 required让调度器尽量打散万一没位置也能调度上去。4.2 反亲和策略为什么“好像没生效”这个场景我排查过很多次。YAML 里的 podAntiAffinity 写得没问题events 也显示调度成功但kubectl get pods -o wide一看两个副本就在同一个节点上。原因几乎都出在 topologyKey。举个具体例子。副本 A 在可用区 A 的节点 1 上。你写了反亲和要求topologyKey: kubernetes.io/hostname。调度器检查时发现可用区 B 的节点 2 和节点 1 的主机名不同符合条件于是放心调度。从函数上看反亲和的规则完好无损但你的目的可能是“跨可用区”而 hostname 这个拓扑级别管不到“可用区”这个层级。所以排查反亲和问题时第一步就是确认 topologyKey 是否跟你最终意图一致。想跨可用区分散就用topology.kubernetes.io/zone想跨节点分散就用kubernetes.io/hostname。两者不能混用。另外还要注意labelSelector 匹配的是 Pod 标签。反亲和需要参照“同类 Pod”所以 Deployment 模板里的 Pod 标签必须稳定存在。如果每次发布都是随机标签比如release: v1每次都改反亲和的匹配对象就跑偏了。正确做法是使用一个固定不变的标签比如app: order-service。4.3 topologyKey 选错的连锁反应选错拓扑域不只会导致“打散不彻底”还可能带来资源碎片化。我曾经把反亲和设成按topology.kubernetes.io/region打散结果一个可用区内的多个节点几乎没参与分配所有副本都被硬性分到不同地域。可用区 B 的负载夸张地高可用区 C 却空着。表面上看副本确实分散了但按地域打散的最低粒度太大3 个副本直接吃掉 3 个地域底下的节点分布严重不均匀。还有一次反着来hostname 打散配在了只有 2 个节点的环境中副本数却是 3。第三个 Pod 永远 Pending。这两个例子想说明的是**在选择拓扑域时先确认集群的节点规模、可用区数量以及副本数三者之间的关系。**副本数量一旦超过可用拓扑域数量硬性约束就会导致无法调度。如果不知道节点分布在哪些可用区可以先这样看kubectl get nodes -L topology.kubernetes.io/zone输出会列出每个节点属于哪个 zone。结合副本数再去决定用哪个 topologyKey心里就有底了。4.4 节点标签变化后Pod 不会自动迁移前面提过IgnoredDuringExecution这个后缀。它对应的行为是一旦 Pod 已经调度成功即使后来节点的标签改变或者不再满足亲和性条件运行中的 Pod 也不会被自动驱逐或重调度。这个设计在多数场景下是合理的。如果节点标签一变就把运行中的 Pod 全赶走一次配置变更可能引发大规模重启代价太高。但如果你的集群内部有脚本动态调整节点标签就需要注意它能让新 Pod 不去某些节点却不能让旧 Pod 从那些节点离开。想要让存量 Pod 按新策略重新分布需要主动重建kubectl rollout restart deployment/order-service -n namespace把 Deployment 滚动重建所有 Pod 重新走一遍调度新标签规则才会生效。另外一个实际经验是给节点打标签后不要指望所有未调度的 Pod 会自动“涌”向新标签节点。只有新创建或重建的 Pod 才会按新规则调度。如果你的环境里有一批 Pod 是长时间运行不重建的改动标签前要想好这一步的影响。4.5 排查工具组合速查根据我自己的经验排查亲和性问题时只需要三组命令基本就能定位九成问题。目的命令关注点查看节点标签是否满足要求kubectl get nodes --show-labels是否缺少 key / value 是否一致查看 Pod 调度事件kubectl describe pod pod-nameFailedScheduling 原因链查看 Pod 分布在哪个节点kubectl get pods -o wide -n namespace是否同节点 / 同可用区检查某节点上的 Pod 标签kubectl get pods -n namespace --field-selector spec.nodeNamenode与新调度的 Pod 是否匹配反亲和条件我习惯一套组合动作先跑完再下结论。先看节点再看事件最后对照 Pod 标签。直接在宿主机上用kubectl get nodes -L确认拓扑域别只凭想象。还有一个小技巧反亲和规则里加required时可以临时把它降级为preferred等集群状态稳定后再改回去。这在多副本重建时特别有用可以避免因节点不足导致 Pending 把服务卡死。5. 一些实操心得亲和一个不复杂的特性但要把它用顺还是有一些细节值得单独总结。一是标签体系最好提前规划。所有节点标签、Pod 标签统一约定。生产环境里我见过太多app、role、type混用的项目最后没人说得清哪个标签对应哪类节点。建议在一开始就维护一份标签清单至少包含节点用途如node-role、磁盘类型、可用区、以及服务归属app固定标签。不要一个场景一套标签后面排查会哭。二是从简单入手。先用 nodeSelector 判断是否能满足不行再用 nodeAffinity。Pod 亲和和反亲和字段较长嵌套层级深出问题时事件信息也比较难读。只要 resource 合理、topologyKey 明确大多数场景用 podAntiAffinity 配合 nodeAffinity 已经足够。三是权重和软硬性的取舍。必要用硬性required可选用软性preferred。硬性规则保证“必须这样”但会带来调度失败的风险。对于线上服务我一般把高可用相关设置为 required跨服务同节点优化设置为 preferred。四是结合 PodDisruptionBudget 一起用。反亲和保证副本分散在不同拓扑域但节点维护时如果同时停掉多个节点抗风险能力还是不够。给核心服务加上 PDB允许中断的副本数量要小于最小可用副本数。这样维护窗口就不会把所有副本一锅端。这条建议单独用没有意义但和反亲和组合之后高可用能力会明显提升。最后再提一个很隐蔽的点亲和性规则改变后必须重建 Pod 才会生效kubectl apply只更新 Deployment 模板不会自动迁移旧 Pod。我见过不止一个团队改了 affinity 配置后发现线上分布没变化最后才发现是忘了 rollout restart。这个动作虽然简单却是把“配置正确”变成“运行正确”的最后一步。