Kubernetes 调度深度解析:从原理到落地,让 Pod 稳定可控

 互联网   2026-03-03 16:51   7 人阅读  0 条评论
Kubernetes 调度深度解析:从原理到落地,让 Pod 稳定可控  第1张

Kubernetes Pod调度策略详解

一、概述

1.1 背景介绍

Pod调度是Kubernetes的核心机制之一,决定了Pod最终运行在哪个节点上。默认调度器kube-scheduler通过一系列预选(Filtering)和优选(Scoring)算法完成调度决策,但默认行为在生产环境中往往不够用。

实际场景中经常遇到的问题:数据库Pod被调度到了没有SSD的节点上,导致IO性能差;两个高负载服务的Pod被调度到同一个节点,互相抢资源;GPU节点上跑了一堆普通业务Pod,真正需要GPU的任务反而调度不上去。

这些问题都需要通过调度策略来解决。Kubernetes提供了nodeSelector、nodeAffinity、podAffinity/podAntiAffinity、taints/tolerations、topologySpreadConstraints等多种调度机制,本文逐一讲解并给出生产环境的配置方案。

1.2 技术特点

  • 多层调度控制:从简单的nodeSelector到复杂的自定义调度器,提供不同粒度的调度控制能力
  • 软硬约束结合:requiredDuringScheduling是硬约束(不满足就不调度),preferredDuringScheduling是软约束(尽量满足,不满足也能调度)
  • 拓扑感知:topologySpreadConstraints支持按zone、node、rack等拓扑域分散Pod,实现跨故障域部署
  • 抢占机制:PriorityClass支持高优先级Pod抢占低优先级Pod的资源

1.3 适用场景

  • 场景一:将数据库、缓存等IO密集型Pod调度到SSD节点,计算密集型Pod调度到高CPU节点
  • 场景二:同一服务的多个副本分散到不同节点/可用区,避免单点故障导致服务全部不可用
  • 场景三:GPU、FPGA等特殊硬件资源的独占调度,防止普通Pod占用专用资源
  • 场景四:多租户集群中,不同团队的Pod隔离到各自的节点池

1.4 环境要求

组件
版本要求
说明
Kubernetes
1.24+
topologySpreadConstraints在1.19 GA,PodSecurity在1.25 GA
kube-scheduler
与集群版本一致
自定义调度器需要单独部署
节点标签
提前规划
调度策略依赖节点标签,需要统一标签规范
metrics-server
0.6+
资源感知调度需要metrics数据

二、详细步骤

2.1 准备工作

2.1.1 节点标签规划

调度策略的基础是节点标签,先把标签体系规划好:

# 查看现有节点标签
kubectl get nodes --show-labels

# 按硬件类型打标签
kubectl label node k8s-worker-01 disktype=ssd
kubectl label node k8s-worker-02 disktype=ssd
kubectl label node k8s-worker-03 disktype=hdd

# 按业务用途打标签
kubectl label node k8s-worker-01 workload-type=database
kubectl label node k8s-worker-02 workload-type=application
kubectl label node k8s-worker-03 workload-type=application

# 按可用区打标签(如果是多机房部署)
kubectl label node k8s-worker-01 topology.kubernetes.io/zone=zone-a
kubectl label node k8s-worker-02 topology.kubernetes.io/zone=zone-b
kubectl label node k8s-worker-03 topology.kubernetes.io/zone=zone-c

# GPU节点标签
kubectl label node k8s-gpu-01 accelerator=nvidia-tesla-v100
kubectl label node k8s-gpu-02 accelerator=nvidia-tesla-a100

# 验证标签
kubectl get nodes -L disktype,workload-type,topology.kubernetes.io/zone

注意:标签key的命名要有规范,建议用<domain>/<name>格式,如company.com/team=backend。Kubernetes内置标签用kubernetes.iok8s.io前缀,自定义标签不要用这两个前缀。

2.1.2 理解调度流程

kube-scheduler的调度流程分为两个阶段:

  1. 预选(Filtering):过滤掉不满足条件的节点,比如资源不足、nodeSelector不匹配、taint不容忍等
  2. 优选(Scoring):对通过预选的节点打分,选择得分最高的节点
# 查看scheduler的调度日志(需要提高日志级别)
# 修改/etc/kubernetes/manifests/kube-scheduler.yaml
# 在command中添加 --v=4
# 然后查看日志
kubectl logs -n kube-system kube-scheduler-k8s-master-01 --tail=50

2.1.3 调度策略优先级

多种调度策略同时存在时的生效顺序:

  1. nodeName(最高优先级):直接指定节点名,跳过调度器
  2. taints/tolerations:节点污点过滤,不容忍的Pod直接排除
  3. nodeSelector:简单的标签匹配过滤
  4. nodeAffinity:更灵活的节点亲和性规则
  5. podAffinity/podAntiAffinity:Pod间的亲和/反亲和
  6. topologySpreadConstraints:拓扑分散约束
  7. 资源请求:节点剩余资源是否满足Pod的requests

2.2 核心配置

2.2.1 nodeSelector(最简单的调度约束)

nodeSelector是最基础的调度方式,通过标签键值对匹配节点:

# 文件:nginx-nodeselector.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:nginx-ssd
namespace:default
spec:
replicas:3
selector:
    matchLabels:
      app:nginx-ssd
template:
    metadata:
      labels:
        app:nginx-ssd
    spec:
      nodeSelector:
        disktype:ssd
      containers:
        -name:nginx
          image:nginx:1.24
          resources:
            requests:
              cpu:100m
              memory:128Mi
            limits:
              cpu:200m
              memory:256Mi
kubectl apply -f nginx-nodeselector.yaml

# 验证Pod只调度到了ssd节点
kubectl get pods -l app=nginx-ssd -o wide

注意:nodeSelector是硬约束,如果没有节点匹配标签,Pod会一直Pending。生产环境建议配合nodeAffinity的软约束使用。

2.2.2 nodeAffinity(节点亲和性)

nodeAffinity比nodeSelector更灵活,支持多种操作符和软硬约束:

# 文件:app-node-affinity.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:app-with-affinity
namespace:default
spec:
replicas:6
selector:
    matchLabels:
      app:app-affinity
template:
    metadata:
      labels:
        app:app-affinity
    spec:
      affinity:
        nodeAffinity:
          # 硬约束:必须调度到zone-a或zone-b
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              -matchExpressions:
                  -key:topology.kubernetes.io/zone
                    operator:In
                    values:
                      -zone-a
                      -zone-b
          # 软约束:优先调度到ssd节点,权重1-100
          preferredDuringSchedulingIgnoredDuringExecution:
            -weight:80
              preference:
                matchExpressions:
                  -key:disktype
                    operator:In
                    values:
                      -ssd
            -weight:20
              preference:
                matchExpressions:
                  -key:workload-type
                    operator:In
                    values:
                      -application
      containers:
        -name:app
          image:nginx:1.24
          resources:
            requests:
              cpu:200m
              memory:256Mi

操作符说明

  • In:标签值在列表中
  • NotIn:标签值不在列表中
  • Exists:标签存在(不关心值)
  • DoesNotExist:标签不存在
  • Gt:标签值大于指定值(仅限数字)
  • Lt:标签值小于指定值(仅限数字)

注意requiredDuringSchedulingIgnoredDuringExecution中的IgnoredDuringExecution表示Pod已经运行后,即使节点标签变了也不会驱逐Pod。Kubernetes计划实现RequiredDuringExecution但目前还没有。

2.2.3 podAffinity和podAntiAffinity(Pod间亲和/反亲和)

控制Pod之间的调度关系,典型场景:Web应用和缓存部署在同一节点减少网络延迟,同一服务的多个副本分散到不同节点。

# 文件:web-cache-affinity.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:web-frontend
namespace:default
spec:
replicas:3
selector:
    matchLabels:
      app:web-frontend
template:
    metadata:
      labels:
        app:web-frontend
    spec:
      affinity:
        # Pod亲和:和redis-cache部署在同一节点
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            -weight:100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    -key:app
                      operator:In
                      values:
                        -redis-cache
                topologyKey:kubernetes.io/hostname
        # Pod反亲和:同一服务的副本分散到不同节点
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            -labelSelector:
                matchExpressions:
                  -key:app
                    operator:In
                    values:
                      -web-frontend
              topologyKey:kubernetes.io/hostname
      containers:
        -name:web
          image:nginx:1.24
          resources:
            requests:
              cpu:200m
              memory:256Mi

说明

  • topologyKey: kubernetes.io/hostname表示以节点为拓扑域,同一节点上的Pod视为同一拓扑域
  • topologyKey: topology.kubernetes.io/zone表示以可用区为拓扑域
  • podAntiAffinity的硬约束会限制副本数不能超过节点数,3个副本至少需要3个节点

警告:podAffinity/podAntiAffinity的计算复杂度是O(N^2),N是集群中的Pod数量。在Pod数量超过5000的大集群中,大量使用podAffinity会导致调度延迟从毫秒级上升到秒级。

2.2.4 Taints和Tolerations(污点和容忍)

Taints从节点角度排斥Pod,Tolerations从Pod角度容忍污点。两者配合实现节点专用化。

# 给GPU节点添加污点,只允许GPU任务调度
kubectl taint nodes k8s-gpu-01 gpu=true:NoSchedule
kubectl taint nodes k8s-gpu-02 gpu=true:NoSchedule

# 给维护中的节点添加NoExecute污点,驱逐现有Pod
kubectl taint nodes k8s-worker-03 maintenance=true:NoExecute

# 查看节点污点
kubectl describe node k8s-gpu-01 | grep -A 5 Taints

# 删除污点
kubectl taint nodes k8s-worker-03 maintenance=true:NoExecute-

Pod中配置Tolerations:

# 文件:gpu-job.yaml
apiVersion:batch/v1
kind:Job
metadata:
name:gpu-training-job
namespace:ml-training
spec:
template:
    spec:
      tolerations:
        # 容忍gpu污点
        -key:"gpu"
          operator:"Equal"
          value:"true"
          effect:"NoSchedule"
      nodeSelector:
        accelerator:nvidia-tesla-v100
      containers:
        -name:training
          image:pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime
          resources:
            limits:
              nvidia.com/gpu:1
            requests:
              cpu:"4"
              memory:"16Gi"
      restartPolicy:Never

Taint Effect说明

  • NoSchedule:新Pod不会调度到该节点,已有Pod不受影响
  • PreferNoSchedule:尽量不调度,但资源不足时仍可调度
  • NoExecute:新Pod不调度,已有Pod如果不容忍会被驱逐。可以设置tolerationSeconds指定驱逐前的等待时间
# 容忍NoExecute污点,但最多等待300秒后被驱逐
tolerations:
  - key: "maintenance"
    operator: "Equal"
    value: "true"
    effect: "NoExecute"
    tolerationSeconds: 300

2.2.5 topologySpreadConstraints(拓扑分散约束)

1.19版本GA的功能,比podAntiAffinity更精细地控制Pod在拓扑域间的分布:

# 文件:app-topology-spread.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:app-spread
namespace:default
spec:
replicas:9
selector:
    matchLabels:
      app:app-spread
template:
    metadata:
      labels:
        app:app-spread
    spec:
      topologySpreadConstraints:
        # 跨可用区均匀分布,最大偏差1
        -maxSkew:1
          topologyKey:topology.kubernetes.io/zone
          whenUnsatisfiable:DoNotSchedule
          labelSelector:
            matchLabels:
              app:app-spread
        # 跨节点均匀分布,最大偏差1,软约束
        -maxSkew:1
          topologyKey:kubernetes.io/hostname
          whenUnsatisfiable:ScheduleAnyway
          labelSelector:
            matchLabels:
              app:app-spread
      containers:
        -name:app
          image:nginx:1.24
          resources:
            requests:
              cpu:100m
              memory:128Mi

参数说明

  • maxSkew:拓扑域间Pod数量的最大差值。设为1表示任意两个域的Pod数量差不超过1
  • topologyKey:拓扑域的标签key
  • whenUnsatisfiable:不满足约束时的行为,DoNotSchedule(硬约束)或ScheduleAnyway(软约束)

9个副本在3个zone中的分布结果:zone-a=3, zone-b=3, zone-c=3。如果zone-c只有1个节点且资源不足,DoNotSchedule会导致部分Pod Pending,ScheduleAnyway则会尽量均匀但允许偏差。

2.2.6 PriorityClass(优先级和抢占)

高优先级Pod可以抢占低优先级Pod的资源:

# 定义优先级类
apiVersion:scheduling.k8s.io/v1
kind:PriorityClass
metadata:
name:critical-production
value:1000000
globalDefault:false
preemptionPolicy:PreemptLowerPriority
description:"生产核心服务,可抢占低优先级Pod"
---
apiVersion:scheduling.k8s.io/v1
kind:PriorityClass
metadata:
name:normal-production
value:500000
globalDefault:true
preemptionPolicy:PreemptLowerPriority
description:"普通生产服务"
---
apiVersion:scheduling.k8s.io/v1
kind:PriorityClass
metadata:
name:batch-job
value:100000
globalDefault:false
preemptionPolicy:Never
description:"批处理任务,不抢占其他Pod"

在Pod中引用:

apiVersion: apps/v1
kind:Deployment
metadata:
name:core-api
spec:
replicas:3
selector:
    matchLabels:
      app:core-api
template:
    metadata:
      labels:
        app:core-api
    spec:
      priorityClassName:critical-production
      containers:
        -name:api
          image:myapp:v1.0
          resources:
            requests:
              cpu:"1"
              memory:"2Gi"

警告preemptionPolicy: PreemptLowerPriority会驱逐低优先级Pod来腾出资源,被驱逐的Pod会收到SIGTERM信号。确保应用能正确处理优雅关闭,否则会丢数据。生产环境建议批处理任务设置preemptionPolicy: Never

2.3 启动和验证

2.3.1 验证调度结果

# 查看Pod调度到了哪个节点
kubectl get pods -o wide -l app=app-spread

# 查看Pod的调度事件
kubectl describe pod <pod-name> | grep -A 10 Events

# 查看调度失败的原因
kubectl get events --field-selector reason=FailedScheduling -A

# 查看节点资源分配情况
kubectl describe node k8s-worker-01 | grep -A 20 "Allocated resources"

2.3.2 调度模拟测试

# 使用kubectl创建一个dry-run的Pod,查看是否能调度成功
kubectl run test-schedule --image=nginx:1.24 --dry-run=server -o yaml \
  --overrides='{
    "spec": {
      "nodeSelector": {"disktype": "ssd"},
      "containers": [{"name": "test", "image": "nginx:1.24", "resources": {"requests": {"cpu": "100m", "memory": "128Mi"}}}]
    }
  }'


# 查看各节点的可分配资源
kubectl get nodes -o custom-columns=\
NAME:.metadata.name,\
CPU_ALLOC:.status.allocatable.cpu,\
MEM_ALLOC:.status.allocatable.memory,\
PODS_ALLOC:.status.allocatable.pods

2.3.3 验证拓扑分散

# 查看Pod在各zone的分布
kubectl get pods -l app=app-spread -o custom-columns=\
NAME:.metadata.name,\
NODE:.spec.nodeName,\
ZONE:.spec.nodeName

# 更精确的方式:通过节点标签查看
for pod in $(kubectl get pods -l app=app-spread -o jsonpath='{.items[*].spec.nodeName}'); do
  zone=$(kubectl get node $pod -o jsonpath='{.metadata.labels.topology\.kubernetes\.io/zone}')
  echo "Node: $pod, Zone: $zone"
done

三、示例代码和配置

3.1 完整配置示例

3.1.1 多租户节点池隔离方案

生产环境中不同团队共用一个集群,通过taint+nodeAffinity实现节点池隔离:

# 文件:namespace-resource-setup.yaml
# 第一步:创建团队namespace
apiVersion:v1
kind:Namespace
metadata:
name:team-backend
labels:
    team:backend
---
apiVersion:v1
kind:Namespace
metadata:
name:team-data
labels:
    team:data
---
# 第二步:为每个团队设置ResourceQuota
apiVersion:v1
kind:ResourceQuota
metadata:
name:backend-quota
namespace:team-backend
spec:
hard:
    requests.cpu:"20"
    requests.memory:40Gi
    limits.cpu:"40"
    limits.memory:80Gi
    pods:"100"
---
apiVersion:v1
kind:ResourceQuota
metadata:
name:data-quota
namespace:team-data
spec:
hard:
    requests.cpu:"40"
    requests.memory:80Gi
    limits.cpu:"80"
    limits.memory:160Gi
    pods:"200"

节点打标签和污点:

# backend团队节点池
kubectl label node k8s-worker-{01..05} node-pool=backend
kubectl taint nodes k8s-worker-{01..05} node-pool=backend:NoSchedule

# data团队节点池
kubectl label node k8s-worker-{06..10} node-pool=data
kubectl taint nodes k8s-worker-{06..10} node-pool=data:NoSchedule

# 公共节点池(不加污点,所有Pod都能调度)
kubectl label node k8s-worker-{11..15} node-pool=shared

团队Deployment模板:

# 文件:backend-app-template.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:backend-api
namespace:team-backend
spec:
replicas:5
selector:
    matchLabels:
      app:backend-api
template:
    metadata:
      labels:
        app:backend-api
    spec:
      tolerations:
        -key:"node-pool"
          operator:"Equal"
          value:"backend"
          effect:"NoSchedule"
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              -matchExpressions:
                  -key:node-pool
                    operator:In
                    values:
                      -backend
                      -shared
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            -weight:100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    -key:app
                      operator:In
                      values:
                        -backend-api
                topologyKey:kubernetes.io/hostname
      containers:
        -name:api
          image:backend-api:v2.1.0
          ports:
            -containerPort:8080
          resources:
            requests:
              cpu:500m
              memory:512Mi
            limits:
              cpu:"1"
              memory:1Gi
          readinessProbe:
            httpGet:
              path:/health
              port:8080
            initialDelaySeconds:10
            periodSeconds:5
          livenessProbe:
            httpGet:
              path:/health
              port:8080
            initialDelaySeconds:30
            periodSeconds:10

3.1.2 调度策略自动注入脚本

通过Kyverno策略自动为特定namespace的Pod注入调度规则,避免每个Deployment都手动配置:

# 文件:kyverno-scheduling-policy.yaml
apiVersion:kyverno.io/v1
kind:ClusterPolicy
metadata:
name:inject-node-affinity-backend
spec:
rules:
    -name:add-backend-scheduling
      match:
        any:
          -resources:
              kinds:
                -Pod
              namespaces:
                -team-backend
      mutate:
        patchStrategicMerge:
          spec:
            tolerations:
              -key:"node-pool"
                operator:"Equal"
                value:"backend"
                effect:"NoSchedule"
            affinity:
              nodeAffinity:
                requiredDuringSchedulingIgnoredDuringExecution:
                  nodeSelectorTerms:
                    -matchExpressions:
                        -key:node-pool
                          operator:In
                          values:
                            -backend
                            -shared

注意:Kyverno需要单独安装(helm install kyverno kyverno/kyverno -n kyverno --create-namespace)。这种方式比在每个Deployment里写调度规则更易维护,团队只需要关注业务配置,调度策略由平台团队统一管理。

3.2 实际应用案例

案例一:数据库Pod的调度策略

场景描述:MySQL主从集群部署在K8s中,主库需要SSD+高内存节点,从库可以用普通节点。主从Pod不能在同一节点上,避免节点故障导致主从同时不可用。

实现代码

# 文件:mysql-master-statefulset.yaml
apiVersion:apps/v1
kind:StatefulSet
metadata:
name:mysql-master
namespace:database
spec:
serviceName:mysql-master
replicas:1
selector:
    matchLabels:
      app:mysql
      role:master
template:
    metadata:
      labels:
        app:mysql
        role:master
    spec:
      priorityClassName:critical-production
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              -matchExpressions:
                  -key:disktype
                    operator:In
                    values:
                      -ssd
                  -key:workload-type
                    operator:In
                    values:
                      -database
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            -labelSelector:
                matchExpressions:
                  -key:app
                    operator:In
                    values:
                      -mysql
              topologyKey:kubernetes.io/hostname
      containers:
        -name:mysql
          image:mysql:8.0.35
          ports:
            -containerPort:3306
          env:
            -name:MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name:mysql-secret
                  key:root-password
          resources:
            requests:
              cpu:"2"
              memory:4Gi
            limits:
              cpu:"4"
              memory:8Gi
          volumeMounts:
            -name:mysql-data
              mountPath:/var/lib/mysql
volumeClaimTemplates:
    -metadata:
        name:mysql-data
      spec:
        accessModes:["ReadWriteOnce"]
        storageClassName:local-ssd
        resources:
          requests:
            storage:100Gi
---
# MySQL从库
apiVersion:apps/v1
kind:StatefulSet
metadata:
name:mysql-slave
namespace:database
spec:
serviceName:mysql-slave
replicas:2
selector:
    matchLabels:
      app:mysql
      role:slave
template:
    metadata:
      labels:
        app:mysql
        role:slave
    spec:
      priorityClassName:normal-production
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            -labelSelector:
                matchExpressions:
                  -key:app
                    operator:In
                    values:
                      -mysql
              topologyKey:kubernetes.io/hostname
      topologySpreadConstraints:
        -maxSkew:1
          topologyKey:topology.kubernetes.io/zone
          whenUnsatisfiable:DoNotSchedule
          labelSelector:
            matchLabels:
              app:mysql
              role:slave
      containers:
        -name:mysql
          image:mysql:8.0.35
          ports:
            -containerPort:3306
          resources:
            requests:
              cpu:"1"
              memory:2Gi
            limits:
              cpu:"2"
              memory:4Gi
          volumeMounts:
            -name:mysql-data
              mountPath:/var/lib/mysql
volumeClaimTemplates:
    -metadata:
        name:mysql-data
      spec:
        accessModes:["ReadWriteOnce"]
        storageClassName:standard
        resources:
          requests:
            storage:100Gi

运行结果

NAME             READY   STATUS    NODE            ZONE
mysql-master-0   1/1     Running   k8s-worker-01   zone-a    (SSD+database节点)
mysql-slave-0    1/1     Running   k8s-worker-02   zone-b    (不同节点不同zone)
mysql-slave-1    1/1     Running   k8s-worker-03   zone-c    (不同节点不同zone)

案例二:混合调度策略实现灰度发布

场景描述:灰度发布时,新版本Pod先调度到特定的灰度节点,验证通过后再扩展到所有节点。通过标签和调度策略控制灰度范围。

实现步骤

  1. 给灰度节点打标签:
kubectl label node k8s-worker-01 canary=true
  1. 灰度版本Deployment:
# 文件:app-canary.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:myapp-canary
namespace:production
spec:
replicas:2
selector:
    matchLabels:
      app:myapp
      version:canary
template:
    metadata:
      labels:
        app:myapp
        version:canary
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              -matchExpressions:
                  -key:canary
                    operator:In
                    values:
                      -"true"
      containers:
        -name:myapp
          image:myapp:v2.0.0-rc1
          ports:
            -containerPort:8080
          resources:
            requests:
              cpu:200m
              memory:256Mi
  1. 灰度验证通过后,全量发布:
# 移除灰度节点约束,更新稳定版Deployment的镜像
kubectl set image deployment/myapp-stable myapp=myapp:v2.0.0 -n production

# 缩容灰度Deployment
kubectl scale deployment/myapp-canary --replicas=0 -n production

# 清理灰度标签
kubectl label node k8s-worker-01 canary-

四、最佳实践和注意事项

4.1 最佳实践

4.1.1 性能优化

  • 减少podAffinity的使用范围:podAffinity/podAntiAffinity的调度计算复杂度高,在500+节点集群中,一个带podAffinity的Pod调度耗时从5ms增加到200ms。能用topologySpreadConstraints替代的场景优先用topologySpreadConstraints。

    # 不推荐:用podAntiAffinity实现分散
    podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
        -labelSelector:
            matchLabels:
              app:myapp
          topologyKey:kubernetes.io/hostname

    # 推荐:用topologySpreadConstraints替代
    topologySpreadConstraints:
    -maxSkew:1
        topologyKey:kubernetes.io/hostname
        whenUnsatisfiable:DoNotSchedule
        labelSelector:
          matchLabels:
            app:myapp
  • 合理设置resource requests:调度器根据requests而非limits做调度决策。requests设太高导致节点利用率低(实测平均CPU利用率只有15%),设太低导致节点超卖严重,Pod被OOMKill。建议requests设为实际使用量的P95值。

    # 查看Pod实际资源使用,作为requests设置参考
    kubectl top pods -n production --sort-by=cpu
    kubectl top pods -n production --sort-by=memory
  • 使用Descheduler重平衡:节点扩容或Pod漂移后,集群负载可能不均衡。Descheduler可以驱逐不符合当前调度策略的Pod,让调度器重新调度。

    # 安装Descheduler
    helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
    helm install descheduler descheduler/descheduler -n kube-system \
      --set schedule="*/5 * * * *"

4.1.2 安全加固

  • 限制nodeName直接指定:nodeName会跳过调度器的所有检查(包括资源检查),生产环境通过RBAC限制普通用户使用nodeName字段。

    # OPA/Gatekeeper策略:禁止使用nodeName
    apiVersion:constraints.gatekeeper.sh/v1beta1
    kind:K8sDenyNodeName
    metadata:
    name:deny-nodename
    spec:
    match:
        kinds:
          -apiGroups:[""]
            kinds:["Pod"]
        excludedNamespaces:["kube-system"]
  • PriorityClass权限控制:高优先级PriorityClass的创建和使用需要限制,防止普通用户创建高优先级Pod抢占核心服务资源。

    # RBAC:只允许admin使用critical-production优先级
    apiVersion:rbac.authorization.k8s.io/v1
    kind:ClusterRole
    metadata:
    name:use-critical-priority
    rules:
    -apiGroups:["scheduling.k8s.io"]
        resources:["priorityclasses"]
        resourceNames:["critical-production"]
        verbs:["get","list"]
  • 节点污点防篡改:关键节点(如GPU节点)的污点被误删会导致普通Pod涌入。通过准入控制webhook拦截对特定节点taint的修改操作。

4.1.3 高可用配置

  • HA方案一:核心服务至少3副本,配合podAntiAffinity硬约束分散到不同节点,再用topologySpreadConstraints分散到不同可用区

  • HA方案二:使用PodDisruptionBudget(PDB)限制同时不可用的Pod数量,防止节点维护时服务中断

    apiVersion: policy/v1
    kind:PodDisruptionBudget
    metadata:
    name:myapp-pdb
    spec:
    minAvailable:2
    selector:
        matchLabels:
          app:myapp
  • 备份策略:调度策略配置(PriorityClass、节点标签、污点)纳入GitOps管理,用ArgoCD或FluxCD同步

4.2 注意事项

4.2.1 配置注意事项

警告:调度策略配置错误可能导致Pod无法调度或调度到错误节点,修改前在测试环境验证。

  • 注意 nodeAffinity的requiredDuringSchedulingIgnoredDuringExecution中,多个nodeSelectorTerms之间是OR关系,同一个nodeSelectorTerm中的多个matchExpressions之间是AND关系。搞混了会导致调度结果不符合预期。
  • 注意 podAntiAffinity硬约束会限制副本数上限。如果topologyKeykubernetes.io/hostname,副本数不能超过可用节点数,否则多出来的Pod永远Pending。
  • 注意 topologySpreadConstraints的labelSelector必须和Pod自身的标签匹配,否则约束不生效。这个错误很隐蔽,Pod能正常调度但分布不均匀。

4.2.2 常见错误

错误现象
原因分析
解决方案
Pod一直Pending,事件显示0/6 nodes are available
nodeSelector或nodeAffinity没有匹配的节点
检查节点标签是否正确:kubectl get nodes --show-labels
Pod调度成功但分布不均匀
topologySpreadConstraints的labelSelector写错
确认labelSelector和Pod的labels完全一致
高优先级Pod抢占后,被抢占的Pod无法重新调度
集群资源不足,被抢占的Pod也找不到合适节点
扩容节点或降低resource requests
taint添加后已有Pod没被驱逐
使用了NoSchedule而非NoExecute
NoSchedule只影响新Pod,要驱逐已有Pod用NoExecute
节点维护drain失败
Pod有PDB限制,minAvailable不满足
先扩容副本数,或临时调整PDB的minAvailable

4.2.3 兼容性问题

  • 版本兼容:topologySpreadConstraints在1.18 Beta、1.19 GA;minDomains字段在1.25 Beta,使用前确认集群版本
  • 平台兼容:云厂商托管K8s通常自动设置topology.kubernetes.io/zone标签,自建集群需要手动设置
  • 组件依赖:Descheduler版本需要和K8s版本匹配,0.27.x支持K8s 1.25-1.28

五、故障排查和监控

5.1 故障排查

5.1.1 日志查看

# 查看kube-scheduler日志
kubectl logs -n kube-system -l component=kube-scheduler --tail=100

# 查看调度事件
kubectl get events -A --field-selector reason=FailedScheduling --sort-by='.lastTimestamp'

# 查看特定Pod的调度事件
kubectl describe pod <pod-name> -n <namespace> | grep -A 20 Events

# 查看节点资源分配详情
kubectl describe node <node-name> | grep -A 30 "Allocated resources"

5.1.2 常见问题排查

问题一:Pod Pending,报Insufficient cpu

# 诊断命令
kubectl describe pod <pod-name> | grep -A 5 "Events"
kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU_REQ:.status.allocatable.cpu,MEM_REQ:.status.allocatable.memory

# 查看各节点已分配资源
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
  echo "=== $node ==="
  kubectl describe node $node | grep -A 5 "Allocated resources"
done

解决方案

  1. 检查Pod的resource requests是否设置过高
  2. 检查节点是否有足够的可分配资源(allocatable - 已分配)
  3. 考虑扩容节点或优化现有Pod的资源配置

问题二:调度策略不生效,Pod没有按预期分布

# 诊断命令
kubectl get pod <pod-name> -o yaml | grep -A 50 "affinity"
kubectl get pod <pod-name> -o yaml | grep -A 20 "topologySpreadConstraints"

# 检查节点标签是否正确
kubectl get nodes --show-labels | grep <expected-label>

解决方案

  • 确认YAML缩进正确,affinity字段层级关系容易写错
  • 确认labelSelector和Pod标签一致
  • kubectl apply --dry-run=server验证YAML语法

问题三:节点drain卡住不动

  • 症状kubectl drain命令长时间无响应

  • 排查

    # 查看哪些Pod阻止了drain
    kubectl drain <node> --ignore-daemonsets --delete-emptydir-data --dry-run=client

    # 检查PDB
    kubectl get pdb -A
  • 解决

    1. DaemonSet的Pod加--ignore-daemonsets跳过
    2. 使用emptyDir的Pod加--delete-emptydir-data
    3. PDB限制导致的,先扩容副本再drain
    4. 有Pod设置了terminationGracePeriodSeconds很长,加--timeout=300s限制等待时间

5.1.3 调试模式

# 提高scheduler日志级别
# 编辑/etc/kubernetes/manifests/kube-scheduler.yaml
# 在command中添加 --v=10(最详细,仅调试用)

# 查看scheduler的调度决策过程
kubectl logs -n kube-system kube-scheduler-k8s-master-01 | grep "pod-name"

# 使用kubectl-scheduler-simulator模拟调度(需要单独安装)
# https://github.com/kubernetes-sigs/kube-scheduler-simulator

# 查看scheduler的metrics
kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes

5.2 性能监控

5.2.1 关键指标监控

# 调度延迟
kubectl get --raw /metrics | grep scheduler_scheduling_algorithm_duration_seconds

# 调度队列长度
kubectl get --raw /metrics | grep scheduler_pending_pods

# 抢占次数
kubectl get --raw /metrics | grep scheduler_preemption_victims

# 节点资源使用率
kubectl top nodes

5.2.2 监控指标说明

指标名称
正常范围
告警阈值
说明
调度延迟(P99)
<100ms
>500ms
超过500ms说明调度器过载或策略过于复杂
Pending Pod数量
0
>10持续5分钟
持续有Pod无法调度需要排查
调度失败率
<1%
>5%
高失败率说明资源不足或策略配置有问题
节点CPU分配率
40%-70%
>85%
分配率过高会导致新Pod无法调度
节点内存分配率
50%-80%
>90%
接近100%时需要扩容
抢占事件数
0
>5次/小时
频繁抢占说明资源规划不合理

5.2.3 Prometheus监控规则

# 文件:scheduler-alerts.yaml
apiVersion:monitoring.coreos.com/v1
kind:PrometheusRule
metadata:
name:scheduler-alerts
namespace:monitoring
spec:
groups:
    -name:kube-scheduler
      rules:
        -alert:SchedulerHighLatency
          expr:|
            histogram_quantile(0.99,
              sum(rate(scheduler_scheduling_algorithm_duration_seconds_bucket[5m])) by (le)
            ) > 0.5
          for:10m
          labels:
            severity:warning
          annotations:
            summary:"Scheduler P99 latency exceeds 500ms"

        -alert:PodsPendingTooLong
          expr:|
            sum(scheduler_pending_pods{queue="active"}) > 10
          for:5m
          labels:
            severity:warning
          annotations:
            summary:"More than 10 pods pending for over 5 minutes"

        -alert:SchedulerUnhealthy
          expr:absent(up{job="kube-scheduler"}==1)
          for:3m
          labels:
            severity:critical
          annotations:
            summary:"kube-scheduler is not running"

        -alert:NodeHighAllocation
          expr:|
            (1 - sum(kube_node_status_allocatable{resource="cpu"} - kube_pod_container_resource_requests{resource="cpu"}) by (node)
            / sum(kube_node_status_allocatable{resource="cpu"}) by (node)) > 0.85
          for:10m
          labels:
            severity:warning
          annotations:
            summary:"Node {{ $labels.node }} CPU allocation exceeds 85%"

        -alert:FrequentPreemption
          expr:|
            increase(scheduler_preemption_victims[1h]) > 5
          for:5m
          labels:
            severity:warning
          annotations:
            summary:"More than 5 preemption events in the last hour"

5.3 备份与恢复

5.3.1 备份策略

#!/bin/bash
# 调度策略配置备份脚本
# 文件:/opt/scripts/scheduling-config-backup.sh

set -euo pipefail

BACKUP_DIR="/data/scheduling-backup/$(date +%Y%m%d)"
mkdir -p "${BACKUP_DIR}"

# 备份PriorityClass
kubectl get priorityclass -o yaml > "${BACKUP_DIR}/priorityclasses.yaml"

# 备份PDB
kubectl get pdb -A -o yaml > "${BACKUP_DIR}/pdbs.yaml"

# 备份节点标签和污点
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
  kubectl get node "$node" -o jsonpath='{.metadata.labels}' > "${BACKUP_DIR}/${node}-labels.json"
  kubectl get node "$node" -o jsonpath='{.spec.taints}' > "${BACKUP_DIR}/${node}-taints.json"
done

# 备份Kyverno策略(如果使用)
kubectl get clusterpolicy -o yaml > "${BACKUP_DIR}/kyverno-policies.yaml" 2>/dev/null || true

echo"[$(date)] Scheduling config backup completed: ${BACKUP_DIR}"

5.3.2 恢复流程

  1. 停止服务:暂停业务部署,避免恢复过程中的调度冲突
  2. 恢复数据kubectl apply -f ${BACKUP_DIR}/priorityclasses.yaml
  3. 验证完整性kubectl get priorityclass确认PriorityClass恢复
  4. 恢复节点配置:逐节点恢复标签和污点
  5. 验证调度:创建测试Pod验证调度策略是否生效

六、总结

6.1 技术要点回顾

  • 要点一:nodeSelector适合简单场景,nodeAffinity适合需要软硬约束结合的场景,两者都基于节点标签,标签规划是基础
  • 要点二:podAntiAffinity硬约束会限制副本数不超过节点数,大规模集群中计算开销大,优先用topologySpreadConstraints替代
  • 要点三:taints/tolerations从节点角度控制调度,适合节点专用化场景(GPU节点、数据库节点),NoExecute会驱逐已有Pod
  • 要点四:PriorityClass的抢占机制要谨慎使用,批处理任务设置preemptionPolicy: Never,核心服务设置高优先级
  • 要点五:topologySpreadConstraints是生产环境跨故障域部署的首选方案,maxSkew: 1配合DoNotSchedule保证严格均匀分布

6.2 进阶学习方向

  1. 自定义调度器:当内置调度策略无法满足需求时,可以开发自定义调度器,通过Scheduling Framework扩展点实现

    • 学习资源:Scheduling Framework
    • 实践建议:先用调度器扩展(Extender)验证逻辑,再考虑写Framework插件
  2. Descheduler深度使用:配置LowNodeUtilization、RemoveDuplicates等策略,自动重平衡集群负载

    • 学习资源:Descheduler GitHub
    • 实践建议:先在非生产环境测试Descheduler策略,避免误驱逐核心服务
  3. Volcano批调度器:针对AI/大数据场景的批调度器,支持Gang Scheduling(一组Pod要么全部调度成功,要么全部不调度)

6.3 参考资料

  • Kubernetes调度官方文档 - 调度机制全面说明
  • kube-scheduler源码 - 理解调度算法实现
  • Descheduler项目 - Pod重调度工具
  • Volcano项目 - 批调度器

附录

A. 命令速查表

# 节点标签管理
kubectl label node <node> key=value              # 添加标签
kubectl label node <node> key=value --overwrite   # 修改标签
kubectl label node <node> key-                    # 删除标签
kubectl get nodes --show-labels                   # 查看所有标签
kubectl get nodes -L key1,key2                    # 查看指定标签列

# 污点管理
kubectl taint nodes <node> key=value:NoSchedule   # 添加污点
kubectl taint nodes <node> key=value:NoSchedule-  # 删除污点
kubectl taint nodes <node> key-                   # 删除指定key的所有污点
kubectl describe node <node> | grep Taints        # 查看污点

# 调度排查
kubectl get events --field-selector reason=FailedScheduling -A  # 调度失败事件
kubectl describe pod <pod> | grep -A 10 Events    # Pod事件
kubectl get pods -o wide                          # 查看Pod所在节点
kubectl top nodes                                 # 节点资源使用
kubectl describe node <node> | grep -A 20 "Allocated resources"# 已分配资源

# 节点维护
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data  # 腾空节点
kubectl uncordon <node>                           # 恢复调度
kubectl cordon <node>                             # 标记不可调度

B. 配置参数详解

nodeAffinity操作符

操作符
含义
示例
In
值在列表中
values: ["ssd", "nvme"]
NotIn
值不在列表中
values: ["hdd"]
Exists
标签存在
不需要values字段
DoesNotExist
标签不存在
不需要values字段
Gt
值大于
values: ["100"]
(数字字符串)
Lt
值小于
values: ["50"]
(数字字符串)

Taint Effect对比

Effect
对新Pod
对已有Pod
适用场景
NoSchedule
不调度
不影响
节点专用化
PreferNoSchedule
尽量不调度
不影响
软限制
NoExecute
不调度
驱逐
节点维护、故障隔离

topologySpreadConstraints参数

参数
类型
说明
maxSkew
int
拓扑域间Pod数量最大差值,1表示严格均匀
topologyKey
string
拓扑域标签key
whenUnsatisfiable
string
DoNotSchedule
硬约束 / ScheduleAnyway软约束
labelSelector
object
匹配Pod的标签选择器
minDomains
int
最少拓扑域数量(1.25 Beta)
matchLabelKeys
[]string
用于区分不同版本的Pod(1.27 Beta)

C. 术语表

术语
英文
解释
预选
Filtering
调度第一阶段,过滤不满足条件的节点
优选
Scoring
调度第二阶段,对候选节点打分排序
亲和性
Affinity
Pod倾向于调度到满足条件的节点或Pod附近
反亲和性
Anti-Affinity
Pod倾向于远离满足条件的Pod
污点
Taint
节点上的排斥标记,阻止不容忍的Pod调度
容忍
Toleration
Pod上的声明,表示可以容忍节点的污点
拓扑域
Topology Domain
按标签划分的节点组,如同一可用区的节点
抢占
Preemption
高优先级Pod驱逐低优先级Pod以获取资源
PDB
PodDisruptionBudget
Pod中断预算,限制同时不可用的Pod数量

(版权归原作者所有,侵删)


免责声明:本文内容来源于网络,所载内容仅供参考。转载仅为学习和交流之目的,如无意中侵犯您的合法权益,请及时联系Docker中文社区!


Kubernetes 调度深度解析:从原理到落地,让 Pod 稳定可控  第2张
本文地址:https://kubernetes.top/?id=455
温馨提示:文章内容系作者个人观点,不代表Docker中文对观点赞同或支持。
版权声明:本文为转载文章,来源于 互联网 ,版权归原作者所有,欢迎分享本文,转载请保留出处!

 发表评论


表情

还没有留言,还不快点抢沙发?