AI时代的Kubernetes教程36招-第18招-Label、Taint、Affinity和Topology Spread怎么把Pod调度到正确节点?

Kubernetes 调度从 Filter 到 Score:GPU 节点保护与跨 Zone 均匀分布

硬约束决定能不能落,软约束决定先落哪个。

[读者] 正在为 GPU 推理集群设计落点规则的 K8s 平台工程师
[痛点] ASR/TTS/Avatar/LLM 混部互相挤占,Gateway 副本挤在同一个 Zone
[现在读] 要把调度从能跑升级成按规则跑
[读完] 能写出 Filter 与 Score 视角的约束组合与验证方案
[旧方案] nodeSelector 打一个宽泛的 GPU label
    |
    v
[新需求] GPU 节点要受保护、多业务必须显式声明容忍、Gateway 跨 Zone 均匀
    |
    v
[冲突] label 只管可以被选中,管不住被抢;AntiAffinity 管不住域内失衡
    |
    v
[后果] 关键推理 Pod Pending,或全挤进一个 Zone,故障域被放大

我是老李,在一家做多模态推理的团队负责 Kubernetes 平台。

集群里跑着四类在线服务:ASR 语音识别、TTS 语音合成、Avatar 数字人、LLM 大模型推理,全都需要 GPU;另外还有一批只吃 CPU 的数据预处理和离线评测任务。

最早的做法很朴素:给 GPU 节点打一个宽泛的 label,业务 Deployment 写 nodeSelector,能跑起来就算成功。

变化来得很快。一次误配之后,CPU 侧的预处理任务爬满了 GPU 节点;同时 Gateway 这类无状态转发层扩容到十几个副本,被同一个 Zone 的节点一次性吃下。

为什么节点打了 label,别的业务还是能挤上来?

为什么 Pod 明明写了 toleration,却还是没落到想要的机型上?

为什么副本数够了,Gateway 还是全挤在一个 Zone?

01、故事

那天的现场是一个混部集群。GPU 节点分成两批:一批显存大,适合 LLM 和 Avatar;一批显存中等,适合 ASR 和 TTS。CPU 节点单独一组。

最初的上线方案只做了一件事:给所有 GPU 节点打上 hardware.example.com/gpu=true,业务 Deployment 写一行 nodeSelector。能跑,而且跑得挺好。

任务是:把每个业务的落点规则从「能跑」升级成「按规则跑」,并且让节点侧有能力拒绝不该上来的负载。

问题在于,nodeSelector 表达的是 Pod 的意愿,不是节点的边界。它只能让 Pod 说「我想去有 GPU label 的节点」,却不能阻止任何一个没写 GPU 意愿的 Pod 也落到这些节点上。

第二个变化来自 Gateway。它是一个无状态转发层,副本数从 4 提到 14,本意是扛住流量。结果这批 Pod 被同一个 Zone 的节点连续吃下,跨 Zone 分布这个假设,从来没有被任何一条规则保证过。

第三个变化来自新业务。Avatar 需要显存更大的机型,LLM 需要显存更大的同时还要求同机型批量一致。这时才发现,光靠一个 gpu=true 的 label,根本区分不出机型的层次。

到这里,问题从「调度能不能成功」变成了「调度规则能不能表达业务边界」。

落点先讲清
边界再收窄
混部不失序
集群才安稳

02、问题

旧方案失效的地方有三处。

第一处,GPU 节点没有拒绝能力。只有 label,没有 taint。label 表达的是「可以被选中」,taint 表达的才是「默认不被选中」。CPU 预处理任务按自己的规则找节点,自然就爬上来了。

第二处,业务侧只有硬选择,没有软偏好,也没有显式容忍。nodeSelector 一旦写错 key,Pod 直接 Pending,而 Pending 的原因藏在 Events 里,很容易被误判成资源不足。

第三处,Gateway 的均匀分布完全依赖调度器的默认打分。默认打分里有资源均衡插件,但它做的是节点之间的资源均衡,不是拓扑域之间的数量均匀。

业务影响是直接的:GPU 被非 GPU 负载占用之后,ASR 的响应抖动明显;Gateway 集中在一个 Zone,一旦该 Zone 的出口网络或节点池出问题,整个入口的可用性就跟着掉。

技术表现可以归纳成三类现象:Pod 长期 Pending;Pod Running 但机型不符合预期;Pod Running 且机型符合,但分布严重倾斜。

可验证的完成标准我定了四条,后面所有章节都围绕它们展开。

一,任何不带显式容忍的负载,都不能落在受保护的 GPU 节点上,这条要能用一次 kubectl describe pod 的 Events 证明。

二,任何一个 GPU 业务的 Pod,落点机型必须与声明一致,滚动更新期间也一致。

三,Gateway 的副本在可用 Zone 之间的数量差不超过设定值,越界时表现为 Pending 而不是静默倾斜。

四,所有规则都写在 deploy/gpu/scheduling.yaml 里,可评审、可 diff、可回滚。

失效有现象
现象有证据
证据定标准
标准才可验

03、原理

先立骨架。调度器不是一步决定 Pod 去哪,而是一条流水线:Filter、Score、Reserve、Permit、PreBind、Bind。

Filter 阶段是一组过滤插件串行执行,任何一个返回不可行,这个节点就出局。参与过滤的包括 NodeUnschedulable、TaintToleration、NodeAffinity、NodeName、NodePorts、NodeResourcesFit、VolumeBinding、InterPodAffinity、PodTopologySpread 等。

Score 阶段在剩下的这批节点上打分,每个插件给出 0 到 100 的分数,按插件权重归一化后加权求和,得分最高的节点进入下一阶段。

反直觉判断:Filter 和 Score 不是两次投票,而是集合裁剪加集合排序。Filter 负责把可行集合缩小,Score 只在这个集合内部排序。一个节点一旦被 Filter 淘汰,你给 preferred 配多高的权重,都不会让它重新回到候选里。

这条判断直接解释了后面两个故障。第一个故障里 Pod 一直 Pending,是因为可行集合被 taint 过滤器裁成了空集,Score 根本没有机会介入。第二个故障里 Gateway 分布倾斜,是因为可行集合本身就被 nodeSelector 收窄了,skew 只是在这个被收窄的集合上「看起来均匀」。

第二个反直觉判断:Taint 和 nodeAffinity 不是替代关系,也不是或的关系。Taint 是节点侧拒绝,nodeAffinity 是 Pod 侧选择,两者在 Filter 阶段是逻辑与。给 GPU 节点打上 taint 之后,一个写了 nodeAffinity 却没写 toleration 的 Pod,依然上不去。

第三个要点在 TopologySpreadConstraints。它的核心参数是 maxSkew、topologyKey、whenUnsatisfiable 和 labelSelector。maxSkew 表示在同一个拓扑域上,匹配 labelSelector 的 Pod 数量之间允许的最大差值。whenUnsatisfiable 取 DoNotSchedule 时是硬约束,取 ScheduleAnyway 时只是软偏好。

关键在于:参与 skew 计算的域,是那些通过了 nodeAffinity、nodeSelector 等约束的节点所在的域。约束收窄了节点池,就同时收窄了 skew 的计算范围。官方文档为这一点提供了 nodeAffinityPolicy 和 nodeTaintsPolicy 等参数来控制,具体取值与默认行为需按当前官方文档核验。

第四个要点是权重。Score 阶段各插件都带有权重,NodeResourcesFit、NodeResourcesBalancedAllocation、ImageLocality、InterPodAffinity、NodeAffinity、PodTopologySpread、TaintToleration 都会参与打分,具体默认值随版本调整,需按当前官方文档核验。改权重是集群级动作,不要拿它当单个业务的路由规则用。

还有一条工程经验值得提。官方文档明确提到,Pod 间亲和与反亲和需要大量计算,在大规模集群中会显著拖慢调度,不推荐在数百节点以上的集群使用。所以 Gateway 的均匀分布,用 TopologySpreadConstraints 比用 podAntiAffinity 更划算。

过滤定可行
打分定高低
截断在集合
排序在集合

04、架构

[输入] 待调度 Pod + 节点池标签与污点
    |
    v
[模块] Filter 插件链(TaintToleration / NodeAffinity / NodeResourcesFit / PodTopologySpread)
    |
    v
[数据/状态] 可行节点集合 + 各节点的 topology 域归属
    |
    v
[处理] Score 插件链加权求和,选出最优节点
    |
    v
[输出] 绑定结果 + 实际落点与域分布

这张图里有四条边界要说明。

第一条边界是节点侧与 Pod 侧的分工。节点的 label 和 taint 是地契,Pod 的 nodeAffinity 和 toleration 是通行证。地契负责定义资源池和准入,通行证负责声明意图。

第二条边界是硬约束与软约束的分工。required 系列进 Filter,preferred 系列进 Score。想让一条规则决定「能不能落」,就用 required;只想影响「落在哪更好」,就用 preferred。

第三条边界是资源均衡与拓扑均衡的分工。前者由 NodeResourcesFit 和 NodeResourcesBalancedAllocation 负责,它是节点维度的;后者由 PodTopologySpread 负责,它是域维度的。两者不互相替代。

第四条边界是顶层规则的归属。所有约束集中写在 deploy/gpu/scheduling.yaml,通过 Kustomize 或 Helm 分发到各业务,避免每个业务各写一份、各不一致。

收益是可预期的:GPU 节点不被非 GPU 负载侵占;业务落点可解释,出问题能直接定位到哪条规则;跨 Zone 分布从运气变成规则。

代价同样明确:硬约束变多之后,可行集合变小,集群碎片化更严重,Pending 的概率上升;TopologySpreadConstraints 在 whenUnsatisfiable 取 DoNotSchedule 时,会为了 skew 放弃资源更优的节点,可能出现资源利用率下降。

适用条件:节点池有清晰的机型分层;在线业务规模稳定、副本数可预测;有一份统一的调度配置文件,而不是散落在几十个 Deployment 里。如果节点池本身同构、业务之间又没有隔离诉求,那这套规则带来的就只有复杂度。

地契分资源
证件说意图
硬软各归位
域内才算均

05、实战一次

环境与工具:一个 Kubernetes 集群(版本需按当前官方文档核验),kubectl 已配置好上下文,集群里至少两个 Zone,且 GPU 节点已按机型分组。以下内容未在当前环境实测,以下为预期结果。

依赖:不需要额外组件,使用默认调度器与默认插件集即可。所有清单统一放在 deploy/gpu/scheduling.yaml。

第一步,给 GPU 节点打受保护的 label 与 taint。

# 机型标签:区分显存层次
kubectl label node gpu-a10-01 hardware.example.com/gpu=true
kubectl label node gpu-a10-01 hardware.example.com/gpu-model=a10

# 节点侧拒绝:默认不让任何 Pod 上来
kubectl taint node gpu-a10-01 sku=gpu:NoSchedule

第二步,业务侧写显式容忍与硬亲和。以 ASR 为例,片段如下。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: asr-online
  namespace: inference
spec:
  replicas: 4
  selector:
    matchLabels:
      app: asr-online
  template:
    metadata:
      labels:
        app: asr-online
    spec:
      tolerations:
        - key: sku
          operator: Equal
          value: gpu
          effect: NoSchedule
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: hardware.example.com/gpu-model
                    operator: In
                    values:
                      - a10
      containers:
        - name: asr
          image: registry.example.com/asr:1.0.0
          resources:
            limits:
              nvidia.com/gpu: 1

这里注意两点。toleration 的 operator 用 Equal 时要同时给出 key 和 value;nodeAffinity 用 required 而不是 preferred,因为机型是硬要求。

第三步,Gateway 跨 Zone 均匀分布。

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: gateway

第四步,启动与验证。

kubectl apply -f deploy/gpu/scheduling.yaml
kubectl get pods -n inference -o wide
kubectl describe pod -n inference -l app=asr-online

示例输出(节选):

NAME                          READY   STATUS    NODE
asr-online-6c9d7f8b4-2xk9p    1/1     Running   gpu-a10-01
gateway-7f4c9d6b8-qq2mv       1/1     Running   node-zone-b-03
gateway-7f4c9d6b8-jj7lt       1/1     Running   node-zone-c-01

验证三个点:不带 toleration 的 Pod 上不了 GPU 节点,这一步可以用一个临时 Deployment 反证;ASR 的 Pod 落点机型与声明一致;Gateway 的副本在各个可用 Zone 上的数量差不超过 1。

V1 到这里跑通。留下的问题是:滚动更新期间 Gateway 的 skew 会不会被新旧两版 Pod 互相计数而打破?跨 Zone 均匀之后,Pod 是否会集中到某些性能更差的节点上?这些留到第 07 章处理。

先跑通一条
再验证一条
规则有实证
才算落了地

06、排查

排查一:ASR 的 Pod 一直 Pending

现象:ASR 上线后,四个副本里有两个长时间处于 Pending。

怀疑:GPU 资源不够,节点上没有空闲的 nvidia.com/gpu。

检查:

kubectl describe pod -n inference asr-online-6c9d7f8b4-2xk9p

证据:Events 里出现节点过滤失败的信息,形如 0/12 nodes are available: 8 node(s) had untolerated taint {sku: gpu}, 4 node(s) didn't match Pod's node affinity。这类判决是 Filter 阶段直接给出的。

根因:toleration 写成了 effect: NoExecute,而节点上的 taint 是 NoSchedule。toleration 的 effect 必须与 taint 的 effect 对应,或者省略 effect 表示匹配全部。

错误尝试: 先怀疑是资源不足,去调大 requests,或者申请加节点。为什么错:Pending 的根因写在 Events 里,Filter 阶段失败会明确给出 untolerated taint 或 node affinity 之类的判决语句,和资源不足给出的判决语句完全不同。看到 Pending 就去要节点,是把一个配置问题当成容量问题,加再多节点也不会让这个 Pod 上来。

修复:把 toleration 的 effect 改成 NoSchedule,重新滚动。

排查二:Gateway 副本分布严重倾斜

现象:Gateway 扩到 14 个副本后,10 个落在 zone-a,zone-b 只有 4 个,zone-c 一个都没有。

怀疑:调度器随机性太大,或者某个 Zone 的节点被打上了不可调度标记。

检查:

kubectl get pods -n ingress -l app=gateway -o wide
kubectl get nodes -L topology.kubernetes.io/zone
kubectl get deploy gateway -n ingress -o yaml

证据:Deployment 里根本没有 topologySpreadConstraints 字段;节点侧没有不可调度标记;默认打分插件只做资源维度的均衡。

根因:默认打分保证的是节点间资源均衡,不是拓扑域间数量均匀。副本数够,从来不等于分布均匀。

修复:给 Gateway 加上以 topology.kubernetes.io/zone 为 topologyKey 的 TopologySpreadConstraints。

排查三:LLM 的 Pod 落到小显存机型

现象:LLM 业务声明需要大显存机型,实际有 Pod 落在 A10 节点上,运行几分钟后 OOM 重启。

怀疑:节点的 label 打错了。

检查:

kubectl get pod -n inference -l app=llm -o yaml
kubectl get nodes -l hardware.example.com/gpu-model=a10

证据:Pod 的 nodeAffinity 用的是 preferredDuringSchedulingIgnoredDuringExecution,而不是 required。

根因:软约束只参与 Score,不参与 Filter。当大显存节点在资源维度评分偏低时,调度器会把这个 Pod 放到 A10 节点上。

修复:机型要求改成 requiredDuringSchedulingIgnoredDuringExecution。

现象要落到
证据要指向
根因要唯一
修复要收敛

07、优化

三个排查给出的根因分别是:toleration 的 effect 不匹配、缺少拓扑均衡约束、软硬约束用错位置。V2 就按这三条改。

第一处修改,统一 toleration 的写法。所有 GPU 业务使用同一段容忍声明,effect 显式写 NoSchedule。

tolerations:
  - key: sku
    operator: Equal
    value: gpu
    effect: NoSchedule

原因是 effect 省略虽然也能匹配,但省略之后语义变模糊,评审时看不出意图。显式写出来,规则才可读。新行为是:任何漏写 effect 的变体,在清单校验阶段就会被拒绝合并。

第二处修改,Gateway 的拓扑约束从无到有,并补上几个关键字段。

topologySpreadConstraints:
  - maxSkew: 1
    minDomains: 3
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    nodeAffinityPolicy: Honor
    nodeTaintsPolicy: Honor
    labelSelector:
      matchLabels:
        app: gateway
    matchLabelKeys:
      - pod-template-hash

minDomains 用来声明最少需要几个域参与,避免只有两个 Zone 可用时把 skew 也算成满足。nodeAffinityPolicy 和 nodeTaintsPolicy 取 Honor,表示只统计真正可调度的节点,避免因为节点被污染而误算 skew。matchLabelKeys 用于滚动更新场景,让新旧两版 ReplicaSet 的 Pod 分别计算 skew,而不是互相计数。这几个字段的可用性与默认值需按当前官方文档核验。

第三处修改,机型要求一律升级为 required,并且把机型约束收敛到一处维护。

nodeAffinity:
  requiredDuringSchedulingIgnoredDuringExecution:
    nodeSelectorTerms:
      - matchExpressions:
          - key: hardware.example.com/gpu-model
            operator: In
            values:
              - a800

验证同样要落到命令上。Gateway 的分布可以直接看:

kubectl get pods -n ingress -l app=gateway -o wide
kubectl get nodes -L topology.kubernetes.io/zone

示例输出:

NAME                       READY   STATUS    NODE
gateway-7f4c9d6b8-2xj4p    1/1     Running   node-zone-a-01
gateway-7f4c9d6b8-8dk2m    1/1     Running   node-zone-b-02
gateway-7f4c9d6b8-b7vq9    1/1     Running   node-zone-c-03

把节点名映射到 Zone 之后统计数量,最大差值不超过 1,就符合 maxSkew。分布不均时,多出来的那个 Pod 会处于 Pending,Events 给出与 maxSkew 相关的判决,而不是静默倾斜。

需要说清的代价:whenUnsatisfiable 取 DoNotSchedule 后,Gateway 的副本数如果超过可用域的承载能力,多余副本会一直 Pending。这是有意的行为——宁可 Pending 并告警,也不要静默地把可用性押在一个 Zone 上。

改一处根因
补一条验证
代价要写明
收益才可信

08、演进

[同一输入] 一个 Gateway Deployment,14 个副本
    |
    +--[V1] 无拓扑约束,靠默认打分 / 代价:副本集中,故障域放大
    |
    +--[V2] Zone 级 TopologySpreadConstraints + minDomains + matchLabelKeys / 代价:可能 Pending
    |
[Trade-off]
得到一个可声明的分布保证;失去一部分资源最优性;适用于多 Zone 且副本数可预测的无状态层

把 V1 和 V2 放在一起看。

正确性上,V1 的分布是概率性的,V2 的分布是约束性的。V1 在多数时候看起来也均匀,但它均匀的原因是默认打分插件恰好把资源摊平了,一旦节点资源不齐,倾斜立刻出现。

稳定性上,V1 在节点故障时会自然漂移,不需要人工干预;V2 在域不可用时,多余副本会 Pending,需要人来判断是接受更低副本数,还是临时放宽 whenUnsatisfiable。

复杂度上,V1 零配置;V2 需要理解 maxSkew、minDomains、nodeAffinityPolicy、nodeTaintsPolicy 和 matchLabelKeys 之间的交互。这些参数的可用性随版本变化,需按当前官方文档核验,升级集群时要单独回归。

成本上,V2 会牺牲一部分装箱效率。为了维持跨 Zone 的 skew,调度器可能放弃资源利用率更高的节点,选择域上更均衡的节点。在 GPU 这种稀缺资源上,这个代价要单独评估——GPU 业务通常用机型亲和加反亲和,而不是用跨域均匀。

适用范围上,V2 的拓扑约束适合 Gateway、接入层、无状态 API 这类副本等价、域故障要扛住的服务;不适合单副本的有状态服务,也不适合 GPU 推理这种资源绑定极强的服务。

遗留问题有两个。第一,跨 Zone 均匀不等价于跨 Zone 等价:Zone 之间的网络时延和带宽可能不同,均匀分布会让一部分请求走更远的链路,这要靠流量层解决,不是调度层能解决的。第二,nodeAffinityPolicy 与 nodeTaintsPolicy 在不支持这些字段的版本上会被忽略,行为退化回默认,需按当前官方文档核验并做版本门禁。

旧法靠概率
新法靠约束
得到是确定
失去是灵活

09、洞见

9.1 硬约束是集合运算,软约束是排序运算

这条来自第 03 章和第 06 章的第三条排查链。required 系列进 Filter,决定可行集合;preferred 系列进 Score,只在可行集合里排序。

反直觉判断:把一条规则从 preferred 改成 required,得到的不是更强的偏好,而是不同的阶段。它会从排序依据变成存在性条件,副作用是可行集合可能直接变成空集,Pod 从落得不好变成根本落不下去。

所以在改硬软属性之前,必须先确认候选节点集合至少覆盖两个以上的域,否则就是把业务压成单点。

9.2 节点的保护与 Pod 的声明必须成对出现

这条来自 GPU 节点的 taint 与业务的 toleration。只打 taint 不加 toleration,业务全部 Pending;只加 toleration 不打 taint,保护形同虚设。

反直觉判断:taint 不是比 label 更高级的路由方式,它是相反方向的一条规则。label 让 Pod 主动选择节点,taint 让节点主动拒绝 Pod。两者不是强弱关系,是方向关系,缺任何一边,规则都不闭合。

工程上的落法是:把成对的 label、taint 与业务侧的 toleration、nodeAffinity 写进同一份清单文件,评审时一起看,不允许拆开维护。

9.3 均匀分布是相对的,域是被约束收窄过的

这条来自第 03 章对 skew 计算范围的判断。TopologySpreadConstraints 的每个域是否参与计算,取决于该域内是否存在通过 Filter 的节点。

反直觉判断:你以为自己在做全局均匀,实际做的可能只是两个 Zone 之间的均匀。给 Deployment 加了 nodeSelector 或 nodeAffinity 之后,可用域会同步收缩,skew 也只在这些域内计算,报表上依然显示差值为 1。

要验证这一点,不能只看 Pod 分布,还要看节点 label 的分布和实际可用域的数量。

9.4 调度是声明式的,验证必须是可观测的

调度器不会因为你在文档里写了规则就按规则执行,它只执行它读到的字段。所有约束必须落到对象上,所有验证必须落到命令上。

这条判断的价值在于:它把「我们约定 GPU 业务都加 toleration」这类口头规范,转换成「清单校验不通过就拒绝合并」的流水线规则。规范只有变成校验,才具备约束力。

软硬非强弱
方向才是界
域随约束缩
校验才成规

10、系统落地

原来有什么:一份宽泛的 GPU label,加上散落在各业务 Deployment 里的 nodeSelector;节点侧没有任何拒绝能力;跨 Zone 分布完全靠默认打分。

本篇新增了什么:节点侧的一套 label 与 taint 分层,业务侧统一的 toleration 与 required nodeAffinity 模板,Gateway 的 Zone 级 TopologySpreadConstraints,以及一份可评审的统一清单 deploy/gpu/scheduling.yaml。

现在能做什么:能让非 GPU 负载无法落到受保护的 GPU 节点上,并且这件事有 Events 作为证据;能让每个业务的落点机型与声明一致;能让 Gateway 的跨 Zone 分布从不变量变成被约束的可验证项。

还缺什么:缺少清单校验门禁,规则现在靠人遵守,还没有靠流水线强制执行;缺少对 Pending 的分类告警,目前 Pending 只能靠人去看 Events,无法区分是容量不足还是约束写错;缺少域可用性的观测,当某个 Zone 整体不可用时,Gateway 的多余副本 Pending 不会被自动识别。

下一步如何演进:第一步,把 toleration 与 nodeAffinity 的必填项做成准入校验,让不合规的清单进不了集群;第二步,把与 taint、skew 相关的 Pending 判决抽成指标和告警,让约束写错和资源不足在告警层面就能分开;第三步,再考虑跨 Zone 的流量亲和与调度约束是否需要对齐,这一步涉及流量层,不在本次范围内。

原先靠约定
现在靠清单
先补上校验
再谈自动化

11、小结

Q1 → label 只表达可以被选中,taint 才表达默认拒绝;保护必须由节点侧的 taint 与 Pod 侧的 toleration 成对完成
Q2 → toleration 与 nodeAffinity 分别由 Filter 的两个不同插件校验,机型选择还用 preferred 就只会影响排序,不影响可行性
Q3 → 副本数只保证总量,不保证分布;跨 Zone 均匀必须显式声明 TopologySpreadConstraints,且 skew 只在通过 Filter 的域内计算
状态 → 已能给 GPU 节点做分层保护,让 ASR/TTS/Avatar/LLM 显式容忍,并让 Gateway 具备可验证的跨 Zone 分布约束

一图定分流
一证辨对错
一式接混部
一役见章法

12、作业

12.1 理解题:为什么给 GPU 节点打了 label 之后,仍然需要打 taint?

参考答案:label 只提供节点侧的可被选择属性,它不会阻止任何 Pod 调度上去。调度器在 Filter 阶段并不会因为节点带 GPU label 就拒绝 CPU 负载。只有 taint 才会在 TaintToleration 插件里把不容忍的 Pod 过滤掉。因此 label 用来表达节点属于哪类资源池,taint 用来表达节点默认不被准入,两者方向不同,缺一不可。

12.2 实战题:给一个两 Zone 集群中的 Gateway Deployment 加上跨 Zone 均匀分布,并验证分布数量。

参考答案:给 Pod 模板加 topologySpreadConstraints,topologyKey 取 topology.kubernetes.io/zone,maxSkew 取 1,whenUnsatisfiable 取 DoNotSchedule,labelSelector 匹配 app: gateway,并根据集群版本决定是否加 minDomains、nodeAffinityPolicy、nodeTaintsPolicy 与 matchLabelKeys。验证时用 kubectl get pods 输出每个 Pod 所在节点,把节点映射到 Zone 后统计数量,观察最大差值是否不超过 maxSkew。如果副本数不足以覆盖所有 Zone,需要先确认 minDomains 的设置是否与实际可用域数量一致。

12.3 排障题:某个 GPU 业务的 Pod 一直 Pending,Events 显示节点存在 untolerated taint,如何定位并修复?

参考答案:先确认节点上 taint 的 key、value 与 effect,再对照 Pod 的 tolerations 逐字段比较。重点检查 effect 是否写成了 NoExecute 或 PreferNoSchedule,而节点的 taint 是 NoSchedule;也要检查 operator 是否为 Equal 以及 value 是否拼写一致。最容易忽略的是 effect 字段,省略 effect 表示匹配所有 effect,写错 effect 则完全不匹配。修复方式是把 toleration 的 key、value、effect 与节点 taint 对齐,然后重新滚动。修复后应再用一次 describe 确认 Events 里的判决变成以可调度节点计数开头。

12.4 架构判断题:把 Gateway 的跨 Zone 均匀分布改成用 podAntiAffinity 实现,是否更合适?

参考答案:通常不合适。podAntiAffinity 表达的是不要和某类 Pod 落在同一个拓扑域,它在语义上只能逼近均匀,无法声明最大差值;而且要维持均匀,需要给每个副本一个独立的匹配标签,配置复杂度高。更重要的是,官方文档明确指出 Pod 间亲和与反亲和需要大量计算,在大规模集群中会显著拖慢调度,不推荐在数百节点以上的集群使用。TopologySpreadConstraints 直接声明 maxSkew,语义与目标一致,计算开销也更可控。只有在需要表达两个业务不能同域这类互斥语义时,才使用 podAntiAffinity。

一题辨方向
一题练声明
一题走证据
一题定取舍

13、思考

回到最初的那个冲突:节点侧只打了一个 label,Pod 侧只写了一行 nodeSelector,两边都以为自己表达了边界,实际上谁也没有表达边界。

Filter 到 Score 这条流水线给出的最大启发是:调度规则要按集合裁剪和集合排序两件事来分工。凡是决定能不能落的,交给 required 与 taint;凡是决定落在哪更好的,交给 preferred 与打分权重。混在一边,就会出现改了一个字段、从落得不好变成根本落不下去的情况。

第二点是保护机制必须成对。节点的 label 与 taint,业务的 nodeAffinity 与 toleration,是两组必须同时存在的声明。任何一组缺一边,规则都不闭合,而且失效方式很隐蔽——不是报错,是静默地落到错误的节点上。

第三点是均匀分布必须显式声明。默认打分做的是资源维度的均衡,不是拓扑维度的均衡。副本数够,从来不等于故障域扛得住。

最后一点是规则必须可验证。能让一个约束真正生效的,不是文档里的约定,而是命令行的输出和流水线里的门禁。

边界要说清
成对才算数
声明显式写
验证落到实

posted @ 2026-10-12 10:14  李福春  阅读(2)  评论(0)    收藏  举报