最近看一个服务的调度配置,发现它同时用了 nodeSelector、nodeAffinity 和 podAntiAffinity。表面上看是“让服务跑到指定节点池,并尽量分散”,但真正排查 Pending 的时候,发现这些规则叠在一起以后不太直观。
这篇简单记录一下我自己看调度规则时的判断顺序。
我一开始看错的地方是:以为 preferred 写了就一定会分散,后来才意识到它只是调度器打分时的偏好。只要资源、污点、PVC 或其他硬条件把节点集合压小,软亲和性很容易被牺牲。
先看节点标签
亲和性本质上还是围绕 label 做匹配。排查前先看节点上到底有没有这些 label:
如果 YAML 里写了 topology.kubernetes.io/zone,但节点上根本没有这个 label,后面分析半天都是偏的。
nodeSelector 和 nodeAffinity
nodeSelector 比较直接,只能做简单的等值匹配:
nodeAffinity 更灵活,可以写 In、NotIn、Exists、DoesNotExist、Gt、Lt 这些操作符,也可以区分硬约束和软约束。
| |
requiredDuringSchedulingIgnoredDuringExecution 是硬条件,不满足就不会调度。preferredDuringSchedulingIgnoredDuringExecution 是软条件,调度器会尽量满足,但不满足也可以调度。
IgnoredDuringExecution 的意思是:Pod 已经调度成功后,如果节点 label 后面变了,Kubernetes 不会因为这个亲和性规则把 Pod 赶走。
还有一个容易忽略的点:如果同时写了 nodeSelector 和 nodeAffinity,它们都必须满足。不是二选一。
多个 nodeSelectorTerms 之间是 OR,只要满足其中一个 term 就可以。同一个 term 里的多个 matchExpressions 是 AND,必须全部满足。
我通常会把它翻译成一行布尔表达式再看:
这么写出来以后,很多 Pending 就不神秘了。不是 Kubernetes 调度器“抽风”,而是自己把可用节点交集写没了。
podAffinity 和 podAntiAffinity
podAffinity 不是看节点 label,而是看“某个拓扑域里有没有符合 label 的 Pod”。
比如希望业务 Pod 尽量跟缓存 Pod 在同一个可用区,减少跨可用区访问:
这个规则要小心。如果 redis 本身还没起来,或者 redis 不在当前 namespace,新的业务 Pod 就可能 Pending。podAffinity 默认只匹配当前 namespace,如果要跨 namespace,需要显式写 namespaces 或 namespaceSelector。
更常见的是 podAntiAffinity,比如同一个 Deployment 的多个副本尽量不要落在同一个节点:
这里用的是 preferred,所以只是尽量分散。如果节点资源紧张,多个 Pod 仍然可能被放到同一个节点。
这个配置看起来短,但里面有几个字段不能只按字面理解。
weight: 100 只在 preferredDuringSchedulingIgnoredDuringExecution 里出现,范围是 1 到 100。它不是“强制程度”,而是调度器打分时这个偏好的权重。多个 preferred 规则同时存在时,调度器会把满足偏好的节点加分,最后选择综合分数更高的节点。也就是说,weight: 100 仍然是软偏好,不会让 Pod Pending。
podAffinityTerm 才是真正描述“跟谁保持距离”的规则。它至少要看三个东西:
labelSelector:选择已有 Pod。topologyKey:定义“同一个拓扑域”。namespaces/namespaceSelector:决定跨不跨 namespace 查已有 Pod。
topologyKey: kubernetes.io/hostname 的意思不是“按节点名匹配 Pod label”,而是看节点上这个 label 的值。通常每个节点的 kubernetes.io/hostname 不同,所以效果就是“尽量不要放到同一个节点”。如果换成 topology.kubernetes.io/zone,含义就变成“尽量不要放到同一个可用区”。
这个差别很大:
如果只是想让副本分散到不同节点,用 hostname 就够了。如果想跨可用区做容灾,要用 zone,但这时候更推荐一起评估 topologySpreadConstraints,因为它表达“分布倾斜度”会更直观。
如果要强制分散,可以写成 required:
如果集群只有 2 个可用节点,但 Deployment 要 3 个副本,第 3 个 Pod 很可能 Pending。
大集群里还要注意性能。官方文档提醒 inter-pod affinity/anti-affinity 在大规模集群中可能拖慢调度,因为调度器需要跨节点检查已有 Pod 的标签和拓扑域。简单说,节点和 Pod 越多,这类规则越不便宜。能用 topologySpreadConstraints 表达“均匀分布”的场景,我会优先考虑它。
还有一个准入层面的细节:对于 requiredDuringSchedulingIgnoredDuringExecution 的 Pod anti-affinity,Kubernetes 默认的 LimitPodHardAntiAffinityTopology 准入控制器会把 topologyKey 限制为 kubernetes.io/hostname。也就是说,hard anti-affinity 想直接用 zone 做强约束,可能会被准入策略拦住。这个限制是为了避免过重、过宽的硬反亲和规则影响调度。
podAntiAffinity 里容易漏掉的配置
上面的例子只用了 matchLabels,实际项目里经常不够。
matchExpressions:不要只会 matchLabels
如果同一个服务有多个组件,不想跟某几类组件混在一起,可以用 matchExpressions:
| |
这个规则的意思是:调度当前 Pod 时,尽量避开已经运行了 app=api-server 且 component 不是 canary 的节点。
matchLabels 和 matchExpressions 是 AND 关系,不是 OR。这个点很容易写错。如果你既写了:
那它匹配的是 app=api-server AND component in (stable)。
namespaces / namespaceSelector:默认只看当前 namespace
podAffinityTerm 默认只匹配当前 Pod 所在 namespace 里的 Pod。这个默认行为有时候会让人误判。
比如你有两个 namespace:
如果你希望两个 namespace 的 api-server 也不要落到同一个节点,必须显式指定 namespace:
如果 namespace 很多,用 namespaceSelector 更合适。比如只匹配带有 tenant=prod 标签的 namespace:
这里要先给 namespace 打标签:
如果 namespaces 和 namespaceSelector 同时写,匹配范围是两者的并集。都不写时,就是当前 Pod 的 namespace。
matchLabelKeys:跟当前 Pod 的标签值绑定
Kubernetes 还支持 matchLabelKeys。它会从“当前正在调度的 Pod”上取指定 key 的 value,再和 labelSelector 合并。
这个字段适合 Deployment 滚动发布时使用,避免新旧 ReplicaSet 的 Pod 互相干扰。比如不同 revision 的 Pod 都有 pod-template-hash:
含义是:不仅要匹配 app=api-server,还要拿当前 Pod 的 pod-template-hash 值去匹配已有 Pod。这样调度器更关注同一个 rollout revision 的 Pod 分布,而不是把历史 revision 也算进去。
这个配置比简单的 app=api-server 更细,尤其适合滚动发布时避免新旧版本互相影响调度判断。
mismatchLabelKeys:租户隔离时更有用
mismatchLabelKeys 是另一个容易忽略的字段。它会从当前 Pod 标签里取 key 的值,然后匹配“这个 key 的值不相同”的已有 Pod。
一个典型场景是多租户隔离:不要把不同 tenant 的 Pod 放到同一个节点池或同一个节点。
假设当前 Pod 是 tenant=team-a,这个规则会让它排斥同一 hostname 上已有的 tenant!=team-a 的 Pod。
这个字段不要随便用在经常变的 label 上。官方文档也提醒,不要对调度后还可能直接修改的 label 使用这类动态匹配字段,否则 kubelet 不会因为 label 后续变化而重新触发调度。
一个更完整的 Deployment 例子
如果我要给一个生产 API 写“尽量分散,但不要因为节点不足卡死”的配置,会更像这样:
| |
这个配置表达的是:
- 优先不要和其他
api-server放在同一节点。 - 跨
tenant=prod的 namespace 一起考虑。 - 次一级偏好是不要都堆在同一个 zone。
- 因为都是 preferred,所以资源紧张时不会导致 Pod Pending。
如果业务真的要求“一台节点只能跑一个副本”,再把第一条改成 required。但我一般不会一上来就这么做,除非副本数、节点数、扩容策略都已经算清楚。
排查时先看 Events
如果调度失败,不要先猜,直接 describe:
| |
重点看最后的 Events,常见会看到类似:
然后再反查:
亲和性配置本身不复杂,复杂的是这些规则会跟资源、污点、PVC、拓扑分布一起叠加。我的习惯是先确认硬约束,再看软约束,不要一上来就改 replicas 或重启。
最后还有一个现实建议:不要为了“看起来高可用”把所有规则都写成 required。亲和性配置的目的应该是表达业务边界,不是把调度器绑死。比如核心服务跨节点分散可以硬一点,普通后台任务用 preferred 往往更合适。