【架构实战】Kubernetes调度器深度剖析:从Pod调度到自定义调度器

📅 2026/8/3 16:41:15
【架构实战】Kubernetes调度器深度剖析:从Pod调度到自定义调度器
你有没有遇到过这样的场景明明集群里还有大量空闲节点新创建的Pod却一直Pending或者两个CPU密集型的Pod被调度到了同一个节点导致互相拖垮又或者你希望把某些敏感业务固定调度到指定的一组机器上却不知道从何下手。这些问题全都指向Kubernetes里一个既核心又容易被忽视的组件——调度器kube-scheduler。这篇文章我会从调度器的整体架构讲起把调度流程、常用调度策略、亲和性反亲和性、污点容忍以及自定义调度器这些内容一次讲透。全文基于我在生产环境中的真实踩坑经历希望能帮你把调度这件事彻底搞明白。一、调度器到底在干什么1.1 调度器的定位Kubernetes是一个分布式系统Master节点上跑着各种控制面组件其中kube-scheduler负责一件事为待调度的Pod找到一个最合适的节点。听起来简单但最合适三个字背后是一整套复杂的决策逻辑。调度器要同时考虑节点的资源剩余量CPU、内存、GPU等Pod对资源的需求requests和limits各种约束条件亲和性、污点、拓扑分布等集群当前的负载均衡情况1.2 调度不是随机分配很多人以为调度器是随便挑一个有空闲资源的节点这是最大的误解。实际上kube-scheduler的决策过程分为两个阶段过滤阶段Filtering先把不满足硬性条件的节点剔除掉。比如节点资源不够、端口冲突、不满足nodeSelector等这些节点直接出局。打分阶段Scoring在剩下的候选节点里根据一系列评分规则给每个节点打分得分最高的节点胜出。这种先过滤、后打分的两阶段模型是整个调度器的核心设计思想。它保证了调度结果既满足约束正确性又尽量最优优化性。二、调度流程全解析2.1 从Pod创建到调度完成一个Pod从创建到运行完整经历以下流程用户提交Podkubectl apply 或通过Deployment等控制器创建Pod进入Pending状态被写入etcdkube-scheduler通过watch机制监听到新的Pod且该Pod的spec.nodeName为空调度器执行过滤打分选出最优节点调度器将结果写回Pod的spec.nodeName设置为目标节点目标节点上的kubelet监听到绑定拉取镜像、启动容器关键点调度器只负责选节点和写绑定真正干活的是kubelet。所以即使调度器挂了已经运行中的Pod不会受影响只是新Pod无法被调度。2.2 调度队列调度器内部维护了多个队列来管理待调度的PodactiveQ活跃队列存放等待调度的PodbackoffQ退避队列调度失败的Pod会进入这里等待一段时间后重试unschedulableQ不可调度队列比如资源不足导致失败的Pod等集群资源变化后再尝试这三个队列的配合让调度器在集群资源紧张时不会疯狂重试浪费CPU而是耐心等待资源释放。三、常用调度策略详解3.1 nodeSelector最简单的定向调度nodeSelector是最基础的调度约束通过节点的label来筛选节点apiVersion:v1kind:Podmetadata:name:nginx-gpuspec:nodeSelector:gpu:truecontainers:-name:nginximage:nginx只有带gputrue标签的节点才会被选中。优点是简单直观缺点是表达能力有限——只能做等值匹配无法表达或、非这类复杂逻辑。3.2 节点亲和性nodeSelector的进阶版节点亲和性nodeAffinity解决了nodeSelector表达能力不足的问题支持更丰富的匹配规则spec:affinity:nodeAffinity:requiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:-matchExpressions:-key:disktypeoperator:Invalues:-ssd-nvmepreferredDuringSchedulingIgnoredDuringExecution:-weight:100preference:matchExpressions:-key:zoneoperator:Invalues:-cn-east-1这里有两个关键概念requiredDuringSchedulingIgnoredDuringExecution硬性要求必须满足否则Pod无法调度。注意后半段IgnoredDuringExecution表示节点标签后续变化不影响已调度的Pod这个设计是为了避免频繁迁移Pod。preferredDuringSchedulingIgnoredDuringExecution软性偏好不满足也能调度但满足的节点会获得更高分数。可以设置weight权重实现尽量调度到某个区域的效果。3.3 Pod亲和性与反亲和性节点亲和性解决的是Pod往哪些节点上放而Pod亲和性解决的是Pod和哪些Pod放一起。Pod亲和性podAffinity把相关联的Pod调度到一起。典型场景Web应用和它依赖的Redis缓存放在同一可用区减少跨机房网络延迟。spec:affinity:podAffinity:requiredDuringSchedulingIgnoredDuringExecution:-labelSelector:matchExpressions:-key:appoperator:Invalues:-redistopologyKey:kubernetes.io/hostnamePod反亲和性podAntiAffinity把互斥的Pod分散开。典型场景同一个应用的多个副本不要调度到同一台机器避免单点故障。spec:affinity:podAntiAffinity:preferredDuringSchedulingIgnoredDuringExecution:-weight:100podAffinityTerm:labelSelector:matchExpressions:-key:appoperator:Invalues:-webtopologyKey:kubernetes.io/hostname这里有个非常重要的参数topologyKey。它决定了分散到什么粒度——kubernetes.io/hostname表示按节点分散topology.kubernetes.io/zone表示按可用区分散。生产环境建议至少做到按节点分散核心服务按可用区分散。3.4 污点与容忍节点的门禁系统污点Taint是打在节点上的标记表示这个节点有特殊属性默认不接受普通Pod。容忍Toleration是Pod的声明表示我能忍受这个污点。# 给节点打污点不调度普通Podkubectl taint nodes node1dedicatedspecial:NoSchedule# 查看节点污点kubectl describenodenode1|grepTaints污点有三种效果effectNoSchedule不调度新Pod已运行的不管PreferNoSchedule尽量不调度但不是硬性拒绝NoExecute不仅不调度新Pod还会驱逐已经运行且不容忍的Pod对应的容忍示例spec:tolerations:-key:dedicatedoperator:Equalvalue:specialeffect:NoSchedule生产实战经验Master节点默认带有node-role.kubernetes.io/master:NoSchedule污点这就是为什么普通Pod不会跑到Master上。如果想利用Master节点资源可以给特定Pod加容忍但一定要谨慎——Master挂了的代价远高于省下的那点资源。3.5 拓扑分布约束让Pod雨露均沾拓扑分布约束topologySpreadConstraints是Kubernetes 1.19引入的调度策略用于控制Pod在集群中的分布均匀度spec:topologySpreadConstraints:-maxSkew:1topologyKey:kubernetes.io/hostnamewhenUnsatisfiable:DoNotSchedulelabelSelector:matchLabels:app:web这段配置的意思是appweb的Pod在节点上的分布偏差skew最大为1。比如集群有3个节点10个副本那么每个节点上Pod数量的差距不能超过1——大致是3、3、4的分布而不是8、1、1。whenUnsatisfiable有两个取值DoNotSchedule无法满足时不让调度硬约束ScheduleAnyway无法满足时尽量均匀软约束这个功能在滚动发布、多可用区部署时特别有用能避免所有新副本都挤到一个节点的尴尬。四、自定义调度器当默认调度器不够用时4.1 为什么要自定义调度器默认调度器已经很强大了但总有它覆盖不到的场景业务需要感知数据本地性比如离线计算任务希望调度到数据所在节点需要批量调度一组Pod要么全部调度成功要么全部失败gang scheduling需要感知自定义资源比如GPU显存、特殊加速卡需要实现多租户配额之外更细粒度的资源控制4.2 三种自定义方式方式一扩展默认调度器推荐Kubernetes 1.19支持调度框架Scheduling Framework可以编写调度插件注入到默认调度器的过滤/打分/绑定等扩展点typeMyScorePluginstruct{}func(p*MyScorePlugin)Name()string{returnMyScore}func(p*MyScorePlugin)Score(ctx context.Context,state*framework.CycleState,pod*v1.Pod,nodeNamestring)(int64,*framework.Status){// 自定义打分逻辑比如优先选择负载最低的节点return100,nil}编译成二进制后通过调度器的配置文件启用apiVersion:kubescheduler.config.k8s.io/v1kind:KubeSchedulerConfigurationprofiles:-schedulerName:default-schedulerplugins:score:enabled:-name:MyScore方式二独立的自定义调度器实现一个独立的调度器监听Pod并自行决定绑定。最简单的写法是用client-go// 伪代码监听未调度的Pod按自定义规则选节点然后绑定forpod:rangepodInformer.Informer().Run(){ifpod.Spec.NodeNamepod.Spec.SchedulerNamemy-scheduler{node:myPickBestNode(pod)// 自定义选节点逻辑bindPod(pod,node)// 调用Binding API}}Pod通过schedulerName字段指定使用哪个调度器spec:schedulerName:my-scheduler方式三调度器插件市场社区有一些成熟的第三方调度器可以直接用比如Volcano主打批量任务和gang scheduling适合AI/大数据场景Koordinator阿里开源的混部调度器支持QoS感知和资源超卖Descheduler严格说它不是调度器而是再调度器负责把不合理的Pod重新调度4.3 多调度器共存一个集群可以同时运行多个调度器通过schedulerName区分。默认调度器处理不指定schedulerName的Pod自定义调度器处理指定自己的Pod。两者互不干扰这在灰度验证自定义调度器时特别有用。五、生产环境排障实战5.1 Pod一直Pending怎么办这是最高频的问题。排查步骤# 第一步看Pod状态和事件kubectl describe podpod-name# 第二步看调度器日志kubectl logs-nkube-system kube-scheduler-master-name常见的Pending原因资源不足所有节点都不满足requests。事件里会显示0/5 nodes are available此时要区分是CPU还是内存不足考虑扩容节点或调低requests节点有污点且Pod无容忍事件会提示node(s) had taintnodeSelector/nodeAffinity不匹配事件会提示节点不满足选择器PVC未绑定Pod依赖的PVC处于Pending状态调度器会等PVC就绪端口冲突Pod要用的hostPort已被占用5.2 调度不均衡怎么办某台节点负载特别高其他节点很闲检查是否有Pod反亲和性缺失同一个Deployment的副本被调度到一起是否设置了合理的requests如果所有Pod的requests都写得很低调度器无法准确评估节点负载是否启用了拓扑分布约束建议核心服务都加上topologySpreadConstraints是否使用Descheduler定期整理调度器只在Pod创建时决策一次运行中的Pod不会自动迁移Descheduler可以帮我们把不合理的调度纠正过来5.3 调度延迟高的优化如果Pod从创建到Running耗时明显变长检查调度队列是否堆积调度器metricsscheduler_queue_depth检查API Server响应是否变慢调度器的每次决策都要读写etcd考虑升级调度器版本新版本在调度吞吐上有持续优化六、总结与最佳实践调度是Kubernetes里看不见但极其重要的能力。结合我的生产经验给你几条落地建议所有工作负载都要写合理的requests这是调度准确性的基石不写requests的Pod会让调度器盲人摸象核心服务必须配Pod反亲和性至少按hostname分散避免同节点故障导致全部挂掉用拓扑分布约束替代手动打标签更优雅、更自动Master节点不要轻易去污点除非你清楚知道后果自定义调度器是最后手段先确认默认调度器调度框架插件能否满足需求监控调度指标队列深度、调度时延、失败原因这些指标能帮你提前发现问题调度器就像Kubernetes的交通指挥中心它不直接干活但它的每个决策都影响着整个集群的效率和稳定性。把调度搞明白了你的集群才算真正活了起来。