Skip to content

执行器加了一倍,测试还是跑得一样慢:能力匹配、队头阻塞与瓶颈迁移 ​

一个自动化测试平台,浏览器用例和真机用例共用一个任务队列。大家抱怨排队太久,于是浏览器执行器从 5 台加到 10 台,结果浏览器用例的等待时间一点没变。后来改了调度逻辑,浏览器用例快了,可整批测试的完成时间仍然不变;再后来真机也加了,执行器很多,任务却一个一个慢慢地开始。

测试执行、构建、批处理这类平台,执行器往往不是一样的:有的带 Chrome,有的接着一台安卓设备,有的有 GPU。任务声明自己需要什么能力,调度器把它派给有这种能力的空闲执行器。这里有三个容易被忽略的问题:调度策略本身会让一种任务挡住另一种;扩容的收益取决于瓶颈在哪种能力上;执行器都够了以后,瓶颈可能搬到调度器自己身上。

本文用一个离散时间的确定性模拟把这几种情况算出来:40 个任务同时到达,每 5 个里有 1 个需要 android(耗时 6 刻),其余需要 chrome(耗时 2 刻)。这是一个模拟,不代表任何具体平台的数字。实验见文末。

任务队列android · chrome · chrome…调度器按能力匹配 · 派发速率chrome × 52 刻 / 个android × 16 刻 / 个 · 瓶颈加 chrome整批不变加 android48 → 24 刻chrome 任务全部完成的时刻只看队头:44 刻按能力匹配:14 刻调度器每刻派发 1 个:41 刻每解决一个瓶颈,先找下一个:按能力看利用率,再看调度器的派发速率执行器崩溃时,没有租约的任务永远停在运行中;有租约的到期后回到队列
图 1 · 实测(离散时间模拟,40 个任务,8 个需要 android):只看队头时 chrome 任务全部完成要 44 刻,按能力匹配只要 14 刻;chrome 从 5 台加到 10 台整批时间不变,android 加 1 台整批时间从 48 刻减到 24 刻;调度器每刻只派发 1 个时,执行器再多也要 41 刻。

一、队头阻塞:只看第一个任务 ​

最简单的调度器只看队列里的第一个任务:有匹配的空闲执行器就派发,没有就等。

python
if policy == "head":
    pick = 0 if free[queue[0][1]] > 0 else None
else:   # 按能力匹配:在队列里找第一个有空闲执行器的任务
    pick = next((i for i, (_, cap) in enumerate(queue) if free[cap] > 0), None)
text
只看队头,chrome 5 台、android 1 台:全部完成 48 刻,chrome 任务全部完成 44 刻、平均等待 21.0 刻
按能力匹配,chrome 5 台、android 1 台:全部完成 48 刻,chrome 任务全部完成 14 刻、平均等待 5.4 刻
只看队头,chrome 加到 10 台:全部完成 48 刻,chrome 任务全部完成 44 刻、平均等待 21.0 刻(被排在前面的 android 任务挡住)

只看队头时,唯一的 android 执行器一忙,排在队头的 android 任务就让整个队列停下,后面的 chrome 任务明明有空闲执行器也开不了。所以加 chrome 执行器毫无作用,开头那次扩容就是这样。改成按能力匹配后,chrome 任务的平均等待从 21 刻降到 5.4 刻。

按能力匹配也有代价:总是跳过当前做不了的任务,需要稀缺能力的任务可能一直被插队。实际的调度器会按能力分队列、给等待太久的任务加优先级,或者为稀缺能力预留配额。

二、扩容的收益取决于瓶颈在哪 ​

text
按能力匹配,chrome 加到 10 台:全部完成 48 刻,chrome 任务全部完成 8 刻、平均等待 2.2 刻(android 才是瓶颈)
按能力匹配,chrome 5 台、android 加到 2 台:全部完成 24 刻,chrome 任务全部完成 14 刻、平均等待 5.4 刻

8 个 android 任务、每个 6 刻、只有 1 台设备,整批最少就要 48 刻。chrome 加再多,只能让 chrome 任务完成得更早,整批的完成时间不变;android 加 1 台,整批时间减半。扩容前先问:要缩短的是哪一类任务的等待,那类任务的瓶颈是哪种能力。看总的排队长度是看不出来的。

三、执行器够了,瓶颈搬到调度器 ​

text
按能力匹配,chrome 10 台、android 4 台,调度器每刻只能派发 1 个:全部完成 41 刻,chrome 任务全部完成 41 刻、平均等待 20.0 刻,利用率 chrome 16%、android 29%
同样的执行器,调度器不限速:全部完成 12 刻,chrome 任务全部完成 8 刻、平均等待 2.2 刻

调度器每次派发都要做事:分配环境、准备数据、拉起执行器、记录状态。它的速率是有限的。执行器足够多时,这个速率就成了新的瓶颈:40 个任务至少要 40 刻才能全部开始,执行器的利用率只有一到三成。开头最后那个「执行器很多,任务却一个一个慢慢开始」就是这种情况。

每解决一个瓶颈,都要重新找下一个:排队发生在哪里、哪种资源的利用率接近 100%。这与依赖图执行里「并发上限决定能不能接近关键路径」是同一个道理。

四、执行器崩溃:任务要有租约 ​

text
android 2 台、其中一台第 3 刻崩溃、没有租约:完成 39 个,丢失 1 个
同样崩溃、租约 10 刻:完成 40 个,丢失 0 个,全部完成 48 刻(只剩 1 台 android,比不崩溃时的 24 刻慢一倍)

执行器在运行中崩溃,它手上的任务如果只是标记为「运行中」,就再也不会有人处理。派发时给任务一个租约,执行器定期续租;租约到期没有续,任务回到队列。代价是同一个任务可能被执行两次(原来的执行器也许只是网络断了),所以任务要能安全重跑。租约、重新认领和旧持有者的写入怎样处理,见用数据库表当队列。

五、检查清单 ​

问题做法
一种任务挡住另一种按能力匹配或按能力分队列;为稀缺能力的任务防止饥饿
扩容没效果先确定要缩短哪类任务的等待,看那类能力的利用率
执行器空闲而任务在排队检查调度器自身的派发速率、环境准备与数据准备
执行器崩溃任务租约与续租;任务可安全重跑
观测按能力分别统计排队时长、利用率与派发速率,而不是只看总队列长度

配套实验

参考资料

文章以 CC BY-NC-SA 4.0 授权 · 代码片段以 MIT 授权