资讯详情

Kubernetes亲和性调度实战:nodeAffinity与Pod反亲和性解析

📅 2026/10/22 9:09:58 | 华诺云谱 👁 阅读
Kubernetes亲和性调度实战:nodeAffinity与Pod反亲和性解析
1. 为什么默认调度不够用亲和性要解决的真实问题先聊一个我实际遇到过的场景。当时某个业务上线了一套微服务A服务调用B服务很频繁两个服务的Pod都分布在同一个节点池里。按理说节点池里有几十台机器Pod会均匀散开可问题是A、B两个服务的延迟曲线始终不健康P99动不动就飙到几十毫秒以上。后来把Pod列表和节点对应关系拉出来一看发现A服务的几个副本和B服务的几个副本大部分落在不同的物理机上请求绕了一圈又一圈跨节点网络开销全被吃进去了。这就是Kubernetes默认调度策略的局限——调度器只保证有地方能跑不保证跑在最合理的地方。默认的LeastRequested策略确实会把Pod尽量分散到空闲节点上但它完全不懂你的业务依赖关系也不知道哪些Pod应该住得近一点哪些Pod必须老死不相往来。亲和性与反亲和性就是用来补这块短板的。简单说它们让Pod在调度时可以去挑节点、挑邻居。这套机制一共包含三大类型节点亲和性nodeAffinity、Pod间亲和性podAffinity和Pod间反亲和性podAntiAffinity。下面我会从原理到配置再到实际踩坑把这套东西完整过一遍。2. nodeAffinity把Pod钉在想去的节点上2.1 硬规则requiredDuringSchedulingIgnoredDuringExecution先处理最常用的节点亲和性。它解决的是这个Pod必须跑在带哪些标签的节点上的问题。比老的nodeSelector表达力强很多光这一点就值得学明白。配置里最核心的字段是requiredDuringSchedulingIgnoredDuringExecution我把名字拆开你就懂了required表示必须满足不满足就不调度DuringScheduling表示只在调度阶段生效IgnoredDuringExecution表示运行期间如果节点标签变了已经跑在上面的Pod不会被打死重排。apiVersion: apps/v1 kind: Deployment metadata: name: gpu-inference spec: replicas: 2 template: spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: accelerator operator: Exists - key: gpu-model operator: In values: - A100 - V100 containers: - name: inference image: nginx上面这段的意思是节点必须同时有accelerator这个标签不管值是什么并且gpu-model的值是A100或V100POD才会被调度上去。有多个matchExpressions时它们是AND关系。如果你想表达OR关系就拆成多个nodeSelectorTerms调度器会逐个尝试满足任意一组即可。2.2 软规则preferredDuringSchedulingIgnoredDuringExecution与权重硬规则虽然好用但生产环境里真正常用的其实是软规则。原因很简单硬规则一旦条件不满足Pod就卡在Pending而这个不满足可能只是暂时性的。软规则加了一个weight字段取值范围1到100。调度器会给满足条件的节点加分分越高越优先但没有节点满足时Pod照样能调度出去。配合nodeAffinity的软规则你可以实现尽量往SSD节点上放没有SSD也接受这种需求。affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: disk-type operator: In values: - ssd这里weight: 80意思是在调度打分阶段满足disk-typessd的节点会被额外加上80分。权重值本身是个相对概念关键是和其他打分项的权重一起看。你可以在一个Pod上配置多条软规则调度器会把满足条件的节点的加权总分累加再做归一化权重越高影响力越大。2.3 operator的完整语义与常见误区在matchExpressions里operator一共有六种我直接列表说明operator语义典型用法In标签值在给定集合内指定多个允许值NotIn标签值不在给定集合内排除某些节点Exists只要存在该标签即可不管值判断节点类型DoesNotExist该标签必须不存在排除某类节点Gt标签值是数值且大于给定值按资源容量筛选Lt标签值是数值且小于给定值按资源容量筛选这里有几个常见的坑。第一个是原生标签的粒度kubernetes.io/hostname这类标签是Kubernetes自动打上的可以直接用。第二个是Gt和Lt只适用于数值型标签如果你对一个字符串标签用Gt会直接报错。第三个大家最容易忽略的是节点标签的更新不会影响已经运行的Pod除非你手动驱逐或滚动更新。我见过有人把反亲和性写成NotIn然后期望它避让结果完全没生效。原因就是NotIn只是不选择这些节点并不会考虑某个节点上是否已经有其他Pod。想实现避让得用等一下要讲的podAntiAffinity。3. podAffinity让亲密Pod住得近3.1 拓扑域的语义拓扑Key是理解整个亲和性的钥匙说到Pod间亲和性最重要的概念就是topologyKey。它决定住得近到底以什么范围来衡量。topologyKey指定一个节点标签所有拥有相同标签值的节点会被视作同一个拓扑域。比如topologyKey: kubernetes.io/hostname意味着每一台Node都是一个独立拓扑域如果集群接了多个可用区节点上有topology.kubernetes.io/zone标签那么按这个key划分的拓扑域就是一个可用区。Pod亲和性生效的逻辑是找到所有带有指定标签的目标Pod所在的节点用topologyKey去判断这些节点属于哪些拓扑域调度器只会把新Pod放到至少存在一个目标Pod的拓扑域里。这个设计其实很精妙它把离得近抽象成了共享同一拓扑域而不是精确到同一台机器。你在配置时要先想清楚你到底是想让它们住同一台机器还是同一个机架还是同一个可用区3.2 典型场景把有依赖关系的服务放一起最经典的用例就是Web服务和处理它的缓存服务。Web对缓存的访问频率极高如果每次都不命中本地网络性能损耗非常明显。用Pod亲和性把它们的Pod尽量拉进同一个节点收益非常直观。affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - webserver topologyKey: kubernetes.io/hostname这段配置的意思是新Pod调度时必须找到至少一个带appwebserver标签的Pod并且和它在同一台物理机上。如果集群里一个appwebserver的Pod都没有新Pod会因为找不到匹配而Pending。这里要特别注意labelSelector匹配的是Pod的标签而不是Node的标签很多新手会搞混。Pod亲和性关心的是邻居是Pod邻居有什么标签跟节点标签没关系。3.3 namespaceSelector与跨Namespace调度Pod上的标签是命名空间隔离的所以跨Namespace的Pod亲和性不是默认生效的。如果你想跟另一个命名空间里的某组Pod建立亲和关系得靠namespaceSelector明确指定目标命名空间。podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 50 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - analytics topologyKey: topology.kubernetes.io/zone namespaceSelector: matchLabels: team: data这段的意思是如果能跟teamdata这个命名空间下appanalytics的Pod落在同一个可用区就给节点加50分。注意这里我用了软规则避免硬亲和导致新Pod因为目标Pod不存在而一直Pending。跨命名空间亲和性在生产里通常很重一定要先确认目标Pod的副本数和分布否则规则很容易失效。4. podAntiAffinity让冤家Pod离得远4.1 高可用打散反亲和性最核心的价值如果说亲和性是为了聚那反亲和性就是为了散。没有podAntiAffinity之前如果某个Deployment有3个副本调度器完全可能把3个副本放到一台物理机上这台机器一断电整个服务全部挂掉。反亲和性强制要求这些Pod不能落在同一个拓扑域里从物理层面把故障域撑开。affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - api-server topologyKey: kubernetes.io/hostname这是运营中最常用的一段配置。api-server的副本会尽量分到不同节点上让单点故障只影响一个副本。注意我把topologyKey设置成主机名意味着每个节点最多一个该应用的Pod。这是最常见的打散粒度。4.2 隔离子网与互斥部署除了高可用反亲和性还能实现业务层面的隔离。比如某些安全合规要求下你不能让两个不同服务的Pod跑在同一台机器上。或者你有个大数据任务集群你不想让它和在线业务混部就可以在两边都加上互斥的反亲和规则。这个场景下topologyKey的选择就很讲究。如果你只要求别在同一台机器就按kubernetes.io/hostname打散如果你要求别在同一个可用区因为故障域是可用区级别的那就换成topology.kubernetes.io/zone。粒度越粗可以调度的空间越小代价也越大务必要权衡。4.3 性能代价不要在生产里堆一打反亲和规则反亲和性不是免费的午餐。它本质上要求调度器在每次调度时进行反向查询找出所有满足该拓扑域的节点和Pod然后排除掉一部分节点。当集群规模增大、Pod数量变多、反亲和规则变复杂时调度性能会肉眼可见地下降。尤其是requiredDuringSchedulingIgnoredDuringExecution的硬反亲和。它要求调度器找到的节点上没有匹配目标label的Pod这会让候选节点池大幅收缩。一个100个节点的集群如果每个节点上都跑着匹配的Pod那新Pod就永远Pending。更麻烦的是如果反亲和规则和副本数配置不当形成了死锁比如20个副本要求分布在19个节点上最后一个Pod永远找不到位置。我见过一个真实案例某个服务设置了硬反亲和规定Pod副本必须分布在不同节点上但这个服务有40个副本集群只有38个可用节点。结果最后两个Pod一直Pending三天都没人发现直到业务方反馈容量不够才排查出来。日志里全是0/38 nodes available原因字段也写清楚了只是没人认真看。5. 组合运用的真实落地场景5.1 场景一分布式缓存的拓扑打散以Redis集群为例你有多个主从分片主从节点不应该落在同一台机器上否则这台机器挂了主从同时丢失。同时多个分片之间又要相对均匀地分布避免某个节点上全是Redis Pod。affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: redis-cluster operator: In values: - shard-a topologyKey: kubernetes.io/hostname这个配置让同一个分片的Redis Pod不落在同一台机器。如果你想连主备都跨可用区隔离把topologyKey改成可用区的标签就行代价是要求每个可用区的节点数大于分片数否则照样会Pending。5.2 场景二在线服务与离线任务隔离很多大集群会把离线训练任务和在线服务混在一起跑提高资源利用率。但混部的前提是隔离不能让离线任务把在线服务的CPU打爆。这种场景通常需要双重亲和性在线服务用nodeAffinity偏好在线节点池的节点离线任务用podAntiAffinity避免落到在线服务所在节点。实际操作中离线任务还可以配合preferredDuringSchedulingIgnoredDuringExecution做软避让在线业务压力上来时离线任务可以换到别的机器不会硬生生卡死。这是生产环境最常见的玩法。5.3 场景三把Controller和业务Pod放在同一拓扑域有些业务里控制面组件和数据面组件之间通信延迟比较敏感但控制面副本数又不多直接硬亲和怕调度不出去。完美解法是控制面用软亲和尽量跟业务Pod靠在一起与此同时业务Pod不要用反亲和把控制面挤走。affinity: podAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 70 podAffinityTerm: labelSelector: matchExpressions: - key: app operator: In values: - worker topologyKey: topology.kubernetes.io/zone注意软亲和的weight选70而不是100我是故意的——因为如果设成100会压过其他正常的资源打分项导致节点选择过度集中周边节点全被浪费。这个经验是踩过坑后总结出来的weight值看着简单但它和调度器自带的其他评分项比如资源余量是互相竞争的别一开始就上满值。6. 排错与经验为什么你的亲和性规则没生效6.1 对称性约束是反亲和性最大的隐藏陷阱这是反亲和性最反直觉的一个性质。Kubernetes文档里有一句话反亲和性要求必须是对称的。什么意思如果服务A定义了对服务B的反亲和性那么调度器在把B的Pod放下去的时候也会自动认为B对A有反亲和性会进行检查即使B自己没定义任何规则。这种自动对称性会让调度条件变得更严格。我举个具体例子假设有服务A和服务BA定义了对B的硬反亲和要求不能和B在同一个节点。集群里有10个节点9个已经各跑了一个B的Pod此时A的Pod来了会发现只有1个节点可用——因为那9个节点上都有B。这是合理的。但如果反过来B的Pod先调度它会检查所有节点发现A的Pod不在这台节点上可能就直接调度到某台已有A的Pod的节点。为了让反亲和规则真正保护到双方通常需要两边都显式声明反亲和关系不要单边定义。6.2 topologyKey粒度引发的全集群不可调度topologyKey的粒度直接决定了可用节点范围。如果你设了topologyKey: kubernetes.io/hostname那么每个节点都是一个独立域可用范围就是全集群的节点数如果你设成topology.kubernetes.io/zone可用范围就被压缩到某个可用区。生产环境里最常见的死法就是把topologyKey设成可用区但某个区的节点数小于你的Pod副本数于是那一批Pod全Pending。排查方法就一个看事件和describe输出如果Pending原因是node(s) didnt match pod anti-affinity rules不要急着改规则先把kubectl get nodes --show-labels拉出来看看目标拓扑域里到底有几个节点满足条件。很多时候是节点数不够不是规则写错了。6.3 operator与value的配置逻辑陷阱我整理几个最容易出错的写法In的值列表里带了空格或者空字符串比如values: [A100, ]这个空值实际是匹配一个空标签值几乎没有节点能满足结果就是Pod永远调度不出去。在Exists后面还写了values这是不合法的会直接报校验错误。Gt或Lt的值写了非数字校验直接报must be an integer。把Pod的matchLabels和Node的labels混在一起在podAffinity的labelSelector里写节点标签导致永远匹配不到。这些错误在apply时就能发现但有时候因为YAML太长被忽略了导致调度器那边一直Pending事件里还看不出来原因只能去看kubectl describe pod的Events。建议写复杂亲和性配置前先kubectl explain查字段定义或者用本地校验工具提前看一下。6.4 验证手段如何确认规则真的生效配置写完后用三步验证法第一步检查Pod状态和节点分布kubectl get pods -o wide -l appapi-server如果你配的是反亲和期望看到Pod分散在不同节点上。如果都在一台机器说明规则有问题。第二步看调度事件kubectl describe pod pod-nameEvents里如果有Successfully assigned再看下面有没有调度器的亲和性提示日志。Pending的情况下Events里的原因会非常明确。第三步验证节点标签kubectl get nodes --show-labels | grep your-label拿节点标签和你的matchExpressions手动比对这一步最简单也最容易被跳过但能定位90%以上的问题。最后分享一个实战体会我在自己的集群里把亲和性功能用了两三年之后才敢说彻底摸熟。期间最大的教训是别把亲和性当万能工具。硬规则一多调度的不确定性就成倍增加排查问题的复杂度也直线上升。合理的做法是能软则软、能少则少。默认调度器的随机性本身是一种天然的资源打散能力只有在多副本高可用、强依赖业务、混部隔离这些场景里才值得引入亲和性。同时借助preferredDuringSchedulingIgnoredDuringExecution平衡收益和风险是保证调度弹性的关键。每次配置之前先反推一遍期望的拓扑结果再写规则最后用kubectl get pods -o wide去核对实际分布这套流程走熟了亲和性就是调度工具箱里一个顺手又强大的东西。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑