一个 100 秒的长任务排在前面,后面 8 个 1 秒的任务每个都要等 100 秒。这叫护航效应。 换成最短优先,平均完成时间从 104 秒降到 16 秒——同样的活,只换顺序。 加执行槽有效但边际递减;并行度越高,调度策略越不重要。
上一篇讲三个瓶颈的解法。这一篇讲谁来分派这些活。 教材 3.6、3.7 两节:资源管理器、分布式处理的开发方式。 3.6 讲的是「多个任务抢有限资源时谁先上」,3.7 讲的是「这套东西你是自己搭还是买」。
一、一个长任务能拖死一整队
假设有 9 个任务:1 个要跑 100 秒,8 个各要 1 秒。只有 1 个执行槽。 按到达顺序(先来先服务):大任务先跑 100 秒,8 个小任务排在后面,每个都要等到 100 秒之后才开始。 结果每个小任务的完成时间分别是 101、102……108 秒。平均完成 104 秒。
换成最短优先:8 个小任务 8 秒全做完,大任务排最后。平均完成 16 秒。 同样的活、同样的机器,只换了个顺序,平均等待时间少了 6.5 倍。这个现象有个名字,叫护航效应。 它解释了一件很常见的事:为什么夜里跑的批处理任务,早上老是超时。 不是算力不够,是那个最大的任务堵在最前面。
二、加槽有用,但会递减
那多加几个执行槽呢?演示算了 1、2、4 个槽三种情况。 1 个槽:FIFO 104 秒、最短优先 16 秒。2 个槽:15.1 秒、13.8 秒。4 个槽:12.8 秒、12.7 秒。 从 1 到 2,降了将近七倍;从 2 到 4,只降了 15%。边际收益断崖式下降。
更有意思的是第四列:槽越多,两种策略的差距越小(6.5 倍 → 1.1 倍 → 1.0 倍)。 并行度够高的时候,调度策略几乎不重要;串着跑的时候,它决定一切。 这句话反过来也成立:如果你的系统本来就是串行的,先别急着加机器,把队列顺序理顺可能更划算。
三、策略各有各的毛病
先来先服务:公平、可预测,但有护航效应,平均等待长。适合夜里跑报表那种批处理。 最短优先:平均完成时间最短,但小任务源源不断地来时,大任务可能永远排不上。这叫饥饿。 适合交互式查询——用户等不了太久。 公平调度:每个队列都有保底份额,多团队共用集群时用。代价是总吞吐略低。
真实系统用的通常是「带权重的公平调度」:既给每个队列保底,又让短的先跑。 YARN、Kubernetes 的调度器,核心就是这个平衡。它们不只管「有多少资源」,还管「谁先上」。
四、自己搭还是买
教材 3.7 节把三条路并列:Hadoop 那套、自行开发、云服务。 Hadoop 那套:你写 Map 和 Reduce,调度、容错、数据分布都由平台管。代价是生态重、调优难。 自行开发:全部自己来,出错全自己扛。只在有特殊需求时才值得。
云服务:你只管业务逻辑,基础设施全托管。代价是按量付费,而且数据在别人机器上。 选择标准其实很简单:你的团队有没有人能 24 小时盯着集群。有就自建省钱,没有就上云省心。 别被「自建更有掌控力」这种话带偏。掌控力是给有余力的团队的,没余力的时候它就是负担。
五、动手 Demo:一个长任务堵住 8 个小任务,能差多少
9 个任务抢 1 个执行槽,比较先来先服务和最短优先。 再加到 2 个、4 个槽,看收益怎么递减。 最后列三种调度策略和三种开发方式的取舍。
def run_fifo(durs, k):
slots = [0.0] * k
done = []
for d in durs: # 按到达顺序取
i = slots.index(min(slots))
slots[i] += d
done.append(slots[i])
return done
def run_sjf(durs, k):
slots = [0.0] * k
done_at = [0.0] * len(durs)
for i in sorted(range(len(durs)), key=lambda j: durs[j]):
j = slots.index(min(slots))
slots[j] += durs[i]
done_at[i] = slots[j]
return done_at
durs = [100.0] + [1.0] * 8 # 1 个长的 + 8 个短的
print('FIFO 平均', sum(run_fifo(durs, 1)) / 9)
print('SJF 平均', sum(run_sjf(durs, 1)) / 9)
实跑输出:
--------------------------------------------------------------------
谁来调度:一个长任务能拖死一整队
--------------------------------------------------------------------
① 先看一个经典场面
--------------------------------------------------------------------
9 个任务:1 个要跑 100 秒,8 个各要 1 秒。
只有 1 个执行槽。
| 任务 | 时长 | FIFO 完成于 | 最短优先完成于 |
|---|---:|---:|---:|
| 大任务 | 100 秒 | 100 秒 | 108 秒 |
| 小任务1 | 1 秒 | 101 秒 | 1 秒 |
| 小任务2 | 1 秒 | 102 秒 | 2 秒 |
| 小任务3 | 1 秒 | 103 秒 | 3 秒 |
| 小任务4 | 1 秒 | 104 秒 | 4 秒 |
| 小任务5 | 1 秒 | 105 秒 | 5 秒 |
| 小任务6 | 1 秒 | 106 秒 | 6 秒 |
| 小任务7 | 1 秒 | 107 秒 | 7 秒 |
| 小任务8 | 1 秒 | 108 秒 | 8 秒 |
平均完成时间:FIFO 104.0 秒,最短优先 16.0 秒
FIFO 下,8 个 1 秒的任务全被那个 100 秒的堵在后面,每个都要等 100 秒。这叫护航效应。
最短优先把它们提到前面,8 个任务 8 秒全做完,大任务排最后。
平均完成时间从 104.0 秒降到 16.0 秒——同样的活,只是换个顺序。
② 加一个槽,还能快多少
--------------------------------------------------------------------
| 执行槽 | FIFO 平均完成 | 最短优先平均完成 | 两者差几倍 |
|---|---:|---:|---:|
| 1 个槽 | 104.0 秒 | 16.0 秒 | 6.5 倍 |
| 2 个槽 | 15.1 秒 | 13.8 秒 | 1.1 倍 |
| 4 个槽 | 12.8 秒 | 12.7 秒 | 1.0 倍 |
加槽确实有用:1 个槽 104 秒,2 个槽 15.1 秒——降了将近七倍。
但 2 个槽到 4 个槽只从 15.1 降到 12.8,边际收益断崖式下降。
更值得注意的是第四列:槽越多,两种策略的差距越小(6.5 倍 → 1.1 倍 → 1.01 倍)。
并行度够高的时候,调度策略几乎不重要;串着跑的时候,它决定一切。
这句话反过来也成立:如果你的系统本来就串行,先别急着加机器,把队列顺序理顺可能更划算。
③ 最短优先的毛病:长的会被饿死
--------------------------------------------------------------------
假如小任务源源不断地来,最短优先永远先做小的,那个 100 秒的大任务可能永远排不上。这叫饥饿。
| 策略 | 好在哪 | 坏在哪 | 适合谁 |
|---|---|---|---|
| 先来先服务 | 公平、可预测 | 护航效应,平均等待长 | 批处理跑报表 |
| 最短优先 | 平均完成时间最短 | 长任务可能饿死 | 交互式查询 |
| 公平调度 | 每个队列都有份 | 总吞吐略低 | 多团队共用集群 |
真实系统里通常是「带权重的公平调度」:既给每个队列保底份额,又让短的先跑。
第 3.6 节讲的资源管理器,核心就是这个平衡。
④ 三种开发方式,各自要付什么
--------------------------------------------------------------------
| 方式 | 你负责 | 平台负责 | 代价 |
|---|---|---|---|
| Hadoop 那套 | 写 Map 和 Reduce | 调度、容错、数据分布 | 生态重,调优难 |
| 自行开发 | 全部 | 没有 | 出错全自己扛 |
| 云服务 | 业务逻辑 | 全部基础设施 | 按量付费,数据在别人机器上 |
教材 3.7 节把这三条并列,其实选择标准很简单:你的团队有没有人能 24 小时盯着集群。
有就自建,没有就上云。
这两个函数做的事很简单:维护 k 个槽的累计时间,每个任务挑最早空出来的那个。 真实调度器还要考虑优先级、配额、抢占,骨架就是这个。 第一张表里那一列「FIFO 完成于」很说明问题:8 个小任务全挤在 101 到 108 秒,每个都干等了 100 秒。 第二张表的重点是第四列。槽越多,策略差距越小——这是在告诉你「先解决并行度,再优化调度」。 第三张表和第四张表是概念对照,不是跑出来的数。策略好坏要看你的任务分布,没有普适答案。
30 秒实验
- 把 8 个小任务改成 80 个,FIFO 和最短优先的差距是变大还是变小?
- 给
run_sjf加一条规则:等超过 50 秒的任务提权。看大任务的完成时间变多少。 - 把 k 改成 9(每个任务一个槽),两种策略的结果应该一样——想想为什么。
常见误区
- 「加机器就能解决排队。」加槽有用,但边际递减;而且加槽解决不了「一个超大任务堵住所有人」。
- 「最短优先最好,都改它。」它会让长任务饿死。真实系统必须加保底或提权。
- 「自建集群更省钱。」先算人工成本。没人 24 小时盯着,省下的钱迟早赔进故障里。
自测三问(点开对答案)
1. 为什么 FIFO 下 8 个小任务每个都要等 100 秒? —— 因为执行槽只有一个,大任务占着它跑 100 秒,后面所有任务只能排队。 等待时间等于前面所有任务的总时长。 2. 执行槽从 2 个加到 4 个,为什么提升很小? —— 因为总工作量没变,瓶颈已经不是槽数了。而且大任务本身就要 100 秒,它决定了最短完工时间下限。 3. 什么时候该用最短优先,什么时候该用公平调度? —— 交互式查询(用户等不了)用最短优先; 多团队共用集群用公平调度,否则某个团队的批任务会把别人的交互查询全堵死。下一篇进第 4 章:从数据里找规律,也就是机器学习。
- ☐ (本篇完,下一篇 4.1 机器学习就是找一个函数)