12.2 单体调度 (Borg)

一句话

Borg 的调度是 “一个大脑 + 悲观并发”:所有任务进同一个队列,调度器独占全局状态、串行决策;没有冲突、逻辑简单,代价是吞吐上限与单点可用性——Google 用 “多个 cell(集群)” 而不是 “一个巨型调度器” 来解决规模问题。

核心模型

概念 含义 调度含义
Cell 一个 Borg 集群(数万台机器) 调度域;跨 cell 不共享状态
Alloc 预留的资源块(相当于 K8s 的 Pod 资源) 任务装在 alloc 里,alloc 决定 “占座”
Job / Task 一组同构任务 / 单个实例 任务是最小调度单位
优先级 / 配额 生产 > 批处理;按团队配额 高优先级可抢占低优先级
Machine 打分 可行性过滤 + 打分选优 打分包含 “已用资源、故障域、镜像本地性”

调度循环三步:可行性过滤(feasible)→ 打分(score)→ 绑定(bind)。这是所有调度器的通用骨架,Borg 的特别之处是 “全局状态 + 串行”。

实测:单体调度没有冲突,但有吞吐上限

模拟 300 节点 × 8 核、提交 2000 个 1 核任务:

架构 成功放置 冲突/拒绝
单体调度 2000/2000 0
两层(offer) 1860/2000 7.0% 拒绝
共享状态(K=4) 1878/2000 23.1% 冲突重试

单体调度的 “零冲突” 是它最大的优点:决策串行,不需要版本校验。它的代价在 “串行” 本身——吞吐受限于单调度器每秒能评估多少任务。

吞吐 ≈ 1 / 单次调度耗时(过滤 + 打分 + 绑定)
单次 1ms → 约 1000 任务/秒;单次 10ms → 100 任务/秒

这也解释了为什么 “大集群 = 拆 cell”,而不是 “让一个调度器更快”。

代码:单体调度的骨架

func (s *Scheduler) Schedule(task Task) (Node, error) {
    feasible := []Node{}
    for _, n := range s.nodes {                 // 全局状态,无并发
        if n.Free(task) && n.Match(task.Constraints) {
            feasible = append(feasible, n)
        }
    }
    if len(feasible) == 0 { return Node{}, ErrNoFit }
    best := feasible[0]
    for _, n := range feasible[1:] {            // 打分:最少剩余 / 最分散
        if s.score(n, task) > s.score(best, task) { best = n }
    }
    return best, s.bind(task, best)             // 绑定成功即占座
}

反模式

反模式 后果
单调度器不做多副本/快速恢复 调度器挂 = 集群 “冻结”(任务不调度,但已在跑的继续跑)
用一个巨型 cell 承载所有业务 单次调度变慢、故障域过大
打分函数只看 CPU 内存/GPU/拓扑被忽略,放置后 OOM
可行性过滤里做重计算(如全量镜像检查) 单次调度耗时上升,吞吐骤降
无优先级队列 批处理任务堵住生产任务
不支持抢占 高优先级任务排不上,SLO 无法保证

三条铁律

🔑 单体调度的优势是 “零冲突”,代价是 “吞吐与单点”。

🔑 规模上去先拆 cell,而不是把调度器写得 “更快”。

🔑 可行性过滤要廉价,重计算放到打分或异步。

自检

Q1 为什么 Borg 用 “多个 cell” 而不是 “一个更大的调度器”?

▸ 答案:因为调度吞吐受单次决策耗时 × 串行度限制:集群越大,节点与任务越多,过滤与打分越慢,且单点故障影响面越大。拆 cell 让每个调度器只面对 “数千台机器”,既恢复吞吐,也把故障域限制在一个集群内。

Q2 “悲观并发” 具体悲观在哪?

▸ 答案:悲观在 “假设冲突一定会发生”:所以干脆不让并发决策——所有任务串行经过同一个调度器,独占状态。这样不需要版本号、不需要重试,代价是并发度 = 1。它适合 “正确性优先、规模可控” 的场景。

Q3 alloc(资源预留)解决了什么问题?

▸ 答案:解决 “资源占座”:任务还没真正跑起来(镜像拉取、启动中)时,资源已经被预留,避免被别的任务抢走。它把 “调度决策” 与 “实际使用” 解耦,是 “在共享集群里做容量保证” 的基本手段。

Q4 为什么 “打分函数” 往往比 “过滤” 更影响结果?

▸ 答案:因为过滤只排除 “放不下” 的节点,通常还剩很多候选;最终放哪由打分决定。打分决定装箱率、故障域分散、热点分布——这些直接体现为 “利用率” 与 “稳定性”。所以调度的 “艺术” 主要在打分权重。

Q5 单调度器挂掉时,已有任务会怎样?

▸ 答案:继续运行(调度器不参与运行时),但新任务无法调度、扩缩容与故障转移会停摆。这就是为什么需要 “调度器多副本/快速接管”,并且要有 “降级模式”(如按预置策略放行关键任务)。

Q6 为什么 “无优先级 + 无抢占” 的集群一定会出现排队?

▸ 答案:因为容量是有限的,而任务的到达是随机的:一旦长任务占满资源,短任务只能等。优先级让 “重要的先做”,抢占让 “已经占着的也能让出来”。缺了这两样,集群只能在 “先到先得” 下堆积。

Q7 单体调度与 K8s 默认调度器的关系?

▸ 答案:K8s 默认调度器是单体式的:单进程、通过 informer 维护本地缓存、串行调度(可多副本但同一时刻只有一个 leader)。它借鉴了 Borg 的思路(过滤 + 打分 + 绑定),但用 list-watch 与 etcd 做状态同步,而不是共享内存。

关联