61.3 批处理与调度
一句话
批处理调度的目标是把 makespan(完成时间)压到最小,但它受两条硬约束:关键路径(依赖链上必须串行的时长)与并行度;并行度超过 “关键路径允许的宽度” 之后,再加资源也不会更快。
调度器与依赖
| 层 | 解决什么 | 典型实现 |
|---|---|---|
| 资源调度 | 谁先拿 CPU/内存 | FIFO(简单)、Capacity(队列配额)、Fair(动态均分) |
| 作业调度 | 何时跑、依赖谁 | DAG 调度器(Airflow/DolphinScheduler/Azkaban) |
| 任务调度 | 单个 task 放哪台机器 | YARN/K8s 的容器分配 |
常见误区:把 “加机器” 当万能解。真正决定批处理时长的是 DAG 形状:串行链路多长、每层能并行多少。
实测:并行度与关键路径
10 个任务的 DAG(1→2,3;2,3→4;4→5..8;5..8→9;9→10),关键路径 1-2-4-5-9-10 共 20 个时间单位:
| 并行度 | makespan | 相对关键路径 |
|---|---|---|
| 1 | 28 | 1.4× |
| 2 | 22 | 1.1× |
| 4 | 21 | 1.05× |
| 8 | 21 | 1.05× |
两个结论:① 并行度从 1 提到 4,makespan 从 28 降到 21(接近关键路径下限);② 从 4 提到 8 完全没有收益——因为 DAG 的宽度不够,多出来的槽位没任务可跑。所以 “扩容前先看关键路径”。
下界:makespan ≥ 关键路径长度
上界:makespan ≤ 总工作量(完全串行)
加班/加机器只能逼近下界,不能突破它
代码:DAG 依赖与关键路径
# 关键路径 = 依赖链上 “任务时长之和” 的最大值
def critical_path(tasks, dur, deps, memo={}):
def f(t):
if t in memo: return memo[t]
memo[t] = dur[t] + max([f(d) for d in deps[t]], default=0)
return memo[t]
return max(f(t) for t in tasks)
# 排期时:先保证关键路径上的任务不被排队/抢占,否则整体直接被拖长
工程含义:关键路径上的任务要优先调度、避免被抢占;非关键路径上的任务可以 “晚点跑”,它们有浮动时间。
反模式
| 反模式 | 后果 |
|---|---|
| 不看 DAG 形状就加资源 | 关键路径不变,钱白花 |
| 关键路径任务被低优任务抢占 | 整体时长被拖长 |
| 调度器按 FIFO 且无队列隔离 | 大作业堵死小作业 |
| 依赖关系写在文档里而非代码里 | 补数/重跑时顺序错乱 |
| 补数任务与日常任务抢资源 | 双跑导致数据重复或延迟 |
| 没有 “重跑幂等” | 失败重跑产生重复分区 |
三条铁律
🔑 批处理时长由关键路径决定;扩容只对 “宽度不足” 的部分有效。
🔑 关键路径任务优先,非关键路径给浮动时间。
🔑 调度依赖要写进代码(DAG 定义),不要靠文档约定。
自检
Q1 为什么并行度从 4 加到 8 完全没收益?
▸ 答案:因为 DAG 的并行宽度不够:在关键路径的某些层,最多只有 4 个可并行任务(如 5..8 一层 4 个),第 5–8 个槽位没有任务可跑。此时瓶颈是 “关键路径长度”(20 个时间单位),不是资源。加资源只会让机器闲置。
Q2 什么是 “关键路径”,为什么它给出下界?
▸ 答案:关键路径是依赖链上时长之和最大的那条路径(本实验是 20)。因为链上的任务必须串行,任何调度都无法让它们重叠,所以完成时间不可能小于这条链的长度。它是 makespan 的理论下界。
Q3 为什么关键路径上的任务不能随便被抢占?
▸ 答案:因为关键路径没有浮动时间:它一旦延迟,整个作业的完成时间就等量延迟。而非关键路径有浮动时间(可以晚点跑而不影响总时长)。所以调度策略应是 “保护关键路径、让非关键路径让路”。
Q4 批处理的 “补数” 为什么容易出事故?
▸ 答案:因为补数通常要重跑历史分区:如果不幂等,会重复写入;如果与日常任务并发,会争抢资源或写同一张表;如果依赖顺序错乱,会读到不完整数据。正确做法是 “补数任务走独立队列 + 写临时分区 + 校验后切换 + 幂等覆盖”。
Q5 FIFO、Capacity、Fair 三种调度器怎么选?
▸ 答案:FIFO 最简单,适合单团队、可预测负载;Capacity 按队列预留配额,适合多团队 “各保底”;Fair 动态均分空闲资源,适合负载波动大的共享集群。共同点是都需要 “队列隔离”,否则大作业会堵死所有人。
Q6 为什么 “小文件” 会让批处理变慢?
▸ 答案:因为每个小文件可能对应一个 task:task 启动、调度、JVM 初始化开销远大于处理几 KB 数据。结果是 “任务数爆炸、单个任务极短”,时间全花在调度上。解决是 “合并小文件” 或 “读取时按大小合并分区”。
Q7 如何判断一个批作业 “还能不能优化”?
▸ 答案:对比 makespan / 关键路径 的比值:接近 1 说明已到理论下界,只能改 DAG(减少依赖链);明显大于 1 说明有 “宽度没用满/排队/抢占”,可以加并行或调调度。另一个信号是 CPU 利用率:长期低于 50% 通常意味着关键路径或 IO 受限。