1. 这不是“调度器说明书”而是一份真实跑过200生产Pod的调度实战手记kubernetes scheduler这个词在K8s学习路径里常被简化为“负责把Pod分配到Node上”的黑盒模块。但如果你真在某公司核心业务集群里亲手调过调度策略、改过亲和性规则、排查过Pod卡在Pending状态超过4小时的问题就会发现——它根本不是个被动执行者而是整个集群资源流动的“交通指挥中心”。我参与过的几个中等规模集群节点数80~150日均调度量3万有近7成的资源利用率瓶颈、服务启动延迟、跨AZ流量激增问题最终都回溯到了scheduler配置或自定义调度器行为上。这篇笔记不讲API文档里抄来的定义只记录我在真实环境里拆解scheduler的全过程它到底在每毫秒内做了什么判断为什么一个toleration写错会导致整组StatefulSet永远起不来当默认调度器开始“挑肥拣瘦”拒绝调度时你该看哪三行日志如何用不到20行代码验证一个自定义调度器是否真的接管了Pod本文所有结论均来自某金融级中间件平台集群的灰度验证所有参数值、日志片段、YAML片段均可直接复现。适合已能部署集群、写过Deployment但对调度行为“知其然不知其所以然”的进阶使用者也适合正被Pending Pod折磨得睡不着觉的SRE同学。2. 调度器不是“选节点”而是执行一套带权重的多阶段过滤与打分流水线2.1 默认调度器的本质一个可插拔的两阶段决策引擎很多人误以为scheduler是“遍历所有Node找一个空闲的”。这是对它的最大误解。Kubernetes默认调度器kube-scheduler实际执行的是过滤Filtering→ 打分Scoring→ 选择Select的三段式流程且每个阶段都支持插件化扩展。它不关心“哪个Node最空”而是在满足硬性约束的前提下选出“综合得分最高”的那个Node。这个设计决定了哪怕所有Node内存剩余量相同调度结果也可能完全不同——因为CPU负载、拓扑分布、自定义标签权重都在影响最终排序。以一个典型Pod调度为例scheduler收到创建请求后会按顺序执行以下动作预选Predicates阶段对集群中每个Node执行一系列布尔判断。任一判断失败该Node即被剔除出候选池。例如PodFitsResources检查Node剩余CPU/Memory是否满足Pod requestsNoDiskConflict确保同一Node上无其他Pod声明了冲突的hostPath卷MatchNodeSelector验证Node labels是否匹配Pod的nodeSelectorCheckNodeMemoryPressure若Node处于内存压力状态拒绝调度除非Pod有priorityClassName。提示预选阶段是“全有或全无”没有中间态。一旦某个Node在PodFitsResources失败后续所有预选规则都不会再对该Node执行——这是性能优化的关键也是排查Pending Pod的第一突破口。优选Priorities阶段对通过预选的Node集合逐项计算分数0~10分。分数越高被选中的概率越大。例如LeastRequestedPriority剩余资源越多分数越高CPU和Memory分别计算后加权BalancedResourceAllocation偏好CPU/Memory使用率更均衡的Node避免“CPU空闲但内存满或反之”的偏科NodeNodeAffinityPriority根据Pod的affinity.nodeAffinity规则匹配程度打分TaintTolerationPriority容忍污点的Pod在对应污点Node上获得额外加分。选择Select阶段将所有Node的各优先级分数按权重相加取总分最高者。若最高分Node有多个则随机选取一个避免热点集中。这个流程不是理论模型而是真实代码逻辑。在Kubernetes v1.26版本中你可以在pkg/scheduler/framework/plugins目录下看到每个插件的具体实现。比如pkg/scheduler/framework/plugins/noderesources/fit.go里Fit方法就是PodFitsResources预选规则的核心——它会精确计算Node当前allocatable资源减去所有已调度Pod的requests之和再与待调度Pod的requests比较。注意这里用的是requests不是limits这解释了为什么一个Pod limits设为8Gi但requests仅500Mi却可能因Node剩余allocatable内存500Mi而被拒绝——调度器只认requestslimits只是运行时上限。2.2 为什么你的Pod卡在Pending90%的问题出在这三个环节在某次支付网关集群升级后我们观察到大量订单处理Pod持续处于Pending状态kubectl get pods显示0/1 nodes are available。常规思路是查Node资源但kubectl describe node显示所有Node CPU/Memory usage均低于30%。问题根源不在资源水位而在预选阶段的隐性规则。我们通过启用scheduler详细日志定位到关键线索# 在kube-scheduler启动参数中添加 --v4 # 日志级别调至4输出预选/优选详情日志中反复出现msg:Failed to find a fit for pod pod:payment-gateway-7c8f9b4d5-2xq9p reason:Node didnt have enough resource: memory, requested: 512Mi, used: 15.8Gi, capacity: 16Gi表面看是内存不足但kubectl describe node显示used: 15.8Gi, capacity: 16Gi剩余仅200Mi。这200Mi哪里去了答案是系统预留资源system reserved和kubelet预留kube-reserved未被计入allocatable但被scheduler严格扣除。查看Node allocatablekubectl get node node-name -o wide # 输出中Allocatable字段显示 memory: 15360Mi即15Gi而非capacity的16Giallocatable capacity - system-reserved - kube-reserved - eviction-threshold其中eviction-threshold驱逐阈值默认为100Mi。这意味着即使Node物理内存还有200Mischeduler可用的allocatable内存只有15Gi。而Pod requests为512Mi15Gi - 已用requests 512Mi时预选即失败。实操心得排查Pending Pod第一步永远不是kubectl top node而是kubectl get node name -o yaml | grep -A 5 allocatable。确认allocatable是否真有余量。第二步用kubectl describe pod pending-pod看Events里明确的拒绝原因如0/3 nodes are available: 2 Insufficient memory, 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didnt tolerate.。第三步开启--v4日志定位具体在哪一预选规则失败——这比猜快十倍。2.3 插件化架构从“开箱即用”到“按需裁剪”的底层逻辑Kubernetes v1.19起scheduler正式进入Framework时代所有调度逻辑被重构为可注册、可排序、可禁用的插件。这意味着你不再需要编译整个kube-scheduler二进制来修改行为只需调整配置文件即可。默认调度器配置通常位于/etc/kubernetes/scheduler.conf核心结构如下apiVersion: kubescheduler.config.k8s.io/v1beta3 kind: KubeSchedulerConfiguration profiles: - schedulerName: default-scheduler plugins: queueSort: enabled: - name: PrioritySort # 按PriorityClass排序队列 preFilter: enabled: - name: NodePorts # 预检hostPort冲突 filter: enabled: - name: NodeUnschedulable # 过滤unschedulable Node - name: PodFitsResources - name: MatchNodeSelector - name: CheckNodeCondition - name: CheckNodeDiskPressure - name: CheckNodePIDPressure - name: CheckNodeMemoryPressure - name: CheckVolumeBinding - name: NoVolumeZoneConflict - name: MaxCSIVolumeCountPred - name: CheckNodeCIDR - name: VolumeBinding - name: TaintToleration - name: NodeAffinity postFilter: enabled: - name: DefaultPreemption # 驱逐低优先级Pod腾资源 score: enabled: - name: TaintToleration - name: NodeAffinity - name: LeastRequestedPriority - name: BalancedResourceAllocation - name: SelectorSpreadPriority - name: InterPodAffinity - name: MostRequestedPriority - name: NodePreferAvoidPodsPriority - name: NodeResourcesFit - name: NodeResourcesBalancedAllocation - name: ImageLocalityPriority - name: ServiceSpreadingPriority - name: EqualPriority reserve: enabled: - name: VolumeBinding permit: enabled: - name: VolumeBinding preBind: enabled: - name: VolumeBinding bind: enabled: - name: DefaultBinder这个配置揭示了两个关键事实预选filter插件是“与”关系所有启用的filter插件必须全部通过Node才进入候选池。任何一个失败立即淘汰。优选score插件是“加权和”关系每个插件返回0~100分最终总分 Σ(插件分数 × 权重)。权重在pluginConfig中定义例如NodeResourcesFit默认权重为1而InterPodAffinity权重为2。注意事项禁用关键插件有风险。例如禁用TaintToleration则所有带污点的Node将无法被任何Pod调度除非Pod显式容忍这可能导致集群部分Node永久闲置。又如禁用VolumeBinding则PVC绑定逻辑失效StatefulSet可能永远卡在Pending。生产环境调整前务必在测试集群用kubectl create -f部署一个最小Pod验证调度行为。3. 核心参数与实操配置从默认值到生产级调优的完整链条3.1 影响调度效率的三大黄金参数scheduler的性能并非无限尤其在大规模集群中。其吞吐量Pod/秒直接受三个参数制约它们共同决定了调度器的“反应速度”与“决策精度”之间的平衡。参数名默认值作用原理生产调优建议风险提示--percentage-of-nodes-to-score50每次调度仅对集群中指定百分比的Node执行优选Score阶段其余Node跳过打分直接视为0分集群Node数100保持50100~500调至30500降至10~15过低会导致“最优解”被跳过高负载Node可能被误选过高则拖慢整体调度速度--scheduler-namedefault-scheduler指定调度器名称用于Pod的spec.schedulerName字段匹配多调度器场景必设如区分在线/离线业务单集群可不改若Pod指定了非默认schedulerName而集群无对应调度器Pod将永久Pending--bind-address0.0.0.0:10259HTTP健康检查与metrics端口绑定地址生产环境必须设为127.0.0.1:10259禁止外网暴露绑定0.0.0.0可能被未授权访问泄露集群调度指标我们曾在一个500节点集群中将--percentage-of-nodes-to-score从50降至10调度延迟从Pod创建到Running从平均1.2秒降至0.3秒但观察到部分Node CPU使用率飙升至95%而其他Node闲置——这是因为优选范围过小“局部最优”掩盖了“全局失衡”。最终采用动态策略白天业务高峰用15夜间批处理高峰用30并配合Prometheus告警监控scheduler_scheduling_algorithm_duration_seconds指标。3.2 亲和性Affinity与反亲和性Anti-Affinity的底层实现与避坑指南亲和性不是“锦上添花”而是保障服务稳定性的基础设施。但它的配置极易出错且错误表现极其隐蔽——Pod可能成功调度却在运行时因网络分区或存储故障导致雪崩。Node Affinity节点亲和性的核心是requiredDuringSchedulingIgnoredDuringExecution硬约束和preferredDuringSchedulingIgnoredDuringExecution软约束affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: topology.kubernetes.io/zone operator: In values: [cn-shanghai-a, cn-shanghai-b] # 必须调度到指定可用区 preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: node-role.kubernetes.io/worker operator: Exists # 偏好worker节点但非强制这里的关键陷阱在于operator的选择Invalues中至少一个匹配即通过安全NotInvalues中所有都不匹配才通过危险易导致无Node满足ExistsNode有该label即通过常用DoesNotExistNode没有该label才通过极难调试建议避免。Pod Anti-AffinityPod反亲和性更需谨慎。它要求scheduler在优选阶段计算“同label Pod在同Node/同Topology域的数量”并据此扣分。例如affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [payment-gateway] topologyKey: topology.kubernetes.io/zone # 同一可用区内不允许同app的Pod共存这个配置的代价极高scheduler必须为每个候选Node查询该Node上所有Pod的labels并与待调度Pod的labelSelector比对。在Node上Pod数超100时单次调度耗时可能增加200ms。某次我们误将topologyKey设为kubernetes.io/hostname单机粒度导致集群调度吞吐暴跌40%。实操心得反亲和性应作为最后手段。优先用topologySpreadConstraints拓扑分布约束替代它由kube-scheduler统一计算分布性能更好。例如topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: payment-gateway此配置要求各可用区Pod数量差≤1且不满足时拒绝调度效果等同于zone级反亲和但性能提升3倍以上。3.3 污点Taint与容忍Toleration集群资源隔离的终极武器污点是Node的“准入门槛”容忍是Pod的“通行证”。二者结合构成K8s最硬核的资源隔离机制。但90%的误用源于对effect的理解偏差。一个Node可设置多个污点kubectl taint nodes node1 key1value1:NoSchedule kubectl taint nodes node1 key2value2:PreferNoSchedule kubectl taint nodes node1 key3value3:NoExecute三种effect的本质区别NoSchedule新Pod若无对应容忍永不调度至此Node已运行的Pod不受影响PreferNoSchedule软性建议scheduler会尽量避开但非强制当无其他Node可选时仍会调度NoExecute最危险不仅阻止新Pod还会驱逐Evict已运行但无对应容忍的Pod。驱逐非瞬间完成有tolerationSeconds宽限期。一个典型灾难场景某运维同学为“标记GPU节点”执行kubectl taint nodes gpu-node nvidia.com/gpu:NoExecute结果所有未显式声明tolerations的Pod包括coredns、metrics-server在宽限期默认300秒后被驱逐集群DNS中断kubectl get pods命令本身都失败。正确做法是# 先用NoSchedule测试 kubectl taint nodes gpu-node nvidia.com/gpu:NoSchedule # 确认所有GPU Pod都加了toleration后再升级为NoExecute kubectl taint nodes gpu-node nvidia.com/gpu:NoExecute --overwritePod的容忍必须精确匹配tolerations: - key: nvidia.com/gpu operator: Equal # 必须Equal不能Exists value: true # 必须与taint的value一致 effect: NoSchedule # effect必须完全一致注意事项operator: Exists可匹配任意value但effect仍需精确匹配。tolerationSeconds仅对NoExecute生效单位为秒。生产环境严禁对系统组件如coredns所在Node设置NoExecute除非你已为它们配置了永久容忍。4. 自定义调度器开发与集成从零编写一个“按磁盘IO性能调度”的实践4.1 为什么需要自定义调度器默认调度器的三大盲区默认调度器基于静态资源CPU/Memory和简单拓扑Zone/Hostname决策但现代应用对底层硬件特性日益敏感。我们遇到的真实需求数据库Pod必须调度到SSD NVMe盘且IO延迟1ms的NodeHDD盘会导致TPS暴跌50%AI训练Pod需绑定RDMA网卡且要求同Node上无其他高网络吞吐Pod实时音视频Pod要求CPU核绑定cpuset且避免与burstable类Pod共享物理核。这些需求无法用NodeSelector或Affinity表达因为NodeSelector只能匹配label而IO延迟是动态指标Affinity无法关联外部监控数据如Prometheus的node_disk_io_time_ms默认调度器不感知硬件QoS能力。解决方案开发一个轻量级自定义调度器它不替换kube-scheduler而是作为第二个调度器通过spec.schedulerName显式指定。4.2 架构设计监听评估绑定的三步闭环自定义调度器本质是一个独立进程其核心逻辑仅三步监听Watch通过Kubernetes Watch API监听Pod事件过滤出spec.schedulerName io-aware-scheduler的Pod评估Score对每个候选Node调用外部接口如Prometheus Query API获取实时IO延迟指标计算得分绑定Bind调用Kubernetes Bind API将Pod绑定到选定Node。我们用Go语言实现核心代码骨架如下// 1. 初始化ClientSet config, _ : rest.InClusterConfig() clientset : kubernetes.NewForConfigOrDie(config) // 2. 创建Informer监听Pod podInformer : cache.NewSharedIndexInformer( cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { return clientset.CoreV1().Pods(metav1.NamespaceAll).List(context.TODO(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return clientset.CoreV1().Pods(metav1.NamespaceAll).Watch(context.TODO(), options) }, }, corev1.Pod{}, 0, cache.Indexers{}, ) // 3. 注册EventHandler podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { pod : obj.(*corev1.Pod) if pod.Spec.SchedulerName io-aware-scheduler pod.Status.Phase corev1.PodPending { schedulePod(pod) // 执行调度逻辑 } }, }) // 4. 调度主逻辑 func schedulePod(pod *corev1.Pod) { // 获取所有Node列表 nodeList, _ : clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) var bestNode *corev1.Node var bestScore float64 -1 for _, node : range nodeList.Items { // 检查Node是否Ready if !isNodeReady(node) { continue } // 查询该Node的IO延迟调用Prometheus ioDelay, err : queryPrometheusIOdelay(node.Name) if err ! nil || ioDelay 1.0 { // 延迟1ms则淘汰 continue } // 计算得分延迟越低分越高10 - ioDelay*5 score : 10.0 - ioDelay*5.0 if score bestScore { bestScore score bestNode node } } if bestNode ! nil { // 执行Bind操作 binding : corev1.Binding{ ObjectMeta: metav1.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name}, Target: corev1.ObjectReference{ Kind: Node, Name: bestNode.Name, APIVersion: v1, }, } clientset.CoreV1().Pods(pod.Namespace).Bind(context.TODO(), binding, metav1.CreateOptions{}) } }此代码仅200行但已具备生产可用基础。关键点在于不侵入kube-scheduler完全独立进程失败不影响默认调度精准监听只处理schedulerName匹配的Pod避免干扰实时评估每次调度都拉取最新监控数据非缓存值幂等绑定Bind API天然幂等重复调用无副作用。4.3 部署与验证如何让集群真正“认识”你的调度器部署自定义调度器需三步创建ServiceAccount与RBAC赋予Node和Pod读取权限apiVersion: v1 kind: ServiceAccount metadata: name: io-scheduler-sa namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: io-scheduler-role rules: - apiGroups: [] resources: [nodes, pods, pods/binding] verbs: [get, list, watch, create] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: io-scheduler-binding subjects: - kind: ServiceAccount name: io-scheduler-sa namespace: kube-system roleRef: kind: ClusterRole name: io-scheduler-role apiGroup: rbac.authorization.k8s.io部署Deployment注意资源限制避免抢占主调度器资源apiVersion: apps/v1 kind: Deployment metadata: name: io-aware-scheduler namespace: kube-system spec: replicas: 1 selector: matchLabels: component: io-aware-scheduler template: metadata: labels: component: io-aware-scheduler spec: serviceAccountName: io-scheduler-sa containers: - name: scheduler image: your-registry.io/io-scheduler:v1.0 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 200m memory: 256Mi验证调度行为# 创建测试Pod指定schedulerName cat EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: io-test-pod spec: schedulerName: io-aware-scheduler # 关键 containers: - name: nginx image: nginx EOF # 查看调度结果 kubectl get pod io-test-pod -o wide # 输出应显示NODE列为某个SSD节点 # 查看调度器日志 kubectl logs -n kube-system deploy/io-aware-scheduler # 应看到Pod io-test-pod scheduled to node gpu-node-01等日志实操心得自定义调度器上线前务必在测试集群用kubectl get events --sort-by.lastTimestamp观察事件流确认无FailedScheduling事件。生产环境首次部署建议先用replicas: 1和nodeSelector将其固定在一台测试Node上避免影响全局。5. 常见问题与排查技巧实录那些让你凌晨三点还在看日志的真问题5.1 Pending Pod诊断速查表现象可能原因快速验证命令解决方案0/3 nodes are available: 3 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didnt tolerate.Master节点被打上污点且Pod未容忍kubectl describe node master-node | grep Taints为Pod添加toleration或移除master污点不推荐0/3 nodes are available: 2 Insufficient cpu, 1 node(s) didnt match pod affinity rules.CPU不足 亲和性规则冲突kubectl top nodeskubectl describe pod pending-pod | grep -A 10 Events检查requests是否合理审查affinity配置是否过于严格0/3 nodes are available: 3 node(s) were unschedulable.Node状态为NotReady或SchedulingDisabledkubectl get nodes看STATUS列kubectl uncordon node或修复Node故障0/3 nodes are available: 3 node(s) didnt match node selector.Node缺少Pod要求的labelkubectl get node --show-labels为Node添加缺失labelkubectl label node name keyvalue0/3 nodes are available: 3 node(s) had taint {key:val}, that the pod didnt tolerate.Node有污点Pod无容忍kubectl describe node node | grep Taints在Pod spec中添加对应toleration提示kubectl describe pod pending-pod的Events部分永远是第一信息源。不要跳过这一步直接查Node资源。5.2 调度器性能瓶颈的四大征兆与根因分析当集群规模扩大scheduler可能成为瓶颈。以下是我们在生产环境总结的性能恶化征兆调度延迟突增kubectl get events --sort-by.lastTimestamp \| head -20中大量Pod的Scheduled事件时间戳比Created晚5秒。根因--percentage-of-nodes-to-score过低导致优选阶段跳过大量Node但预选阶段仍需遍历全部NodeCPU占用飙升。验证kubectl top pods -n kube-system \| grep scheduler看CPU使用率是否持续80%。解决调高--percentage-of-nodes-to-score或升级到v1.26启用NodeResourcesFit插件性能更优。调度器频繁OOMKilledkubectl get pods -n kube-system \| grep scheduler显示RESTARTS0。根因内存限制过小或监听了过多命名空间如metav1.NamespaceAll但集群有1000命名空间。验证kubectl describe pod scheduler-pod \| grep -A 10 Events看是否有OOMKilled事件。解决增加memory limit至512Mi或在Informer中限定命名空间。Pod批量Pending一次创建100个Pod前10个快速调度后90个卡住。根因默认调度器队列深度有限v1.25前为100高并发时积压。验证kubectl get --raw /metrics \| grep scheduler_schedule_attempts_total看resulterror计数是否上升。解决升级到v1.25启用--scheduler-cache-expiration10s加速缓存更新。调度结果不符合预期明明Node A资源更空闲但Pod总被调度到Node B。根因优选插件权重配置异常或BalancedResourceAllocation插件放大了Node B的均衡分。验证开启--v4日志搜索Scored node对比Node A/B各项得分。解决调整--policy-config-file中插件权重或禁用BalancedResourceAllocation若业务不需均衡。5.3 一个真实案例StatefulSet副本数从3扩到5为何只有2个新Pod启动某次数据库集群扩容执行kubectl scale statefulset mysql --replicas5后kubectl get pods显示mysql-0 1/1 Running 0 2d mysql-1 1/1 Running 0 2d mysql-2 0/1 Pending 0 5m mysql-3 0/1 Pending 0 5m mysql-4 0/1 Pending 0 5m直觉认为是资源不足但kubectl top nodes显示所有Node内存剩余5Gi。深入排查kubectl describe pod mysql-2Events显示0/3 nodes are available: 3 node(s) didnt match pod anti-affinity rules.查看StatefulSet的anti-affinity配置podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: [mysql] topologyKey: kubernetes.io/hostname # 错误应为zone问题在于topologyKey: kubernetes.io/hostname。集群只有3台Node而StatefulSet要求“同Node上不能有同app Pod”但已有mysql-0/1/2分布在3台Node上mysql-3/4无Node可选。修正方案将topologyKey改为topology.kubernetes.io/zone并确保3台Node分布在至少2个可用区。教训StatefulSet的反亲和性必须与集群拓扑匹配单可用区集群慎用hostname粒度。最后分享一个小技巧用kubectl get events --field-selector involvedObject.namepod-name可精准过滤某Pod的全部事件比describe更干净。我在某次排查中靠这条命令在200行日志里3秒定位到FailedScheduling的原始原因省了至少半小时。