1. 调度器到底在解决什么问题,为什么不能靠"人工分配"
先说个真实场景。有次我帮朋友排查一套生产集群,总共二十多台工作节点,跑着两百多个服务,资源水位一直很紧张。结果发现有个核心业务Pod已经Pending了三天,没人发现,原因特别基础:它要4核8G的规格,而集群里所有节点剩下的碎片资源,没有一台能完整满足这个组合需求。当时的维护者说了一句话让我印象很深:"我以为它会自动帮我分到最合适的机器上,没想到它连这台机器在哪都找不到。"
这就是调度的本质问题:调度器不是"分配机器",而是"决策并找到一台条件完全满足的节点"。它要替整个集群回答一个非常现实的问题——在这几百台配置不同、负载不同、还不断有旧Pod结束、新Pod启动的动态环境里,我手上的每个工作负载,到底放到哪台机器上才算合理。
很多人觉得这是个小事情,节点多就随便放呗。真不是。我常拿停车位打比方:一个好的调度器相当于一个脑子清醒的停车场管理员,他不仅知道哪里有空位,还知道哪个车位离电梯最近、哪条通道容易堵、哪辆车的车主赶时间、哪片区域晚上有活动不能停。而糟糕的调度器就像闭着眼随便指位置的管理员,结果是:资源明明够,有些车就是停不进去,或者停进去了,晚点想把车开出来得挪一排车。
放在分布式系统里,调度器要同时满足三个看似矛盾的目标:
- 资源利用率要足够高:一台机器只要没被用满,对你来说就是在亏钱。不管你是自建机房还是按量付费,空转的CPU和内存都是真金白银。
- 服务质量要足够稳:不能为了凑利用率,把两个互相争抢CPU的敏感业务塞到同一台机器上,导致业务互相拖垮。
- 系统要能扛故障:某台节点出问题的时候,调度器不能假装看不见,继续把新任务送过去,更得能让旧任务快速换一台机器重新跑起来。
所以调度器本质上是"资源拍卖师+交通警察+保险理赔员"三个角色的集合体。这也是为什么业界几乎所有集群管理系统——Kubernetes、YARN、Mesos、Slurm、HashiCorp Nomad——都把调度器当成最核心的组件来设计,而不是用一套脚本简单处理。
现在调度器已经从"单机按条件挑一台"进化成了"全局打分排序择优录取",从"只看CPU内存"进化成了"考虑GPU、网络带宽、存储拓扑、数据本地性、Pod间亲和反亲和、优先级抢占"这些复杂因素。你只有理解了它存在的根本动机,后面遇到那些"明明资源很充足但就是调度不上去"的问题时,才能有方向去排查,而不是一头扎进日志里瞎翻。
2. 三种主流调度算法的设计逻辑:FIFO、容量调度与公平调度的取舍
调度器不是只有一种实现思路。不同历史时期、不同业务场景,各家公司摸索出了几套截然不同的调度算法。搞懂它们各自的模型和代价,比单纯背概念有用得多。
2.1 最朴素的先来先服务:FIFO调度
最早期的集群调度就是排队。谁先提交任务,谁就先进去被安排。这个方案的优点是实现简单、逻辑清晰,提交顺序就是执行顺序,谁都无话可说。
但它的致命缺陷也显而易见:队头阻塞。想象一下,第一个来的任务要求一台"万中无一"的特定机器(比如指定GPU型号),后面排队的那些只需要普通CPU的任务,全都得等它找到合适的机器才能往下走。
我见过一个真实案例:一个团队提交了个需要某种特殊GPU的深度学习任务,但当时集群里根本没这种卡,后面几百个正常的Spark任务就全被堵在队列里,整个集群利用率掉到5%以下。这就是FIFO最大的坑——它只关心顺序,不关心资源匹配度,遇到一个"刁钻"的任务就能摧毁整条流水线。
现在的纯FIFO调度基本只在单机进程级调度里还能看到影子,多租户集群已经没人敢直接裸用了。
2.2 容量调度:给每个团队圈好"责任田"
容量调度的思路是:把整个集群的物理资源,按比例划分成一个个独立的"队列",每个队列有自己固定的资源上限。A团队最多用30%的CPU,B团队最多用20%,谁都不能越界。
这套方案最明显的好处是隔离性极强。某个团队哪怕写了个资源黑洞应用,最多也只能把自己那个队列吃满,影响不到别人。YARN的Capacity Scheduler、Kubernetes的ResourceQuota,本质都是这个思路——通过配额把故障爆炸半径限制住。
不过代价也很直接:很容易造成资源浪费。假设A团队凌晨没有任务,他那30%的资源空着,B团队业务量突然暴涨,就算B需要更多算力,也拿不到A的空闲份额。内存和CPU又不像人一样可以借来借去。
所以后来主流实现都做了"弹性借用"的改良:队列A空闲时允许队列B临时借用,一旦队列A有新任务提交,借出去的要还回来。这个机制很实用,但它要求调度器要非常敏锐地感知任务进出队列的时机,实现复杂度一下子就上来了。
2.3 公平调度:力争让大家"差不多满意"
公平调度的核心思想,简单说就是动态维持一个均衡水位。调度器实时统计每个用户/队列的累计使用量,在分配新任务时,优先把手头资源给"用得最少"的那个队列。
最经典的实现是Max-Min Fairness:先把资源均分给每个队列,有人用不满的份额再拿出来二次分配,循环往复。这是一个很优雅的增量算法,一套公式就能保证最终出来一个相对均衡的结果。
公平调度的优势是天然适配多租户共享集群。适合那种"很多个团队共用一套集群、谁都不希望自己老被饿着"的场景。
但要注意,它也有自身的问题:为了追求"绝对公平",调度器会增加很多额外的计算量;而且如果每个队列的需求本身就差异巨大,单纯追求公平会导致"大任务吃不饱、小任务被反复让位"。实际落地时通常要加上权重配置,比如核心业务队列权重是10,实验性业务队列权重是1,让公平在加权的基础上慢慢收敛。
2.4 为什么实际系统都是"混合模式"
我个人的经验是:真正能用于生产的调度系统,几乎没有只用上述某一种纯算法的。Kubernetes默认调度器就玩了个组合:先跑一轮filter阶段把不满足硬性条件的节点全部剔除,然后在剩下的节点里按打分规则排序,这就不是单纯"谁先来谁先得"了,而是"条件优先+分数择优"。YARN里你还可以给队列同时配置容量上限和公平权重,实现"长周期看配额、短周期看公平"。理解了这三类算法,你才看得懂生产系统里那一堆调度参数的初衷,而不是照抄别人的配置。
3. 一次调度请求的完整生命周期:从Pod提交到节点绑定的每一步
纸上谈兵没意思,直接看Kubernetes调度器的工作流程最有代表意义。一套完整的调度流程,绝不止"看哪个节点空闲就放过去"那么简单,它分为四个阶段,我一个个拆开讲。
3.1 调度器凭什么能"看到"所有Pod和节点
Kubernetes里的调度器是一个独立组件(kube-scheduler),它通过API Server监听集群状态。你要理解一个关键点:调度器本身没有任何直接操控节点的能力,它只是一个"决策大脑",所有的判断依据来自API Server给它推送的数据快照。
所以集群里每次有节点标注变化、Pod申请新资源、或者某个节点状态异常,API Server都会通知调度器,调度器内部维护着一份"当前集群全景图"的内存缓存。这一层设计使得调度器不用每次都去所有节点上一个个问"你还有多少内存",性能大幅提升。
但也正因为是缓存,它会有一个视图一致性问题——比如某个节点的资源明明已经被其他Pod占了,但调度器缓存的"可用资源"还没更新,这时新来的Pod就有可能被调度到一个实际上已经放不下的节点上。解决思路一般是通过资源预留机制或者后续的"二次确认"兜底,后面我讲坑的时候会细说。
3.2 过滤阶段:把"根本不可能"的节点直接淘汰
这个阶段在Kubernetes里叫"Predicates",新版本代码里叫"Filter"。它做的是大量硬性检查,只要节点有任何一项硬性条件不满足,这个节点就被直接踢出候选名单。常见的检查项包括:
- 节点资源是否够:请求的CPU、内存、GPU、临时存储,都必须在节点当前可分配量范围内。
- PodSelector是否匹配:调度Pod的时候经常打上nodeSelector要求"只调度到拥有标签env=prod的节点",不匹配的直接排除。
- 端口冲突:如果两个Pod都要绑同一个宿主端口比如8080,那二选一,不能放在同一台机器。
- 磁盘和卷冲突:某些存储类型比如ReadWriteOnce的云盘只能被一台机器挂载,如果一个节点已经被占用,后续需要挂载同一块盘的Pod就不能调度过去。
- 污点容忍度:节点打了污点(taint),Pod如果没有对应容忍(toleration),就不能上去。
你可以把这个阶段理解为"海选":不管节点分高分低,先把那些有硬伤的筛掉。很多新手第一反应是"我Pod调度不到节点上,是不是资源不足",其实有大量情况是卡在这个过滤阶段,比如标签没匹配上、存储冲突、宿主端口被占用。
3.3 打分阶段:从"都能放"里选"最合适"的
过滤完,剩下的节点全部满足硬性条件。这时进入第二阶段的打分,Kubernetes里叫"Priorities",新版本叫"Score"。这个阶段的主要工作就是按一组策略规则给每个候选节点打分,分数最高者获胜。
打分策略五花八门,常见的包括:
- LeastRequestedPriority(资源最空优先):把请求量小的节点排前,尽量把负载铺平,防止资源碎片化。
- BalancedResourceAllocation(资源均衡优先):CPU和内存平衡的节点分数更高,避免出现一台节点CPU内存配比极端不均衡还继续塞新任务。
- ImageLocality(本地镜像优先):如果节点上已经有启动Pod需要的镜像,就不用再拉了,启动时间大幅缩短,这类节点得分更高。
- NodeAffinity(节点亲和性):如果有软性偏好偏好(比如preferredDuringScheduling),满足偏好的节点会加分。
打分阶段本质就是把集群运营目标数字化。注意"最合适"不等于"空闲最多",因为某些场景里"数据在本地"比"资源多一点"重要得多——比如大数据任务,如果调度到一台没有对应数据的节点,光拉数据就能把任务的收益抵消掉。
3.4 绑定阶段与最终执行
选出了最优节点,调度器仍然不能自己做主。它只是向API Server发起一个"绑定(Binding)"请求——把Pod和Node关联起来。这个过程是异步的,API Server持久化这个绑定记录之后,目标节点的kubelet会发现"有一个新Pod归我了",然后它再去调容器运行时把Pod真正启动起来。
看到没有,调度器只负责"决策",不负责"执行"。这个边界很重要,因为在多调度器并存的场景下,它可以做到"可以随时换调度器,不影响节点上已经跑着的Pod"。这也是控制器模式的核心哲学:分工明确,各司其职。
3.5 一个完整的时间线例子
我举个例子方便你理解全链路。假设你在Kubernetes里提交了一个名为"nginx-frontend"的Pod,请求1核CPU、512M内存,要求调度到带有zone=shanghai标签的节点上:
- Pod被创建,进入Pending状态,调度器第一时间通过API Server感知到这个新Pod的出现。
- 调度器遍历集群全部节点,先做过滤:把所有没有
zone=shanghai标签的节点全部排除;同时检查内存、CPU是否满足,端口是否冲突,污点是否容忍。假设100个节点里剩5个符合条件。 - 进入打分环节:同样的512M请求,空闲度高的节点分高;如果某个节点本地已经缓存了nginx镜像,可能还会再拿3~5分的镜像本地加分。最后选了最高分节点,比如node-073。
- 调度器发起绑定,Pod的spec里被记录上
nodeName: node-073,状态从Pending变成Running(或者说Scheduled)。 - node-073的kubelet发现有新Pod,拉镜像、创建容器、启动健康检查,最终Pod Ready。
整个流程从提交到最终绑定,在几百节点的集群里通常只需要毫秒到秒级的时间。调度器之所以快,是因为它不需要等容器真正启动,只做"软决策"。
4. 亲和性、污点与自定义调度器:把话说得更细的控制手段
默认调度器已经能处理一般场景,但生产环境里总有"我要把这批Pod放到同一个机架"、"这个节点只能给某个特殊业务用"这种精细需求。Kubernetes和YARN都提供了对应控制原语。
4.1 亲和性与反亲和性
先说节点亲和性。它分硬性要求(requiredDuringScheduling)和软性偏好(preferredDuringScheduling)。硬性要求其实就是高级版nodeSelector,条件不满足时直接不调度;软性偏好则是"我更喜欢,但实在没有也能接受"。
Pod间亲和性(PodAffinity)则解决另一个痛点:把多个相关的服务尽量放到同一个拓扑域里。举个典型场景,两个服务A和B之间高频调用,如果把它们放到不同的可用区,请求延迟和流量费用都会增加;设置PodAffinity让B优先调度到"A已经在跑的节点",就能让数据路径变短、整体吞吐量提升。
反亲和性(PodAntiAffinity)正好相反,目的是分散风险。比如有两个业务副本,我不想让它们落在同一台机器上——一台机器故障不会把两个副本同时打挂。这在部署核心有状态服务(比如Kafka、ZooKeeper、Redis集群)时几乎是必配项。
我要提醒一点:强制的PodAffinity是有调度代价的。当两个Pod必须放在同一台机器上,你就把候选节点范围缩小了一大截,很容易出现"另一个Pod在想调度的节点上,但因为它的副本数量一直在变,导致这个Pod永远等不到它过来"的循环等待。所以我一般建议:核心服务用软性偏好,配合反亲和性的强制规则。
4.2 污点和容忍度:白名单式的准入控制
污点(Taint)和容忍度(Toleration)的设计逻辑很有意思,它和亲和性正好互补。亲和性是"我能接受哪类节点",污点则是"我不想接收哪类Pod"。
给节点打上一个污点,比如dedicated=bigdata:NoSchedule,所有没有明确容忍这个大数据的Pod就不会被调度到这个节点上。这个机制在规划专用节点池时特别有效:GPU节点打上污点,只有带GPU需求的Pod容忍它,才能被调度过去;管理面节点打上污点,业务Pod默认就不会跑上去,确保集群核心组件不被业务流量挤爆。
污点还有个带特殊效果的类型叫NoExecute。它不光阻止新Pod上来,还会把节点上已存在但不容忍该污点的旧Pod直接驱逐。这在节点预维护、网络故障隔离的时候非常有用——给故障节点打上NoExecute污点,上面的业务就会自动迁移走了,不需要人工一台台去删。
4.3 什么时候需要自己写一个调度器
默认调度器的扩展点已经很强了,但总有些场景它覆盖不了:
- 业务调度依赖很专有数据:比如电影渲染任务,需要知道每个节点当前GPU温度、渲染队列排队长度,默认调度器拿不到这些数据,也没法用通用打分策略表达。
- 需要全局最优而不仅是节点局部最优:有些批处理系统要一次性调度几千个Task,希望总体完成时间最短,单纯逐个Pod按"当前状态"决策做不到这种全局规划。
- 调度逻辑涉及业务资源配额体系:比如某个公司内部有一套"业务线信用分",要按信用分决定资源投放优先级,这属于业务策略,不是基础设施策略。
遇到这些情况,你可以做自定义调度器。Kubernetes提供了多调度器并存机制:只要你的调度器通过API Server监听Pod,并且定期提交绑定请求,就能和默认调度器互不干扰。你只需要在Pod的spec里显式声明schedulerName: my-custom-scheduler即可“点名”让某个Pod走自定义调度器。
不过我得泼盆冷水:自定义调度器是个双刃剑。你接手了全部策略实现,什么过滤、打分、高可用、cache一致性全得自己搞定。大多数公司没必要从零写,完全可以先扩展默认调度器(比如用Scheduler Framework的Plugin机制实现自定义打分和过滤),不满足需求再考虑完全自研。我见过有团队为了调度一个极小众场景,直接自研了一套调度器,结果后来维护成本远超收益,最后又迁回默认调度器了。
4.4 调度器Framework的扩展点用法
Kubernetes从1.19开始正式把调度器改成可插拔的Framework架构,内置了一系列扩展点(Extension Points)。常用的包括:
- QueueSort:决定Pod在调度队列里的排序规则。
- PreFilter / Filter:相当于过滤阶段的预检查和主检查。
- PostFilter:过滤完发现没有可用节点时,可以尝试抢占低优先级Pod来腾位置。
- Score:打分阶段,可以注册自己的计分算法。
- Bind:自定义绑定逻辑,比如某些场景需要先预留资源再绑定。
这个机制比自研整套调度器轻量得多。我实际做过的一个案例:给一个AI训练平台写了两个插件,一个在Filter里检查节点是否有空闲GPU显存、另一个在Score里根据"节点上已部署同类训练任务的个数"加分,实现了把同批次训练任务尽量散开的效果,最终任务排队时间比默认调度少了30%左右。
所以我的建议是:先默认调度器,再到Framework插件,最后才轮到全自研。顺序千万不能反了。
5. 抢占、优先级与混部调度:资源不够用的时候怎么办
资源永远是不够用的。调度器常规分配之外,还有一个重要职责:当新任务到来而集群已经没有足够资源时,决定谁让位、什么时候让位。
5.1 优先级类和抢占调度
Kubernetes里通过PriorityClass定义优先级,从-1000到1000000000(10亿),数字越大优先级越高。调度器在处理Pod时,会优先调度高优先级Pod。
当高优先级Pod到达但无节点满足资源条件时,调度器会尝试"抢占":挑一个低优先级Pod赶走,把资源腾出来给高优先级用。这个逻辑特别像救灾时的交通管制:救护车来了,普通车辆要全部让路。
但引入抢占也带来了"资源抖动"的代价:被抢占的Pod说没就没,如果它不是无状态服务,会导致数据丢失或事务回滚。所以生产上我只给两类场景开高优先级抢占:
- 集群控制面组件:比如DNS、网络组件挂了,整个集群都受影响,这种必须能抢占。
- 在线核心交易链路:可以抢占离线计算任务,但不能抢占其他在线服务。
而且要把PriorityClass的差设计得合理:核心业务优先级5,离线任务优先级1,不要动不动就搞10亿,否则会出现"大猴子抢小猴子的凳子,大猴子又被更大的猴子抢"的连环抢占风暴。
5.2 "延迟绑定"和"声明式资源"做内存资源超卖
很多集群为了让利用率更好看,会开内存压缩/超卖,但调度器一超卖就可能出事。你可以给每个节点设置"可分配量"(Allocatable)比物理总量小很多,并在节点上预留一部分"系统保留"和"驱逐阈值"。
比较实用的一个技巧是:用DS(DaemonSet)方式在每个节点固定放一个"资源占位容器",声明给这个节点留下500M内存缓冲。这样调度器天然以为这台节点可分配量少了500M,Pod实际使用量即便偶尔超一点,也不太容易触达内核OOM。这个思路本质上是利用调度器的声明式资源模型来人为预留安全垫,在传统超卖方案里非常好用。
5.3 混部调度:在线服务+离线任务一体化的最佳实践
混部(Colocation)是这几年的大趋势:业务高峰和离线计算高峰通常错峰,白天在线业务忙,晚上离线批处理闲不住,把它们按比例放在同一批节点上,能极大提升集群整体资源利用率。
但混部的难点在于:在线业务对延迟极度敏感,离线任务又爱争抢CPU缓存和内存带宽。调度器需要能识别两种任务特征,并做出分级保障:
- 在线Pod开启CPU绑核/独占策略,离线任务只能用空闲CPU。
- 离线任务设置更低的资源Request,实际运行中在线资源不足时,优先驱逐离线Pod。
- 通过Monitoring侧数据动态调整节点上的调度水位,如果在线业务延迟上升就收缩可混部的离线条数。
Kubernetes社区有Crane、Koordinator这些开源组件专门做这类混部调度优化。我自己体验下来,它们最大的价值是让调度器从"静态配置"转变为"数据驱动的动态调整"——调度器不再只看Request,还会参考实际监控指标来决策。这一点对追求极致性价比的公司很有吸引力,但同时也对监控体系和稳定性保障提出了很高要求。
5.4 调度性能:上百节点时需要注意什么
这节虽然放最后,但生产上经常被忽略。默认Kubernetes调度器在几百节点规模内表现没问题,但到几千节点、每秒大量Pod提交时,容易出现两个坑:
- 调度器性能受"节点过滤遍历"制约:每次都要过一遍所有节点,节点越多耗时越长。社区的解决思路是做节点数分片和并行(比如把节点分成多个组并行过滤,然后汇总结果)。
- Resync周期导致视图滞后:如果调度器缓存和真实集群状态差距过大,会出现"调度到了但绑定失败"的窘境。Kubernetes会在绑定后再次做一轮"调度结果检查"来兜底,但如果你的自定义调度器没有这层保障,就得自己实现一个异步确认机制。
我的实践建议是:超过500个节点的集群,给kube-scheduler单独做资源配额和独立故障域,别让它跟业务Pod抢资源。最好再配置一个健康检查,专门盯着调度器的"调度时延"和"队列堆积数"这两个指标,有异常及时报警,否则调度队列一旦堵了,整个集群看起来还活着,实际上新业务全部Pending。
6. 一个真实疑难杂症的排查过程:节点资源充足但Pod一直Pending
纸上得来终觉浅。我选一个真实案例,带你完整走一遍排查链路。这个案例我印象很深,因为它隐藏得太深了——你说它是调度问题吧,它更像配置问题;你说它是配置问题吧,它又牵涉到调度器工作机制。
6.1 故障现象
用户反馈:在Kubernetes集群里提交了一个Deployment,创建了3个副本,但有一个副本永远Pending。看节点资源,CPU和内存都充足,怎么看都不像资源不够。
6.2 初步排查:看Pod状态和事件
我第一步永远是kubectl describe pod xxx,看Events。结果发现事件里没有任何调度器日志,只是显示Pending。
这个细节很关键:正常情况下,如果调度器真的跑过调度流程,Events里会显示类似Successfully assigned default/xxx to node-073这种信息。如果一个Pod长时间Pending且没有任何调度事件,要么就是调度器根本没看到这个Pod,要么就是Pod的spec里指定了一个不存在的schedulerName,导致没有任何调度器认领它。
6.3 深挖:发现自定义schedulerName残留
查看Pod的YAML,果然发现schedulerName: my-scheduler。但集群里根本没有部署这个自定义调度器,所以默认的kube-scheduler看到这个名字就直接跳过了——就像你给一份快递写了个不存在的快递公司名称,所有快递员看到都绕道走。
这种问题常见于一些DevOps平台或自动化发布系统,它们为了支持自定义调度会默认注入这个字段,结果在没部署对应调度器的环境里就留下了一个坑。解法很简单:把schedulerName删掉或改成default-scheduler,Pod立刻就能被调度了。
排错心得:遇到Pending先别查资源,先看Events里有没有调度事件。没有调度事件,优先怀疑"schedulerName不匹配"或者"调度器整体挂了";有调度事件但失败,再去查过滤阶段的标签、污点、资源。
6.4 第二个坑:节点有污点,Pod没有容忍度
另一个类似案例里,Events能看到调度器在尝试,但显示的提示是0/8 nodes available: 8 node(s) had taint {dedicated=gpu: NoSchedule}。
这就是前面讲的污点机制生效了。解决办法两种:要么给Pod加对应的Toleration,要么把节点上的污点去掉(如果不是刻意要做隔离的话)。
生产上这个坑特别常见:平台团队给某批新节点打了污点,还没来得及给业务方同步,业务的人就开始用——然后一排服务全部Pending。
6.5 第三个坑:端口冲突
还有一次比较隐蔽,两个微服务都监听宿主机的30080端口,调度器过滤阶段就把所有节点都刷掉了(因为每个节点都已经被其中一个服务占了端口)。这种问题Events会提示node(s) had existing hostPort conflicts。
排查思路也简单:把那两个服务的hostPort改成不同端口,或者一个不用hostPort,改用NodePort+Ingress方式暴露即可。这个坑的隐蔽性在于:如果你事先不知道两个服务用同一个端口,扫日志半天也看不出问题。
6.6 推荐的工具和调试姿势
排查调度问题,我常用的三板斧:
kubectl get pod -o wide:快速看Pod被调度到哪个节点。kubectl describe pod:看Events,这是第一手线索。kubectl get events --sort-by=.lastTimestamp:按时间排整个命名空间的事件流,经常能看到调度器在一堆小动作之后的最终决定。
如果是大规模集群调度性能问题,再配合看kube-scheduler的metrics(比如scheduler_scheduling_algorithm_duration_seconds、scheduler_pod_scheduling_attempts)。这个指标可以告诉你调度器本身耗不耗时。
7. 调度器之外:还剩哪些细节容易翻车
最后聊几个我在实际部署中反复踩过的边角问题,虽然不直接影响调度算法,但每次翻车都跟"调度"二字有关。
第一件是资源请求和限额的设置有讲究。很多团队习惯把Pod的Request写得特别低、Limit写得特别高,试图提高装箱率。这确实能让调度器塞下更多Pod,但一旦节点实际负载撞到Limit上限,内核会开始出现CPU Throttling,延迟直线上升。调度器看不懂这个"假象",它只会按Request做决定。所以我建议也设置一下命名空间级别的ResourceQuota,防止团队之间堆高Limit导致节点超卖太狠。
第二件是节点的标签管理。亲和性、污点、拓扑分布约束都依赖标签,标签一乱,调度全乱。生产环境最好对标签做统一规范:哪些标签是基础设施层的(比如node-role),哪些是业务层的(比如app-name),谁有权限增删,都要有明确约速。有次我们排查一个Pod分配不均匀的问题,最后发现是有个节点漏打了区域标签,调度器以为它不在任何可用区,导致大量Pod挤在了其他区域内。
第三件是调度器自身的高可用。kube-scheduler默认是多副本部署的,副本间通过选主机制(leader election)来保证同一时刻只有一个调度器在真正干活。很多人以为多副本就是多个调度器一起干活,不是的——只有leader在处理请求。这也就意味着,就算你有两个kube-scheduler副本,如果选主机制出问题或者leader节点网络抖动,调度任务照样会停摆。建议监控一下kube-scheduler的leader状态,这个指标出问题的时候,集群表面上一切正常,但所有新Pod都会被卡在Pending。
最后说个团队文化层面的事。调度器是基础设施,但影响的是所有人的业务体验。最好建一个"调度资源申请表"和配套的规范文档,明确说明什么样的场景该用亲和性、什么样的场景该用污点、什么样的配置可以放宽Request。很多调度问题根本不是技术故障,而是"没人知道这个集群还有这个约束",结果新服务一上来就撞上历史遗留的调度策略,排障排了两天最后发现加一个toleration就完事了。
8. 调度器的下一步:从"单集群"走向"多集群统一调度"
现在很多公司不止一套Kubernetes集群,有按地域分的,有按环境分的,有按业务部门分的。虽然各集群内部调度器还没有到必须统一改造的程度,但我发现越来越多的团队已经在讨论"多集群调度"的问题。
这个问题核心是:当一个业务应用需要跨集群扩容时,谁来决策放到哪个集群?如果每个集群的调度器只关心自己集群内部的节点,那跨集群的选择就只能靠上层流量调度(比如Ingress权重、DNS轮询)或者人工来决策。但这不够精细——如果上海集群CPU已经跑满90%但杭州集群只有40%,流量权重还硬按50/50分配,那就白白浪费了杭州的算力。
目前业界的思路是在"调度器"之上再做一个"集群选择层",或者说是一个"集群级别调度器"。它负责回答"A应用要部署,该去上海还是杭州、还是两地都部署",而下层每个集群的调度器,再负责在这个集群内部找到最合适的节点。
这个分层架构的好处是:上层的决策可以综合网络延迟、成本、数据主权、容灾等级这些全局因素;下层的调度器不需要也没办法了解全局,只做好自己内部的资源分配就行。我判断未来一两年这个方向会有更多产品化落地机会。
不过这也不意味着每个人都得立刻开始搞多集群统一调度。如果你公司目前只有一两个集群、规模也不大,那单集群内部的调度优化反而更值得投入。先把节点的标签规范、资源配额、污点隔离这些基本功做好,比引入一个复杂的大一统调度器更实际。技术选型永远不要为了赶概念而超前,把当下集群里的水端平了,比什么都强。