ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【Kubernetes从入门到精通】第27篇:节点亲和性和Pod亲和性——让Pod去它该去的地方

2026/8/11 6:01:49 拓冰建站 浏览量
【Kubernetes从入门到精通】第27篇:节点亲和性和Pod亲和性——让Pod去它该去的地方 上一篇【第26篇】Scheduler——Pod的“婚介所“是怎么工作的下一篇【第28篇】污点和容忍——K8s的拒之门外机制摘要上篇咱们聊了Scheduler这个婚介所怎么海选打分但默认的配对算法有时不够用——你想把GPU任务丢到带显卡的Node上把核心服务分散到不同机房把前端和后端部署在同一台机器上降低延迟。这些需求靠默认的打分机制搞不定得给Scheduler加硬性条件和软性偏好。这就是亲和性的用武之地。节点亲和性Node Affinity是Pod→Node的单箭头Pod要求或偏好跑在特定标签的Node上。Pod亲和性Pod Affinity是Pod→Pod的关系网Pod要求或偏好和某些Pod做邻居。Pod反亲和性Anti-Affinity则是Pod→Pod的排斥力Pod要求或偏好不和某些Pod挤在一起——这招是搞高可用的利器把同一个Service的Pod打散到不同可用区一个机房挂了别的还能扛。一、节点亲和性——“我要住在带泳池的房子”1.1 nodeSelector的太爷爷——从简单到灵活的进化你肯定会问Pod YAML里不是有nodeSelector吗为啥还要Node Affinity【nodeSelector vs NodeAffinity——进化之路】 nodeSelector石器时代 NodeAffinity现代文明 ┌──────────────────────────┐ ┌──────────────────────────────┐ │ 只能等于匹配 │ │ 支持 In, NotIn, Exists, │ │ disktype: ssd │ │ DoesNotExist, Gt, Lt │ │ │ │ │ │ 只能硬性要求 │ │ 支持硬性(required)和软性 │ │ 找不到就Pending │ │ (preferred)两种模式 │ │ │ │ │ │ 简单的 KV 匹配 │ │ 支持多条件组合 │ │ │ │ matchExpressions │ └──────────────────────────┘ └──────────────────────────────┘ 简单来说nodeSelector 只能问你有 ssd 标签吗Yes/No NodeAffinity 可以问你的 disktype 在 [ssd, nvme] 里吗还不一定非要满足要点nodeSelector还在用但对于复杂场景它实在太简陋了。K8s也不打算移除它向后兼容但推荐新项目都用Node Affinity——表达能力不是一个级别。1.2 硬亲和——“非你不可”apiVersion:v1kind:Podmetadata:name:gpu-training-podspec:affinity:nodeAffinity:# 硬亲和调度时必须满足不满足就PendingrequiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:-matchExpressions:# 条件1Node必须有 acceleratornvidia-tesla 标签-key:acceleratoroperator:Invalues:-nvidia-tesla-v100-nvidia-tesla-a100# V100或A100都行# 条件2并且 Node的 topology.kubernetes.io/zone 不能在某些Zone-key:topology.kubernetes.io/zoneoperator:NotInvalues:-zone-c# 避开 zone-c那区没GPUcontainers:-name:cuda-trainingimage:nvidia/cuda:11.8-base【硬亲和匹配过程】 集群3个NodePod要求 accelerator In [v100, a100] Node-1 Node-2 Node-3 ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ accelerator: │ │ accelerator: │ │ accelerator: │ │ nvidia-v100 │ │ none │ │ nvidia-a100 │ │ │ │ │ │ │ │ ✅ 匹配 │ │ ❌ 没标签 │ │ ✅ 匹配 │ └────────────────┘ └────────────────┘ └────────────────┘ ↓ ↓ 候选Node-1 候选Node-3 → Scheduler继续打分二选一1.3 软亲和——“最好是这样但不是也行”apiVersion:v1kind:Podmetadata:name:cache-podspec:affinity:nodeAffinity:# 软亲和尽量满足实在不行也能调度preferredDuringSchedulingIgnoredDuringExecution:-weight:80# 权重 80——很想要preference:matchExpressions:-key:disktypeoperator:Invalues:-ssd# 最好跑在SSD节点上-weight:20# 权重 20——有这个更好preference:matchExpressions:-key:node-typeoperator:Invalues:-high-performance# 最好是高性能节点containers:-name:redisimage:redis:7-alpine【软亲和打分机制】 场景集群有4个Node没有完美的SSD高性能节点 ┌──────────┬───────────┬──────────┬─────────┬──────────┐ │ Node │ disktype │node-type │ 得分80 │ 得分20 │ 总分 │ ├──────────┼───────────┼──────────┼─────────┼──────────┼─────┤ │ worker-1 │ ssd │ normal │ 80 │ 0 │ 80 │ │ worker-2 │ hdd │ high-perf│ 0 │ 20 │ 20 │ │ worker-3 │ ssd │ high-perf│ 80 │ 20 │ 100 │ ★ │ worker-4 │ hdd │ normal │ 0 │ 0 │ 0 │ └──────────┴───────────┴──────────┴─────────┴──────────┴─────┘ 结论worker-3 得分最高100分Pod会调度过去 但如果只有 worker-2 和 worker-4Pod最终会选 worker-220分 → 软亲和有更好没有也不会让Pod一直Pending要点软亲和的weight范围是1-100。多个软亲和条件可以组合——比如最好在SSD节点(weight80) 最好在ZoneA(weight50)Scheduler会把所有偏好相加算总分。软亲和不会阻塞调度只是给打分增加倾斜。1.4 requiredIgnored和preferredIgnored的IgnoredDuringExecution是什么鬼【IgnoredDuringExecution 的含义】 调度时检查运行时不检查 调度时Scheduling 运行时Execution ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ Pod创建时检查Node标签 │ │ Pod已经在Node上跑着 │ │ disktypessd? ✅ 调度到Node-1 │ │ 有人把Node-1的标签改了 │ │ │ │ disktypehdd → 怎么办 │ │ │ │ │ │ │ │ Ignored → 不管继续跑 │ └──────────────────────────────┘ └──────────────────────────────┘ 为什么叫IgnoredDuringExecution 因为K8s目前截至v1.30还没有实现RequiredDuringExecution ——即运行中标签变了就驱逐Pod。社区一直在讨论但还没做。二、Pod亲和性——“我要和MM住同一个小区”2.1 Pod Affinity的工作方式Pod亲和性不是看Node标签而是看Node上已经跑了哪些Pod——这是一种关系型调度。【Pod亲和性 vs 节点亲和性——本质区别】 节点亲和性Pod → Node Pod亲和性Pod → Pod ┌───────────────────┐ ┌───────────────────┐ │ Pod: 我要住SSD房 │ │ Pod: 我要和Redis │ │ ↓ │ │ 当邻居 │ │ ┌───┐ │ │ ↓ │ │ │SSD│ ← Node标签 │ │ ┌─────┐ │ │ └───┘ │ │ │Redis│ ← Pod │ │ │ │ └──┬──┘ │ │ 看Node的属性 │ │ │ │ └───────────────────┘ │ ┌───▼──┐ │ │ │Node-A│ │ │ │(Redis│ │ │ │在这!) │ │ │ └──────┘ │ │ 看Node上跑着谁 │ └───────────────────┘2.2 实战——把缓存服务和API服务部署在一起# 场景API Pod 启动后会调用本地 Redis如果Redis在同一Node上# 走 localhost 通信比跨Node快得多延迟从ms级降到μs级# 1. 先部署 RedisapiVersion:apps/v1kind:Deploymentmetadata:name:redis-cachespec:replicas:3selector:matchLabels:app:redis-cachetemplate:metadata:labels:app:redis-cache# ← 这个标签是亲和性匹配的目标spec:containers:-name:redisimage:redis:7-alpine---# 2. API服务——要求调度到有Redis Pod的Node上apiVersion:apps/v1kind:Deploymentmetadata:name:api-serverspec:replicas:5selector:matchLabels:app:api-servertemplate:metadata:labels:app:api-serverspec:affinity:podAffinity:# 硬性要求——必须和Redis Pod在同一拓扑域requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-redis-cache# 找 appredis-cache 的PodtopologyKey:kubernetes.io/hostname# ← 拓扑域 同一台Nodecontainers:-name:apiimage:myapp:latest【Pod亲和性调度结果】 调度前 ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Node-1 │ │ Node-2 │ │ Node-3 │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ │ redis-a │ │ │ │ redis-b │ │ │ │ redis-c │ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ 调度后API Pod 亲和到 Redis Pod ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐ │ Node-1 │ │ Node-2 │ │ Node-3 │ │ ┌──────────┐┌───────┐ │ │ ┌──────────┐┌───────┐ │ │ ┌──────────┐┌───────┐ │ │ │ redis-a ││ api-1 │ │ │ │ redis-b ││ api-2 │ │ │ │ redis-c ││ api-3 │ │ │ └──────────┘└───────┘ │ │ └──────────┘└───────┘ │ │ └──────────┘└───────┘ │ │ ┌───────┐ │ │ ┌───────┐ │ │ │ │ │ api-4 │ │ │ │ api-5 │ │ │ │ │ └───────┘ │ │ └───────┘ │ │ │ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘ API Pod 都挤到有Redis的Node上 → 本地通信延迟极低要点Pod亲和性的topologyKey决定了什么范围内算邻居。kubernetes.io/hostname意味着同一台物理机topology.kubernetes.io/zone意味着同一个可用区。选对你的拓扑域很重要——太大失去意义全都在一起太小调度不上去没有Node同时满足条件。三、Pod反亲和性——“离那个家伙远一点”3.1 反亲和性才是高可用的核心武器【亲和性 vs 反亲和性】 亲和性让Pod互相靠近 反亲和性让Pod互相远离 ┌───────────────────────────┐ ┌───────────────────────────┐ │ API→我要和Redis在一起 │ │ API→别把我和其他API放一起 │ │ │ │ │ │ Node-1: [Redis, API] │ │ 理想每个Node只跑一个API │ │ Node-2: [Redis, API] │ │ Node-1: [API-1] │ │ Node-3: [Redis, API] │ │ Node-2: [API-2] │ │ │ │ Node-3: [API-3] │ │ 好处延迟低 │ │ 好处Node-1挂了API-2/3还在 │ │ 风险整个Node挂了全没 │ │ 风险无本来就是打散的 │ └───────────────────────────┘ └───────────────────────────┘3.2 实战——把Web服务打散到不同可用区apiVersion:apps/v1kind:Deploymentmetadata:name:web-frontendspec:replicas:6selector:matchLabels:app:web-frontendtemplate:metadata:labels:app:web-frontendspec:affinity:# 反亲和性——不要把同一个应用的Pod放在一起podAntiAffinity:# 硬性要求requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-web-frontend# 不和别的 web-frontend Pod 在一起topologyKey:topology.kubernetes.io/zone# ← 每个可用区最多一个Podcontainers:-name:nginximage:nginx:1.25【反亲和性——可用区级打散效果】 集群3个Zone每个Zone有2个Node ┌─────────────────────────────────────────────────────────────┐ │ Zone-A │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Node-1a │ │ Node-2a │ │ │ │ ┌─────────┐ │ │ │ ← 只能有1个 web Pod │ │ │ │ web-1 │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ └─────────────┘ └─────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ Zone-B │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Node-1b │ │ Node-2b │ │ │ │ ┌─────────┐ │ │ │ ← 也只能有1个 │ │ │ │ web-2 │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ └─────────────┘ └─────────────┘ │ ├─────────────────────────────────────────────────────────────┤ │ Zone-C │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Node-1c │ │ Node-2c │ │ │ │ ┌─────────┐ │ │ │ ← 也只能有1个 │ │ │ │ web-3 │ │ │ │ │ │ │ └─────────┘ │ │ │ │ │ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ 6个 replicas但只有3个Zone → 只能调3个Pod 剩下3个Pod一直Pending → topologyKey粒度太大# 解决方案改用 kubernetes.io/hostname 作为拓扑域# 这样每个Node最多一个web Pod6个副本可以分散到6个NodeapiVersion:apps/v1kind:Deploymentmetadata:name:web-frontendspec:replicas:6selector:matchLabels:app:web-frontendtemplate:metadata:labels:app:web-frontendspec:affinity:podAntiAffinity:preferredDuringSchedulingIgnoredDuringExecution:# ← 改用软性-weight:100podAffinityTerm:labelSelector:matchExpressions:-key:appoperator:Invalues:-web-frontendtopologyKey:kubernetes.io/hostname# ← 每个Node尽量不重复containers:-name:nginximage:nginx:1.253.3 topologyKey详解——“到底什么算邻居”topologyKey含义粒度典型场景kubernetes.io/hostname同一台Node最细每个Node只跑一个Pod最高可用性topology.kubernetes.io/zone同一可用区中等跨Zone容灾Zone全挂还有备份topology.kubernetes.io/region同一地域最粗跨Region部署很少用自定义标签自定义拓扑自定义比如按机柜(rack)、按机房(room)【不同 topologyKey 的调度效果对比】 topologyKey: kubernetes.io/hostname 每个Node最多一个同标签Pod ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │Node-1│ │Node-2│ │Node-3│ │Node-4│ │ web-1│ │ web-2│ │ web-3│ │ web-4│ └──────┘ └──────┘ └──────┘ └──────┘ 挂了1台 → 损失25%容量 topologyKey: topology.kubernetes.io/zone 每个Zone最多一个同标签Pod Zone-A Zone-B Zone-C ┌──────┐ ┌──────┐ ┌──────┐ │ web-1│ │ web-2│ │ web-3│ └──────┘ └──────┘ └──────┘ 挂了1个Zone → 损失33%容量 BUT 其他Zone还活着 结论选hostname 防Node故障 选zone 防机房故障需配合软亲和调度更多Pod要点反亲和性最大的坑是replicas 拓扑域数量。比如你设了6个replicas但topologyKey: hostname的集群只有4个Node——那有2个Pod会永远Pending。解决方案(1) 降低replicas(2) 改用软反亲和preferred(3) 扩大拓扑域从hostname改成zone。四、实战——同城双活调度方案4.1 需求场景【同城双活架构需求】 核心服务api-server需要3个副本 要求 1. 必须分散在至少2个可用区一个Zone挂了另一个还能扛 2. 同一个Zone内最多2个Pod 3. 最好每个Node只跑1个Pod ┌─────────────────────────────────────────────────────────┐ │ Region: 北京 │ │ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ Zone-A (机房1) │ │ Zone-B (机房2) │ │ │ │ │ │ │ │ │ │ ┌─────┐ ┌─────┐ │ │ ┌─────┐ │ │ │ │ │Node1│ │Node2│ │ │ │Node3│ │ │ │ │ │ api │ │ api │ │ │ │ api │ │ │ │ │ │ #1 │ │ #2 │ │ │ │ #3 │ │ │ │ │ └─────┘ └─────┘ │ │ └─────┘ │ │ │ └───────────────────┘ └───────────────────┘ │ │ ↑ ↑ │ │ Zone-A 挂了 → Zone-B 继续服务 │ └─────────────────────────────────────────────────────────┘4.2 完整配置apiVersion:apps/v1kind:Deploymentmetadata:name:api-serverspec:replicas:3selector:matchLabels:app:api-servertemplate:metadata:labels:app:api-serverspec:affinity:# 策略1Pod反亲和性——软性尽量每个Node只跑1个podAntiAffinity:preferredDuringSchedulingIgnoredDuringExecution:-weight:100# 最高权重podAffinityTerm:labelSelector:matchExpressions:-key:appoperator:Invalues:-api-servertopologyKey:kubernetes.io/hostname# 每个Node尽量不重复# 策略2Pod反亲和性——硬性必须跨可用区podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-api-servertopologyKey:topology.kubernetes.io/zone# ← 硬性不同Zone# 策略3节点亲和性——避开不稳定的ZonenodeAffinity:requiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:-matchExpressions:-key:topology.kubernetes.io/zoneoperator:NotInvalues:-zone-c# Zone-C不稳定不去-key:node-typeoperator:Invalues:-production# 只调生产节点containers:-name:api-serverimage:api-server:v2.0ports:-containerPort:8080resources:requests:cpu:500mmemory:512Mi# 验证调度结果kubectl get pods-lappapi-server-owide# NAME READY STATUS NODE ZONE# api-server-6d4b7c8f9a-abc12 1/1 Running node-1a zone-a# api-server-6d4b7c8f9a-def34 1/1 Running node-2a zone-a ← 和上面同Zone但不同Node# api-server-6d4b7c8f9a-ghi56 1/1 Running node-3b zone-b ← 另一个Zone# 查看亲和性配置kubectl get pod api-server-6d4b7c8f9a-abc12-oyaml|grep-A30affinity策略配置方式作用本方案中的作用Node Affinity (硬)required...nodeAffinityPod→Node避开不稳定的Zone-C只跑在生产节点上Pod Anti-Affinity (硬)required...podAntiAffinityPod排斥Pod必须跨Zone——Zone-A挂了Zone-B还有Pod Anti-Affinity (软)preferred...podAntiAffinityPod排斥Pod尽量不同Node——进一步降低故障半径Pod Affinity (硬)required...podAffinityPod吸引Pod本方案没用到——如需就近缓存可加要点亲和性可以叠加——一个Pod可以同时配置节点亲和性 Pod亲和性 Pod反亲和性。Scheduler会把所有条件都纳入过滤和打分最终选择满足硬性要求且软性得分最高的Node。但注意别配置冲突——比如Pod亲和性要求A和B在一起同时又用反亲和性说A和B不能在一起——这会让Pod永远调不上去。本篇小结亲和性是K8s调度的微操工具让你能精确控制Pod的落点节点亲和性Node Affinity回答Pod应该去哪种Node——GPU任务去GPU节点SSD敏感的服务去SSD节点Pod亲和性Pod Affinity回答Pod应该和谁做邻居——API和Redis部署在一起降低延迟Pod反亲和性Pod Anti-Affinity回答Pod应该离谁远点——同服务Pod打散到不同可用区实现高可用topologyKey定义了什么范围算在一起——hostname单Nodezone可用区自定义标签自定义拓扑required硬vs preferred软——硬的不满足就Pending软的只是加分项亲和性管的是接近但有时候你需要的是排斥——“这台Node谁都不准来除非你拿通行证”。下一篇咱们聊污点和容忍——K8s的闲人免进机制。上一篇【第26篇】Scheduler——Pod的“婚介所“是怎么工作的下一篇【第28篇】污点和容忍——K8s的拒之门外机制