想象你是大学宿舍楼的管理员。新学期开始,几十位新生提着行李排队登记,每个人都有自己的小愿望:有人希望和同班同学住同一楼层,方便一起上课;有人明确说“我打呼噜,千万别让我和睡眠浅的人住隔壁”;还有人只想要朝南带阳台的房间。你的任务是在有限的房间(节点)里,把几百个学生(Pod)安排得既满足硬性要求,又不至于浪费资源。Kubernetes 的调度器就是这位“宿管阿姨”,而亲和性与反亲和性,就是她手里的那张“入住偏好表”。 本文用“宿舍分配”这条故事线,带你从零看懂 K8s 的调度策略:什么是节点亲和、Pod 亲和,什么是硬亲和、软亲和,拓扑域又是什么。读完你会明白,调度器如何先过滤掉不合格的房间,再给剩余房间打分排序,最终把每个 Pod 放到最合适的位置,让整个集群资源用得更高效、服务更稳定。
对 AI 从业者来说,掌握亲和性与反亲和性,意味着你能像控制微服务分布一样精细地控制模型推理 Pod 的位置,让 GPU 节点、数据节点和计算节点各就各位,是优化集群资源利用率和服务可用性的必备技能。
可视化演示
展示Kubernetes调度器的节点过滤打分流程、亲和与反亲和的Pod分布对比、以及拓扑域maxSkew约束效果,切换模式查看不同调度策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
调度两阶段:Filter与Score Filter阶段排除不满足硬约束的节点 Score阶段对候选节点按亲和权重打分 最高分节点获得Pod放置权 Score = Σ(weight × match) 硬约束过滤+软约束打分选出最优节点
调度器基础
宿管阿姨分配宿舍时,心里有一本账:这间屋还有几个空床位、空调功率够不够、离热水房近不近。她不会直接把新生塞进一个满员的宿舍,也不会让 8 个人挤进 4 人间。Kubernetes 的调度器 kube-scheduler 做的事一模一样:它不停发现“等待入住”的 Pod,先看哪些节点满足硬性资源需求,再对候选节点排序,最后挑一个最合适的。
这里的“硬性需求”就是 Pod 声明的 CPU、内存、GPU 等资源请求。调度器先做预选(Filter),把资源不够的节点直接排除;再做优选(Prioritize),给剩余节点按空闲资源比例打分。分数高不代表一定被选中,但它是重要参考。
资源评分只是基础。宿管阿姨还要看更细的偏好:这位同学想和谁住、想离谁远一点。于是,亲和性规则登场了。
- 节点评分 Score(n)对候选节点
n进行综合打分,得分越高越优先。 - 权重 0.5假设 CPU 和内存同等重要,各占一半权重;实际调度器里权重可配置。
- CPU 空闲率节点上还可分配的 CPU 除以总 CPU,体现“床位剩余比例”。
- 内存空闲率用同样的方式衡量内存余量,避免某个维度成为瓶颈。
亲和性概念
新同学登记入住时,会填一张“住宿偏好表”:想和同班同学同楼层,不想和打呼噜的同学隔壁。K8s 里,节点和 Pod 可以被贴上各种“标签”(Label),比如 zone=az1(所在片区)、app=web(属于哪个应用),而亲和性规则就是用来描述“我想接近哪些标签”或“我想远离哪些标签”的选择器。
亲和性分为硬亲和和软亲和。硬亲和的英文是 requiredDuringSchedulingIgnoredDuringExecution,意思是调度时必须满足,不满足就不把 Pod 放在这个节点上;软亲和是 preferredDuringSchedulingIgnoredDuringExecution,意思是“最好能满足”,满足的话加分,不满足也没关系,只要没有更糟。
明白了标签与匹配,我们就能继续往下看:到底怎么让“人”靠近“房间”或靠近“人”。
- 匹配函数 Match(p, s)判断 Pod/节点
p的标签是否满足选择器s的规则。 - 全称合取规则里的每一项都成立,整体才算匹配,只要有一个不满足就失败。
- 键存在性先检查标签里有没有这个键
k,没有键自然匹配失败。 - 值相等性再检查
k对应的值是否等于规则要求的值v。
节点亲和
有同学说:“我只住朝南带阳台的房间。”这对应的就是节点亲和 nodeAffinity:让 Pod 按节点的标签来筛选和加分。它比老式的 nodeSelector 更灵活,因为不仅支持“等于某个值”,还支持 In(属于某个列表)、NotIn(不属于)、Exists(键存在)等操作符。
节点亲和也有硬性和软性之分。英文名里的 requiredDuringSchedulingIgnoredDuringExecution 是硬条件,必须满足;preferredDuringSchedulingIgnoredDuringExecution 是软条件,只影响打分。硬条件负责“过滤”,软条件负责“加分”。
节点亲和解决的是“房间好不好”的问题,但同学还关心“邻居是谁”,这就是 Pod 亲和要解决的事。
- 硬性规则检查
Req里的每条规则都必须匹配,否则节点被直接淘汰。 - 软性规则遍历遍历
Pref中的每一条软规则r。 - 条件指示器
1[条件]表示条件成立时取 1,否则取 0。 - 加权求和每条软规则有自己的权重
w_r,所有命中规则的权重相加得到节点亲和得分。
Pod亲和
有的同学说:“我想和我的好朋友住同一楼层,下课一起走。”K8s 里这叫 Pod 亲和 podAffinity:让新 Pod 靠近一组已经在运行的目标 Pod。这里最重要的一个概念是拓扑域(Topology Domain),它决定了“同一范围”如何定义——是同一个节点,还是同一个可用区,甚至同一个机房。
topologyKey 就是拓扑域的“刻度尺”。比如 kubernetes.io/hostname 表示“同一节点才算近”;topology.kubernetes.io/zone 表示“同一可用区就算近”。Pod 亲和通过 podSelector 选中目标 Pod,只要目标 Pod 落在某个拓扑域内,新 Pod 在调度时就会给这个域的节点加分。
有“想靠近的”,自然就有“想远离的”。下一个概念,就是反亲和性。
n 处于同一拓扑域的所有 Pod 中,有多少个命中了你要靠近的目标 Pod 选择器。- 同域 Pod 集合
P_{domain}(n)表示所有与节点n在同一个拓扑域里运行的 Pod。 - 对每个 Pod 判断遍历这些 Pod,逐个看它们是否满足
Selector。 - 指示器取值
1[Selector(q)]命中目标 Pod 得 1,不命中得 0。
反亲和性
宿舍里最怕两件事:同寝室两人都打呼噜,或者两个半夜开黑的人凑一起熬夜。管理员在分配时,会特意把“热闹型”的同学拆开。K8s 的 反亲和性 podAntiAffinity 干的就是这件事:让 Pod 不跟某些 Pod 挤在同一个拓扑域。
反亲和性最典型的应用是高可用。比如订单服务有 3 个副本,如果 3 个 Pod 都跑在同一台机器上,机器一宕机服务就全挂了。用反亲和性把它们分散到不同节点、不同可用区,单点故障就不会拖垮整个应用。
亲和与反亲和都摆在面前了,宿管阿姨该怎么综合决策呢?下一节我们把所有规则揉在一起。
- 冲突 Pod 统计
D_{anti}(n)用来累计同一个拓扑域内“我不想靠近”的 Pod 数量。 - 遍历同域 Pod和 Pod 亲和一样,先拿到同一个拓扑域里的所有 Pod。
- 反选器匹配
AntiSelector(q)命中代表这个 Pod 是我们想避开的对象。 - 权重影响不同冲突 Pod 的“讨厌程度”可能不同,用
w_q表达。
调度场景
真正的宿舍分配,从来不是只看一条规则。姓张的同学要求朝南,又想和同班同学同楼层,还不想和打呼噜的住隔壁。宿管阿姨的做法是:先用硬性规则把不合格的房间全部划掉,再用软性规则给剩余房间打分,最后选出总分最高的那个。
K8s 调度器也是这样:nodeAffinity、podAffinity、podAntiAffinity 可以同时出现在一个 Pod 的调度规则里。硬性规则作为过滤器,任一不满足就排除;软性规则进入评分环节,按权重累加。比如计算任务可以“尽量靠近数据所在的节点”,同时“必须避开 CPU 已经被占满的机器”。
规则设计得再好,也要在真实集群里验证。最后一节我们聊聊如何用 kubectl 检查和调试。
- 节点亲和得分
A_{node}(n)来自节点标签匹配的加分,权重为w_n。 - Pod 亲和得分
A_{pod}(n)是靠近目标 Pod 的加分,权重为w_p。 - 反亲和扣分
D_{anti}(n)是冲突 Pod 带来的扣分项,权重为w_a。 - 总分汇总三者相加,决定最终节点的优先级。
实践验证
宿管阿姨分配完宿舍,还要拿着名单挨个确认:谁住进了哪一间?有没有人和要求不符?运维同学手里也有一把“钥匙”:kubectl get pods -o wide 可以看到每个 Pod 最终落在哪个节点;kubectl get nodes --show-labels 可以确认节点的标签值;kubectl describe pod <name> 能看到调度器给出的失败原因,比如 0/3 nodes are available: 2 Insufficient cpu, 1 node(s) didn't match node selector。
最常见的问题有三个:节点标签写错了、Pod 的选择器写错了、拓扑域定义得和实际部署不一致。只要耐心看 Events 和标签,大部分调度问题都能快速定位。
从“调度器如何选房间”到“如何用 kubectl 查作业”,你已经掌握了亲和性调度的完整地图。接下来,我们看看这些能力在真实 AI 工程里的用武之地。
- 已调度 Pod 集合遍历集群中所有已经完成调度的 Pod。
- 检查硬规则失败
HardRuleFails(p)为真表示 Pod 的硬亲和条件没有被满足。 - 检查反亲和冲突
AntiAffinityConflict(p)为真表示它和不该共存的 Pod 同居了一个拓扑域。 - 统计违规数把每一个违规 Pod 累计求和,得到
Violations。
AI 应用场景
微服务同机房低延迟部署
阶段:模型服务部署与流量调度
解决的问题:AI 推理链路通常包含网关、鉴权、模型推理等多个微服务,如果它们跨机房调用,延迟会明显升高。传统做法是手动指定节点,维护成本高且容易出错。用 podAffinity 把推理 Pod 调度到已经运行着网关 Pod 的同拓扑域节点上,就能自动保证“物理距离”很近。
具体案例:某推荐系统在华东双可用区部署,网关和推理服务通过 podAffinity + topology.kubernetes.io/zone 绑定同一可用区,P99 延迟从 120ms 降到 68ms。
多副本高可用反亲和
阶段:模型服务容灾与稳定性保障
解决的问题:模型推理副本如果堆在同一台 GPU 节点,节点故障会造成全部副本失联。通过 podAntiAffinity 的 requiredDuringSchedulingIgnoredDuringExecution,强制让同一模型的副本分散在不同节点或不同可用区,实现故障域隔离。
具体案例:大模型推理服务有 4 个 Pod,配置反亲和键 app=llm-server,topologyKey 用 topology.kubernetes.io/zone,保证 4 个副本落在至少 3 个可用区,单可用区故障时服务不中断。
数据本地性优化
阶段:分布式训练和数据处理
解决的问题:Spark 或 Ray 这类计算框架需要尽量把计算任务调度到“数据所在的节点”上,减少网络传输。通过 nodeAffinity 选择带有 data=local 标签的节点,或者用 podAffinity 靠近数据服务 Pod,可以显著提升数据读取效率。
具体案例:离线训练集群给湖存储节点打上 storage=fast 标签,训练 Pod 声明 nodeAffinity 的 preferred 规则指向该标签,任务读写延迟降低约 35%。
Python 演示
def match(labels, selector): for key, val in selector.items(): if key not in labels: return False if isinstance(val, list) and labels[key] not in val: return False if isinstance(val, str) and labels[key] != val: return False return True class Pod: def __init__(self, name, labels, hard_node=None, pref_node=None): self.name = name self.labels = labels self.hard_node = hard_node or {} self.pref_node = pref_node or {} self.node = None class Scheduler: def __init__(self, nodes): self.nodes = nodes # list of dict: {'name': str, 'labels': dict} self.placements = [] def score(self, node, pod): if pod.hard_node and not match(node['labels'], pod.hard_node): return -1 s = 0.5 if match(node['labels'], pod.pref_node): s += 0.5 return s def schedule(self, pod): best = None for node in self.nodes: s = self.score(node, pod) if s > 0 and (best is None or s > best[1]): best = (node['name'], s) if best: pod.node = best[0] self.placements.append((pod.name, pod.node)) else: pod.node = None return pod.node if __name__ == '__main__': nodes = [ {'name': 'node-a', 'labels': {'zone': 'az1', 'ssd': 'true'}}, {'name': 'node-b', 'labels': {'zone': 'az1', 'ssd': 'false'}}, {'name': 'node-c', 'labels': {'zone': 'az2', 'ssd': 'true'}}, ] sc = Scheduler(nodes) p1 = Pod('web-1', {'app': 'web'}, {'zone': 'az1'}, {'ssd': 'true'}) p2 = Pod('db-1', {'app': 'db'}, {'zone': 'az2'}) for p in (p1, p2): print(p.name, '->', sc.schedule(p))
def match(labels, selector):最基础的标签匹配函数,遍历选择器里所有键值对。if isinstance(val, list) and labels[key] not in val:支持In操作符的简化版:值列表里没有就算不匹配。if pod.hard_node and not match(node['labels'], pod.hard_node):硬亲和不满足,直接返回 -1,表示节点被过滤。s = 0.5基础分,代表“房间已经可住”,后续软偏好在此基础上加分。if match(node['labels'], pod.pref_node): s += 0.5软亲和命中,给节点加分,对应 preferred 规则。best = (node['name'], s)记录当前得分最高的节点。for node in self.nodes:遍历所有候选节点,模拟调度器的优选阶段。
常见误区
误区“既然 nodeSelector 也能按节点标签选节点,那还要 nodeAffinity 干嘛?”
正解nodeSelector 只能表达“键值完全相等”这种最简单的匹配,做不到“值属于某几个选项”“键存在即可”这种表达,也分不清硬条件和软偏好。nodeAffinity 更像表达力更强的“升级版选择器”,支持 In、NotIn、Exists 等操作符,还能用 preferred 给节点加分,而不是一票否决。
为何常见:因为两者都用节点标签作为依据,初学时容易被“看起来差不多”的外表迷惑。
误区“我写了 preferred 亲和规则,如果节点没有这个标签,Pod 为什么还是被调度上去了?是不是配置失效了?”
正解preferredDuringSchedulingIgnoredDuringExecution 本身就是“软”的宿主偏好,它不会阻止调度,只会在打分时影响优先级。真正起强制过滤作用的是 requiredDuringSchedulingIgnoredDuringExecution。软亲和不满足时,调度器只会感叹“可惜”,然后把 Pod 放到别处。
为何常见:名字太长,大家容易忽略 preferred 与 required 的语义差异,以为写了任意亲和规则就会强制生效。
误区“只要我写了 podAffinity,Pod 自然就会和指定 Pod 跑到同一台机器上,topologyKey 随便写个 hostname 就行。”
正解topologyKey 决定了“同一拓扑域”的边界。如果用它表示 kubernetes.io/hostname,调度器会把“同一节点”视为同一域;如果用它表示 topology.kubernetes.io/zone,则会把整个可用区视为一个域。乱填不存在的标签键,会导致调度器永远找不到同域 Pod,亲和规则静默失效。
为何常见:拓扑域这个概念比较抽象,初学者容易把它当成一个普通参数,忽视了它和节点标签的强绑定关系。
误区“我用 podAntiAffinity 设置了副本分散,为什么两个副本还是落在了同一个节点?”
正解如果用的是 preferred 反亲和,调度器会尽量分散,但不保证;即便用了 required 反亲和,也要看是否存在“无解”的情况。比如集群里只有一个节点,Pod 的 required 反亲和要求不能共节点,调度器只能让 Pod 一直 Pending。另外,如果 topologyKey 选得太粗,比如用 zone,同一可用区里不同节点也算“同一域”,副本自然会挤在一起。
为何常见:高可用场景下大家默认“反亲和=一定会分开”,却忽略了 required/preferred 和拓扑域粒度共同决定最终结果。
误区“我把某个节点的标签改了,为什么已经跑在上面的 Pod 没有重新调度到别处?”
正解亲和性规则名字里的 DuringSchedulingIgnoredDuringExecution 已经说明了语义:只在“调度过程中”生效,执行阶段被忽略。Pod 一旦被调度到节点上,即使节点标签后来变了,也不会触发重新调度;只有 Pod 被删除重建,或通过其他机制触发重新调度,新规则才会生效。
为何常见:这个英文名太长,大多数人只记住了前半段“DuringScheduling”,忽略了后半段“IgnoredDuringExecution”。
面试题
Q1. Kubernetes 中负责把 Pod 调度到合适节点上的核心组件是?
- A.kubelet
- B.kube-scheduler← 正确答案
- C.kube-proxy
- D.etcd 存储
答案:B。kubelet 只在节点上运行容器,kube-proxy 负责网络代理,etcd 负责存储集群状态。只有 kube-scheduler 专门负责为待调度的 Pod 寻找合适的节点,就像宿管阿姨专门负责分宿舍。
Q2. 如果一个 Pod 声明了 required 硬亲和规则,但所有节点都不满足,调度器会怎么做?
- A.选择打分最高的节点硬凑
- B.让 Pod 一直处于 Pending 状态← 正确答案
- C.自动删除该规则
- D.把 Pod 调度到控制平面节点
答案:B。required 是硬性条件,没有节点满足时调度器不会安排任何节点,Pod 会留在待调度队列中并持续报错。这就像宿管阿姨发现没有朝南房间,就只能让这位同学继续在等待区等着。
Q3. nodeAffinity 的 matchExpressions 中,operator 为 In 表示什么?
- A.节点的标签值不能等于列表里任意值
- B.只要标签键存在就算匹配
- C.节点的标签值必须属于给定列表← 正确答案
- D.节点不能有该标签
答案:C。In 是“属于”语义,要求节点标签的值在给定列表里才匹配。Exists 只关心键存在,NotIn 则正好相反。
Q4. 下面哪个 topologyKey 最适合实现“跨可用区高可用反亲和”?
- A.kubernetes.io/hostname
- B.topology.kubernetes.io/zone← 正确答案
- C.beta.kubernetes.io/arch
- D.kubernetes.io/os
答案:B。topology.kubernetes.io/zone 代表可用区,用它做拓扑域,反亲和规则会把 Pod 分散到不同可用区,而不是仅仅分散到不同节点。
Q5. requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 的关键区别是?
- A.两个名字不同但效果完全相同
- B.required 不满足就不调度,preferred 只影响打分← 正确答案
- C.preferred 优先级更高
- D.required 只能用于节点亲和
答案:B。required 是硬条件,参与过滤;preferred 是软偏好,参与评分。preferred 不满足也能调度,只是优先级降低,类似“最好有阳台,但实在没有就算了”。
Q6. 当 topologyKey 为 kubernetes.io/hostname 时,两个 Pod 落在同一节点才算同一个拓扑域,这种反亲和主要用于?
- A.节点级副本分散← 正确答案
- B.可用区级容灾
- C.跨集群部署
- D.资源超卖控制
答案:A。hostname 指节点本身,因此反亲和会在节点粒度上把 Pod 拆开,避免所有副本挤在同一台机器上。
Q7. 以下关于硬亲和和软亲和共同作用的说法,正确的是?
- A.硬亲和规则直接参与加分,软亲和规则参与过滤
- B.硬亲和规则必须满足,软亲和规则在满足后加分← 正确答案
- C.硬亲和只在节点资源不足时启用
- D.软亲和规则不满足时 Pod 无法被调度
答案:B。完整调度过程是“先过滤后打分”:硬规则决定节点有没有资格,软规则决定候选节点的名次。硬规则和软规则分工明确,不能互换。
Q8. 亲和性调度可以用于哪些实际场景?(多选)
- A.将网关调度到同机房← 正确答案
- B.让数据库副本跨故障域分散← 正确答案
- C.强制所有 Pod 挤同一个节点
- D.提升数据本地性访问速度← 正确答案
答案:ABD。A 是 Pod 亲和降低延迟,B 是反亲和提升高可用,D 是亲和调度实现数据本地性。C 违反反亲和与资源利用的基本目标,不是合理场景。
Q9. 下列哪些 operator 可以用于 nodeAffinity 的 matchExpressions?(多选)
- A.In 值列表匹配← 正确答案
- B.NotIn 排除列表← 正确答案
- C.Exists 键存在判定← 正确答案
- D.SHA256 校验摘要
答案:ABC。nodeAffinity 支持 In、NotIn、Exists、DoesNotExist 等操作符,它们配合标签实现集合和存在性判断。SHA256 是哈希算法,和标签匹配无关。
Q10. 关于 podAffinity 的 topologyKey,下面说法正确的是?(多选)
- A.必须是节点上的标签键← 正确答案
- B.可以使用 zone 实现跨区亲和← 正确答案
- C.只能用 hostname 作为 key
- D.可以和 nodeAffinity 混用← 正确答案
答案:ABD。topologyKey 的值来自节点标签,不能用任意字符串;zone 是常用拓扑键之一;podAffinity 和 nodeAffinity 可以同时配置。hostname 只是其中一种选择,并非唯一。