一、概述
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的命名要有规范,建议用
2.1.2 理解调度流程
kube-scheduler的调度流程分为两个阶段:
预选(Filtering):过滤掉不满足条件的节点,比如资源不足、nodeSelector不匹配、taint不容忍等
优选(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 调度策略优先级
多种调度策略同时存在时的生效顺序:
nodeName(最高优先级):直接指定节点名,跳过调度器
taints/tolerations:节点污点过滤,不容忍的Pod直接排除
nodeSelector:简单的标签匹配过滤
nodeAffinity:更灵活的节点亲和性规则
podAffinity/podAntiAffinity:Pod间的亲和/反亲和
topologySpreadConstraints:拓扑分散约束
资源请求:节点剩余资源是否满足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| 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 runtest-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
# 更精确的方式:通过节点标签查看
forpodin$(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先调度到特定的灰度节点,验证通过后再扩展到所有节点。通过标签和调度策略控制灰度范围。
实现步骤:
给灰度节点打标签:
kubectl label node k8s-worker-01 canary=true
灰度版本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
灰度验证通过后,全量发布:
# 移除灰度节点约束,更新稳定版Deployment的镜像 kubectlsetimage 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 --setschedule="*/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硬约束会限制副本数上限。如果topologyKey是kubernetes.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-n | grep -A 20 Events # 查看节点资源分配详情 kubectl describe node | grep -A 30"Allocated resources"
5.1.2 常见问题排查
问题一:Pod Pending,报Insufficient cpu
# 诊断命令 kubectl describe pod| grep -A 5"Events" kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU_REQ:.status.allocatable.cpu,MEM_REQ:.status.allocatable.memory # 查看各节点已分配资源 fornodein$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}');do echo"===$node===" kubectl describe node$node| grep -A 5"Allocated resources" done
解决方案:
检查Pod的resource requests是否设置过高
检查节点是否有足够的可分配资源(allocatable - 已分配)
考虑扩容节点或优化现有Pod的资源配置
问题二:调度策略不生效,Pod没有按预期分布
# 诊断命令 kubectl get pod-o yaml | grep -A 50"affinity" kubectl get pod -o yaml | grep -A 20"topologySpreadConstraints" # 检查节点标签是否正确 kubectl get nodes --show-labels | grep
解决方案:
确认YAML缩进正确,affinity字段层级关系容易写错
确认labelSelector和Pod标签一致
用kubectl apply --dry-run=server验证YAML语法
问题三:节点drain卡住不动
症状:kubectl drain命令长时间无响应
排查:
# 查看哪些Pod阻止了drain kubectl drain--ignore-daemonsets --delete-emptydir-data --dry-run=client # 检查PDB kubectl get pdb -A
解决:
DaemonSet的Pod加--ignore-daemonsets跳过
使用emptyDir的Pod加--delete-emptydir-data
PDB限制导致的,先扩容副本再drain
有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"
# 备份节点标签和污点
fornodein$(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 恢复流程
停止服务:暂停业务部署,避免恢复过程中的调度冲突
恢复数据:kubectl apply -f ${BACKUP_DIR}/priorityclasses.yaml
验证完整性:kubectl get priorityclass确认PriorityClass恢复
恢复节点配置:逐节点恢复标签和污点
验证调度:创建测试Pod验证调度策略是否生效
六、总结
6.1 技术要点回顾
要点一:nodeSelector适合简单场景,nodeAffinity适合需要软硬约束结合的场景,两者都基于节点标签,标签规划是基础
要点二:podAntiAffinity硬约束会限制副本数不超过节点数,大规模集群中计算开销大,优先用topologySpreadConstraints替代
要点三:taints/tolerations从节点角度控制调度,适合节点专用化场景(GPU节点、数据库节点),NoExecute会驱逐已有Pod
要点四:PriorityClass的抢占机制要谨慎使用,批处理任务设置preemptionPolicy: Never,核心服务设置高优先级
要点五:topologySpreadConstraints是生产环境跨故障域部署的首选方案,maxSkew: 1配合DoNotSchedule保证严格均匀分布
6.2 进阶学习方向
自定义调度器:当内置调度策略无法满足需求时,可以开发自定义调度器,通过Scheduling Framework扩展点实现
学习资源:Scheduling Framework
实践建议:先用调度器扩展(Extender)验证逻辑,再考虑写Framework插件
Descheduler深度使用:配置LowNodeUtilization、RemoveDuplicates等策略,自动重平衡集群负载
学习资源:Descheduler GitHub
实践建议:先在非生产环境测试Descheduler策略,避免误驱逐核心服务
Volcano批调度器:针对AI/大数据场景的批调度器,支持Gang Scheduling(一组Pod要么全部调度成功,要么全部不调度)
6.3 参考资料
Kubernetes调度官方文档 - 调度机制全面说明
kube-scheduler源码 - 理解调度算法实现
Descheduler项目 - Pod重调度工具
Volcano项目 - 批调度器
附录
A. 命令速查表
# 节点标签管理 kubectl label nodekey=value # 添加标签 kubectl label node key=value --overwrite # 修改标签 kubectl label node key- # 删除标签 kubectl get nodes --show-labels # 查看所有标签 kubectl get nodes -L key1,key2 # 查看指定标签列 # 污点管理 kubectl taint nodes key=value:NoSchedule # 添加污点 kubectl taint nodes key=value:NoSchedule- # 删除污点 kubectl taint nodes key- # 删除指定key的所有污点 kubectl describe node | grep Taints # 查看污点 # 调度排查 kubectl get events --field-selector reason=FailedScheduling -A # 调度失败事件 kubectl describe pod | grep -A 10 Events # Pod事件 kubectl get pods -o wide # 查看Pod所在节点 kubectl top nodes # 节点资源使用 kubectl describe node | grep -A 20"Allocated resources"# 已分配资源 # 节点维护 kubectl drain --ignore-daemonsets --delete-emptydir-data # 腾空节点 kubectl uncordon # 恢复调度 kubectl cordon # 标记不可调度
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数量 |
-
gpu
+关注
关注
28文章
5258浏览量
136039 -
数据库
+关注
关注
7文章
4078浏览量
68524 -
kubernetes
+关注
关注
0文章
273浏览量
9530
原文标题:别再让 Pod “乱跑”:Kubernetes 调度策略原理与落地指南
文章出处:【微信号:magedu-Linux,微信公众号:马哥Linux运维】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
Kubernetes的Device Plugin设计解读
阿里云容器Kubernetes监控(二) - 使用Grafana展现Pod监控数据
从零开始入门 K8s| 详解 Pod 及容器设计模式
不吹不黑,今天我们来聊一聊 Kubernetes 落地的三种方式
深入研究Kubernetes调度
Kubernetes组件pod核心原理
Kubernetes中的Pod简易理解
Kubernetes Pod如何独立工作
Kubernetes Pod如何获取IP地址呢?
配置Kubernetes中Pod使用代理的两种常见方式
Kubernetes Pod调度策略原理与落地指南
评论