K8s Pod调度策略与亲和性实战
发布日期: 2026/07/20 阅读总量: 0

一次线上雪崩:所有Pod挤在同一台机器上

2024年3月,我们一个微服务集群在晚高峰突然崩溃。排查发现:16个Pod全部调度到了同一台32核64G的节点上,其他3台节点几乎空载。这台机器CPU飙到95%,内存耗尽,OOM killer开始屠杀进程。

根因:我们用了默认调度策略,没有配置任何亲和性或反亲和性。K8s调度器只看资源水位,不会主动分散Pod。当第一个Pod调度到node1后,后续Pod因为node1资源剩余最多,继续往node1堆。

这次事故后,我花了2周时间系统研究了K8s调度策略,特别是亲和性与反亲和性。这篇文章把实战经验全写出来,包括配置、压测、踩坑。

K8s调度策略全景

K8s v1.28调度器(kube-scheduler)的调度流程分两步:

  • 过滤(Predicates):选出满足Pod资源请求的节点
  • 打分(Priorities):对过滤后的节点打分,选最高分

亲和性与反亲和性在过滤阶段生效。主要分三类:

  • nodeSelector:最简单的节点选择,匹配标签
  • nodeAffinity:节点亲和性,支持硬约束(required)和软约束(preferred)
  • podAffinity / podAntiAffinity:Pod间亲和/反亲和,控制Pod之间的分布

方案对比:3种调度策略实测

测试环境:K8s v1.28.3,4节点集群(2台4C8G,2台8C16G),部署一个Nginx服务,副本数8。

策略配置复杂度Pod分布均匀度调度耗时(ms)适用场景
默认调度差(集中)12测试环境
nodeSelector中(按标签分组)15固定节点部署
nodeAffinity + podAntiAffinity高(均匀分布)28生产环境

压测数据:用wrk压测30分钟,QPS 5000。默认调度下,单节点CPU 92%,其他节点<20%;使用亲和性策略后,4节点CPU均摊在45-55%。

完整代码实现

1. nodeSelector:简单粗暴

apiVersion: v1
kind: Pod
metadata:
  name: nginx-selector
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: nginx
    image: nginx:1.25-alpine
    resources:
      requests:
        cpu: "500m"
        memory: "512Mi"

给节点打标签:kubectl label node node1 disktype=ssd。Pod只会调度到有ssd标签的节点。

2. nodeAffinity:更灵活的节点选择

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-affinity
spec:
  replicas: 8
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/hostname
                operator: In
                values:
                - node1
                - node2
                - node3
                - node4
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 50
            preference:
              matchExpressions:
              - key: disktype
                operator: In
                values:
                - ssd
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"

requiredDuringSchedulingIgnoredDuringExecution:硬约束,Pod必须调度到指定节点。preferredDuringSchedulingIgnoredDuringExecution:软约束,尽量调度到ssd节点,权重50。

3. podAntiAffinity:解决Pod扎堆

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-anti
spec:
  replicas: 8
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx
            topologyKey: kubernetes.io/hostname
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"

topologyKey: kubernetes.io/hostname 表示按节点维度反亲和,每个节点最多一个Pod。如果副本数超过节点数,多余的Pod会Pending。

4. 组合拳:nodeAffinity + podAntiAffinity + podAffinity

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-combo
spec:
  replicas: 8
  selector:
    matchLabels:
      app: nginx
      tier: frontend
  template:
    metadata:
      labels:
        app: nginx
        tier: frontend
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: node-role.kubernetes.io/worker
                operator: Exists
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - nginx
              topologyKey: kubernetes.io/hostname
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: tier
                operator: In
                values:
                - cache
            topologyKey: kubernetes.io/hostname
      containers:
      - name: nginx
        image: nginx:1.25-alpine
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"

这个配置做了三件事:

  • 只调度到worker节点
  • 尽量让nginx Pod分散到不同节点(软约束)
  • 必须和tier=cache的Pod同节点(硬约束)

5. 用脚本验证调度结果

#!/bin/bash
# 验证Pod分布
echo "=== Pod分布情况 ==="
kubectl get pods -o wide | grep nginx-combo

echo ""
echo "=== 每个节点的Pod数 ==="
kubectl get pods -o wide | grep nginx-combo | awk '{print $7}' | sort | uniq -c | sort -rn

echo ""
echo "=== 节点资源使用 ==="
kubectl top nodes

echo ""
echo "=== 检查Pending Pod ==="
kubectl get pods | grep Pending

效果数据:压测对比

测试工具:wrk,参数:-t4 -c100 -d120s http://nginx-svc

策略平均延迟(ms)P99延迟(ms)QPS错误率
默认调度24589042002.3%
nodeSelector18052051000.5%
nodeAffinity + podAntiAffinity9521068000.0%

组合策略比默认调度QPS提升62%,P99延迟降低76%。核心原因:Pod均匀分布,没有单点瓶颈。

避坑指南(我踩过的3个坑)

坑1:topologyKey用错导致调度失败

一开始我把podAntiAffinity的topologyKey写成kubernetes.io/hostname,但集群节点标签不统一。有的节点是kubernetes.io/hostname,有的是hostname。结果Pod一直Pending。

解决方法:统一节点标签。用kubectl label node --all kubernetes.io/hostname=$(hostname)修复。或者用topologyKey: kubernetes.io/hostname,这是K8s内置标签,所有节点都有。

坑2:requiredDuringScheduling + 副本数超过节点数

配置了required的podAntiAffinity,副本数8,节点数4。结果4个Pod Running,4个Pending。因为硬约束要求每个节点最多一个Pod,节点不够了。

解决方法:用preferredDuringScheduling代替required,或者增加节点。生产环境建议用preferred,允许一定程度的集中。

坑3:podAffinity导致级联调度失败

配置了podAffinity要求与cache Pod同节点。但cache Pod先被调度到node1,nginx Pod跟着去node1。node1资源不够,nginx Pod Pending。而其他节点有资源,但因为亲和性约束,不能调度。

解决方法:用preferredDuringScheduling代替required,或者给cache Pod预留资源。更推荐用topologyKey: kubernetes.io/zone做区域亲和,而不是节点级。

原理深入:调度器如何打分

kube-scheduler v1.28的调度流程:

  • Filter阶段:检查Pod是否满足节点约束。亲和性规则在这里检查。如果required规则不满足,节点被过滤掉。
  • Score阶段:对剩余节点打分。preferred规则在这里生效,权重越高分数越高。

打分算法:

  • NodeAffinity:匹配的preferred规则权重累加
  • PodAffinity:计算节点上匹配的Pod数量,乘以权重
  • PodAntiAffinity:计算节点上匹配的Pod数量,反向打分(越多分越低)

最终得分 = 各策略得分之和。调度器选择最高分节点。

生产建议

  • 优先用preferred(软约束),避免调度失败
  • topologyKey统一用K8s内置标签:kubernetes.io/hostname、topology.kubernetes.io/zone
  • 组合使用nodeAffinity + podAntiAffinity,效果最好
  • 监控Pending Pod,及时调整策略
  • 用descheduler定期重平衡Pod分布

总结

Pod调度策略是K8s生产化必须掌握的技能。从一次雪崩事故出发,我们对比了3种方案,给出了完整YAML配置和压测数据。组合使用nodeAffinity和podAntiAffinity,QPS提升62%,P99延迟降低76%。避坑部分记录了3个真实踩坑案例,希望你能避开。