
当我发现自研调度器和 K8s Scheduler 用了同一种设计哲学引言最近我在做一个分布式任务调度平台核心功能是把任务分发到多个物理节点上执行。随着节点数量从 1 个扩展到多个我遇到了一个经典问题任务应该发给谁最开始我拍脑袋想了个办法看看哪个节点闲着就发给谁。后来需求变了——节点配置不一样了有的 CPU 多有的内存大再后来节点会离线、会重启任务不能丢。为了解决这些问题我一步步把调度逻辑从随便选一个进化成了有策略地选一个最优的。有意思的是后来我去研究 Kubernetes 调度器时发现我遇到的问题K8s 早就遇到过我摸索出来的方案恰好是 K8s 调度器的一个简化版本。我跟身边同事聊过很多人觉得调度器很难但拆开看其实就是排序加过滤。这篇文章不讲理论就讲我踩过的坑。如果你也写过类似的代码可能会觉得很有意思。第一部分我遇到的问题我的系统架构大致是这样一个中央服务管理 N 个 Agent 节点。每个 Agent 部署在一个物理站点上负责执行具体任务。中央服务收到任务后需要决定发给哪个 Agent。一开始只有 2 个节点随便选都行。但随着节点增多三个问题开始浮现第一节点能力不同。有的节点是 16 核 32G 的高配服务器有的只是 4 核 8G 的工控机。把计算密集型任务发给弱节点它根本跑不动。第二节点负载不均。如果不看负载轮询分发最后一定会出现某些节点忙死、某些节点闲死的情况。第三节点可能离线。工控机所在的环境没那么稳定断电、网络抖动时有发生。调度器必须知道哪些节点还活着。所以调度器的核心职责变得清晰了在多个候选节点中选出一个当前最合适的——既要满足任务的资源需求又要考虑节点的实时状态。第二部分我的解决方案我设计了一个基于优先级的节点选择策略。核心逻辑是一段不到 100 行的 Python 代码def _select_best_node(self, nodes): def node_priority(node): # 优先级1: 没有任务的节点优先避免集中过载 has_task 1 if node.current_task_id else 0 # 优先级2: 负载水位低的优先 load node.resources.get(load_watermark, 1.0) # 优先级3: 任务数少的优先兜底 task_count node.resources.get(current_task_count, 1) return (has_task, load, task_count) # 按优先级排序选最优的 available [n for n in nodes if n.status ONLINE] return sorted(available, keynode_priority)[0] if available else None这个排序逻辑看起来简单但背后蕴含了一个重要的设计决策我选择了负载均衡优先而非性能优先。为什么不优先选性能最强的节点因为如果所有任务都往最强节点上堆它一旦过载所有任务的执行时间都会延长全局吞吐量反而下降。牺牲单任务的极致性能换取整个系统稳定性的下限——这是调度系统中一个经典的权衡。当然如果有严格的延迟要求策略可以调整——调度策略没有最好只有最适合场景。has_task 1 优先选择空载节点目的是避免单个节点过载导致整组任务延迟上升load_watermark 作为第二优先级是实时资源水位感知——与 K8s 的资源感知调度思路一致task_count 作为兜底保证极端情况下依然有确定性的选择结果。除此之外我还做了一些配套设计心跳机制每个 Agent 每 5 秒上报一次状态负载、任务数、在线状态中央服务维护一份节点列表故障标记连续 90 秒收不到心跳标记为离线不再分配新任务状态持久化节点信息和任务绑定关系存 MySQL中央服务重启后可以恢复。这套系统已经在生产环境跑了 6 个月2 个站点稳定运行经历过硬件掉线和网络抖动调度层没出过问题。第三部分Kubernetes 调度器是怎么做的带着调度器到底该怎么设计的疑问我去研究了 Kubernetes 的调度器。K8s 的调度器kube-scheduler负责把 Pod 分配到合适的 Node 上。它的调度流程分为三个阶段3.1 Filtering过滤旧称 PredicatesFiltering阶段回答的是“能不能”的问题。调度器遍历集群里所有 Node用一组过滤规则把不满足条件的节点踢掉资源是否足够CPU/内存/GPU 请求是否满足端口是否冲突节点是否打了污点Taints不匹配节点是否健康、是否 Ready如果所有节点都不满足条件Pod 就会一直处于 Pending 状态直到有节点满足条件为止。3.2 Scoring打分旧称 PrioritiesScoring阶段回答的是“哪个更适合”的问题。在通过预选的节点里调度器用一组打分函数给每个节点算分资源是否均衡分配避免节点过载节点亲和性/反亲和性镜像是否已本地存在节省拉取时间是否满足 Pod 的拓扑约束最后选得分最高的节点作为调度结果。3.3 Binding绑定选好节点后调度器把这个决定告诉 API ServerAPI Server 把绑定信息写入 etcd。被选中的 Node 上的 kubelet 监听到新 Pod 分配就开始拉镜像、启动容器。3.4 etcd调度的大脑这里有一个关键设计值得注意Kubernetes 调度器本身是无状态的。调度器每次做决策时都从etcd读取最新的集群状态。etcd 是一个分布式键值存储可以理解为 K8s 的“数据库”——节点信息、Pod 状态、资源分配等所有集群数据都存在这里。这和我的系统用 MySQL 做持久化的思路是一样的——节点信息存 MySQL中央服务重启后从 MySQL 恢复而不是依赖内存。区别只是技术选型K8s 用 etcd我用 MySQL。调度器挂了怎么办重启一个新的调度器进程它从 etcd 读数据立刻就能恢复全部状态。无状态让调度器可以水平扩展也降低了运维复杂度。3.5 节点故障后Pod 怎么办K8s 中节点故障后的 Pod 处理流程如下步骤组件行为1kubelet节点上的 kubelet 停止上报心跳2Controller Manager检测到心跳超时默认 40 秒标记节点为 NotReady3Controller Manager等待容忍时间默认 5 分钟后开始驱逐该节点上的 Pod4Scheduler被驱逐的 Pod 进入待调度队列重新执行调度流程绑定到其他节点注意Pod 的迁移是多个组件协作的结果——Controller Manager 负责检测和驱逐Scheduler 负责重新调度。不是调度器单独完成的。第四部分对比分析在展开详细对比之前先通过架构图从整体上感受两套系统的异同图1两套调度系统的架构对比左Kubernetes / 右我的系统把两套系统放在一起对比会发现很多有趣的设计异同对比维度我的调度器K8s 调度器过滤机制仅过滤“是否在线”Predicates资源、端口、污点、健康等打分机制优先级排序3 个维度Priorities10 个维度加权打分调度频率每个任务一次每个 Pod 一次状态存储MySQL持久化节点/任务状态etcd持久化集群所有状态调度器状态内存缓存 MySQL持久化重启可恢复无状态完全依赖 etcd扩展性硬编码策略插件化架构Scheduling Framework故障处理原地等待 WAL 恢复Controller Manager 驱逐 Scheduler 重新调度代码复杂度~100 行核心逻辑数万行最大的差异在于两点第一状态管理方式不同。K8s 把状态完全交给 etcd调度器本身无状态。而我的调度器在内存中维护节点列表但也同步写入 MySQL——中央重启后从 MySQL 恢复。这是一种半有状态设计内存缓存保证调度性能MySQL 持久化保证故障恢复。这个差异让我意识到把状态从调度器进程里移出去换成共享存储etcd/MySQL是让调度器获得高可用能力的关键。第二故障处理策略不同。K8s 在节点故障后由 Controller Manager 检测节点失联、驱逐 Pod再由 Scheduler 将 Pod 重新调度到其他节点。而我的系统不迁移——因为每个 Agent 绑定特定物理站点工控机任务具有物理亲和性不可漂移。所以我的方案是原地等待 WAL 恢复而不是 K8s 的自动迁移。这不是技术做不到而是业务约束不同。第五部分我可以借鉴什么研究完 K8s 调度器有几个设计思路我确实打算抄过来但有些东西我决定不学。5.1 显式的两阶段结构当前我的调度是“单步排序”把过滤是否在线和打分负载/任务数混在一起。一旦需要增加新的过滤条件比如“节点必须安装特定驱动版本”代码会迅速膨胀。如果改成 K8s 的两阶段模式Filtering阶段过滤掉不满足硬性条件的节点离线、CPU/内存请求不满足、版本不符Scoring阶段对剩余节点进行多维打分排序这只是代码结构的调整不涉及算法变化但扩展性会大幅提升。5.2 插件化架构K8s 的 Scheduling Framework 把调度能力拆成了一个个可插拔的插件。如果未来调度策略变复杂比如要支持 GPU 拓扑感知、网络延迟优先等插件化可以让新增策略不影响核心代码。5.3 无状态化改造如果未来节点数扩展到百级调度器的可用性会变得更重要。一个思路是把节点列表从内存移到 Redis/MySQL调度器每次调度时从存储读取——这样调度器就变成了无状态可以随时重启、甚至多实例部署。5.4 什么不该学不是所有 K8s 的设计都适合我。K8s 的调度器为了通用性做得极其复杂预选策略几十种、优选策略几十种、配置文件几百行。而我的业务场景相对固定“够用”比“复杂”更重要。保持调度核心在 100 行左右用扩展点应对未来变化比一开始就搞大而全的设计更明智。总结说白了调度器就是做选择题给你一堆选项你按规则挑一个。不用数学多好关键是规则合理。我花了很长时间才意识到这点——之前一直以为调度器很神秘现在看其实就是“定规则、算优先级”。回顾整个设计过程我最深的体会是从 1 个节点到 N 个节点不仅是代码量的变化更是思维方式的转变。单节点时你只需要“执行”多节点时你需要“决策”——而决策需要信息心跳、负载、资源、需要规则预选、优选、需要容错故障标记、状态恢复。如果你也写过类似的调度代码不妨去看看 K8s 调度器是怎么设计的——你会发现很多自以为原创的想法其实早有成熟的工业实现。而理解这些工业实现的设计思路比背八股文更能帮你写出更好的代码。