亲和性是放置匹配:可以"偏好"某个标签也可以"强制"它,再用反亲和把副本散开,换取更强的韧性。硬件从来不会长期均匀——这边一池 GPU,那边高速本地 NVMe,还有合规数据的隔离区——而工作负载悄悄依赖落到对的地方。没有显式规则,调度器只按空闲容量铺,需要 GPU 的 Pod 落到没有 GPU 的节点,在运行时而不是调度时才失败。

亲和把这次失败前移到它该在的位置。 placements 写成节点上的数据(标签)配 Pod 上的规则,可 review 的配置代替了"哪些节点是特殊的"部落知识。读 manifest 的人一眼看出工作负载必须跑在哪、为什么。

按标签匹配节点

亲和性规则读取节点标签,让你说明某个 Pod 应该、甚至必须落在带某个标签的节点上。把稳定的标签一次性打在节点上,剩下的就让调度器去匹配。accelerator=nvidia-a100、storage=nvme、zone=regulated 这类标签描述机器的持久属性;因为它们住在 node 对象上,换机器也不影响引用它的 Pod 规则——新硬件打同样的标签,老规则继续生效。

标签名要像 API 字段一样认真起,因为改一个被广泛引用的标签名,就是跨所有匹配它的 manifest 的迁移。约定小而有文档的词表,胜过每队自创 key:workload-class、hardware、compliance 覆盖大多数家底,再配半页约定文档,免得 GPU 标签拼出五种写法。像校验镜像 tag 一样在 CI 里校验标签——要求一个没有任何节点携带的标签的 Pod,是该死在流水线里的拼写错误,而不是凌晨三点的发布。

偏好还是强制

偏好型规则尽力而行,条件允许就把负载摊开;强制型规则则硬性拒绝那些满足不了标签的 Pod。只有放置真地有讲究才用强制,否则偏好能留给调度器更多弹性。成本模型很直白:强制拿可用性换正确——没有匹配节点有位子,Pod 就 Pending,也不去错的地方;偏好拿正确换可用——落到次优但活着的地方。

大多数家底想要的是:少数硬约束用强制(有 GPU、合规区、架构),软诉求全用偏好(跨机架摊、优先新型号)。偏好项的权重给软诉求排优先级:便宜容量优先,贵池子当溢出。强制规则每季度 review,因为去年稀缺的硬件今年可能遍地都是;一条没人记得的强制规则,就是"集群明明有空位却不调度"的病因。

反亲和把副本分开

反亲和看的是其它 Pod,把一个工作负载的副本摊到不同节点,单个节点故障的杀伤面就变小。配合副本数与 topology 键使用,分离才有意义,而不是表面文章。topologyKey: kubernetes.io/hostname 跨机器散,topologyKey: topology.zone 跨故障域散。前者扛死 kubelet,后者扛死机房——按你真正怕的 outage 选键;跨 zone 散要 zone 数不小于副本数才有意义。

强制反亲和值得跟强制亲和一样的谨慎:三副本配强制主机散开,坏一台没事,但工作负载永远超不过节点数,两节点集群根本装不下。对多数服务,高权重的偏好反亲和拿到几乎全部韧性,又没有调度脆弱。强制形态留给那几个 co-location 真的比 Pending 更糟的:quorum 成员、单 leader 对。

新规则上线后不要只看能否调度,还要看落点是否符合预期:用一周时间每天按节点标签分组统计一次实际分布,确认 GPU 负载真的留在 GPU 池、合规负载没有外溢。若发现偏好规则长期被容量压力打败,就把缺口量化成扩容需求写进排期;若某条强制规则的匹配节点长期为零,就回头检查标签名是否拼错或已被改名,避免一条僵尸规则悄悄卡住未来的发布。

坏掉的样子

标志性故障是永久 Pending:events 写着 didn't match pod affinity rules,节点却半空。病因几乎总在标签侧——节点标签没打、改名了、值跟规则对不上(nvme 对 nvme-ssd)。kubectl get nodes -L hardware,workload-class 一张表看清实际标签面,跟 Pod 的要求一对,mismatch 多半当场招供。

更阴的是满足但错位的放置:偏好规则在容量压力下悄悄输掉,几周下来 GPU 负载漂到共享节点,GPU 池子却闲着。偏好意味着调度器可以不听,有压力时它真不听。定期按意图审计放置——按负载和节点标签分组查——对总在漂的规则从偏好升强制,或把逼你妥协的容量缺口补上。

快速验证

标签面、要求、实际落点一起看:

kubectl get nodes -L accelerator,workload-class --no-headers
kubectl describe pod api-0 | grep -A 8 "Node-Selectors"
kubectl get pods -o wide -l app=api | awk '{print $1, $7}' | sort | uniq -c | sort -rn | head

第一条看标签面,第二条看一个 Pod 到底要求什么,第三条证明副本真散开了还是悄悄堆在一台上。

来源