资讯详情

资讯详情

软件生产调度核心算法解析:从先到先得到预测式调度

我印象最深的一次事故是某天下午发布窗口前的构建队列突然堵到了几百个任务。当时所有编译机器CPU都满载但关键的那个服务镜像就是轮不上发布群里所有人都在等。后来查出来原因特别简单调度器用的是先到先得前面排了一堆定时触发的全量回归任务把资源全占了。那之后我才认真研究起软件生产调度中的资源分配算法才发现这玩意儿不是排个队就行这么简单。它直接决定了你的CI快不快、发布稳不稳、测试资源有没有被浪费。这篇文章把我从理论到落地踩过的坑、验证过的方法完整写出来适合正在维护构建集群、负责发布平台、或者想优化研发效能的同学参考。1. 为什么软件生产调度不能靠先到先得1.1 先到先得在天花板下的失效瞬间先到先得的逻辑看起来无懈可击谁先来谁先上。在没有资源争抢的时候它确实是最高效的调度策略因为它不需要任何额外计算队列本身天然有序。可一旦构建、测试、发布任务共享同一批机器瓶颈出现先到先得就会暴露出一个致命问题——它完全没有区分任务价值的能力。举个例子。你有一个8核16G的构建机池同一时间来了三个任务任务A紧急修复线上故障后触发的生产镜像构建预计跑4分钟任务B某个非核心服务的全量回归测试预计跑40分钟任务C凌晨定时任务的补跑预计跑20分钟。按照先到先得谁先提交谁优先。任务B可能在凌晨就已经进入队列等故障发生时它正好卡在最前面于是任务A只能等B跑完。线上多故障一分钟可能意味着几十万请求受影响。这不是算法问题而是调度目标问题——先到先得根本没有紧急这个维度它只在乎谁先来。所以在一个存在资源争抢的软件生产环境里调度的本质不是维护公平而是在有限资源下做价值排序。这个排序的规则就是资源分配算法的核心。1.2 资源分配算法要解决的四个核心矛盾做调度久了你会发现所有矛盾最后都能归到四个问题上。第一个是吞吐量与响应速度的矛盾。如果只追求单位时间跑完的任务数最优策略永远是优先跑短任务但短任务可能都是低价值的杂活紧急的长任务会被无限压后。第二个是公平性与关键任务保障的矛盾。每个服务团队都觉得自己重要如果完全按权重分配小团队的临时任务可能永远排不上如果完全按公平轮询大团队的核心发布又会和普通任务抢同一批资源。第三个是资源利用率与稳定性的矛盾。把资源池塞得越满利用率越高但一旦出现突发任务或依赖重试整个池子可能瞬间雪崩。留有空闲看起来浪费反而是稳定性的保证。第四个是全局最优与局部实时的矛盾。理论上你可以把所有任务放到一个全局模型里求最优解但任务随时到达、随时取消在线求解的代价往往超过收益真正落地时还是要靠启发式策略。这四个矛盾不会消失只会随着团队规模变大而越来越尖锐。理解它们才能理解为什么没有万能算法只有适合场景的取舍。1.3 适用范围不只是CI还有测试与发布很多人一提调度就想到CI构建其实在现代软件生产链路里需要资源分配的环节至少有四个构建调度把源码编译、镜像打包任务分配到构建机群核心指标是等待时间和构建成功率测试调度把单元测试、集成测试、端到端测试分发到测试环境或云真机核心指标是回归耗时和资源成本发布调度把灰度批次、金丝雀发布、多区域部署的窗口编排好核心指标是发布风险和回滚速度环境调度动态分配测试环境、预发环境避免环境互相污染核心指标是冲突率和准备时间。这四个场景对算法的要求不太一样。构建调度偏重优先级和依赖测试调度偏重成本和并行度发布调度偏重窗口和风险控制环境调度偏重隔离策略。但它们的底层模型是一致的一组任务、一组资源、一组约束、一个目标函数。这篇文章后面讲的所有算法和工程细节都可以在这四个场景里复用。2. 常见调度算法家族从贪心到预测式2.1 贪心与短作业优先最快的不等于最优的贪心算法是调度入门的必修课核心思想是每步都做当前看起来最优的选择。对应到任务调度最常见的形式是短作业优先每次从队列里挑预计执行时间最短的任务先跑。短作业优先能把平均等待时间压得很低这是有理论依据的。假设有四个任务耗时分别是1分钟、10分钟、20分钟、2小时按短作业优先的顺序跑完平均等待时间明显比按长作业优先低。这个优势在任务量大、执行时间差异大的测试场景里特别明显——先跑完快的用例能快速反馈同时让更多任务进入执行。但它的毛病也突出长作业可能会被饿死。只要有短任务持续进入一个20分钟的任务就可能永远排在后面。另一个问题是它对预估时间的准确性极度敏感。构建时间可能受缓存命中率影响测试时间可能受数据量影响一旦估短了排序就会失真。我见过一个团队上了短作业优先之后单测平均等待时间下降了三倍但一个大服务的端到端测试从30分钟排队变成3个小时。后来他们不得不加了一个最长等待时间兜底任何任务排队超过45分钟直接提升到队首。这个兜底本质上是对贪心策略的修正——贪心可以负责效率但公平必须由另外的规则兜底。2.2 优先级和加权轮询把重要翻译成数字比贪心更进一步的是优先级调度。每个任务带一个优先级数字调度器每次从最高优先级的任务开始分配资源。优先级可以来自任务类型线上构建 测试 文档生成也可以来自业务维度核心服务 边缘服务。但裸优先级调度有一个经典问题高优先级任务源源不断时低优先级任务永远没有机会。这时候就需要加权轮询来混合。简单说就是给不同优先级的任务分配不同的配额比如高优先级任务占60%的资源中优先级占30%低优先级占10%然后在每一档内部再做轮询。加权轮询的核心是把重要程度翻译成一个可配置的数字。这个数字怎么定我的经验是两个来源一是服务等级协议里的容忍时间容忍时间越短的任务权重越高二是故障影响面影响用户量越大、影响收入越直接的权重越高。这里不建议搞太细的权重表3到5档就够了否则运维同学记不住配置流于形式。另一个值得注意的点是权重最好做成动态的而不是静态的。比如发布窗口前的一个小时发布相关任务的权重自动上调凌晨低峰期长耗时测试任务的权重上调。动态调整让调度器更有感知但实现时要注意别把规则搞得太复杂否则又变成了一团理不清的配置。2.3 最小松弛度算一算每个任务离死线有多远优先级和权重解决的是谁更重要最小松弛度解决的是谁更紧急。松弛度的定义很简单任务的截止时间减去预估执行时间再减去当前时间。松弛度越小说明任务越接近它的最后期限越应该优先调度。举个具体例子。任务A截止时间是10:00预估耗时30分钟现在是9:10松弛度就是20分钟任务B截止时间是9:40预估耗时5分钟现在是9:10松弛度就是25分钟。虽然B的截止时间更早但按照松弛度排序A更紧迫应该先跑。这个算法的优势在于它天然处理了紧急但不重要和重要但不紧急两类任务的权衡。而它的难点在于截止时间怎么定。在软件生产调度里任务的截止时间往往不是硬性的而是可以协商的。比如一个回归测试任务你说它的截止时间是中午12点那么12点前跑完就算准时超了就影响下午的发布。我建议先把截止时间分成两类硬截止和软截止。线上故障修复镜像的构建完成时间是硬截止晚一分钟都有严重后果常规测试任务是软截止超时可以接受但会留下记录。对硬截止任务用松弛度排序对软截止任务再用权重或贪心策略补充混合调度的效果通常比单用一种好得多。2.4 预测式调度与约束优化把分配问题交给解算器前面几种算法都属于启发式它们快、简单、可解释但不保证最优。如果资源规模大、约束复杂还有一个思路把调度问题建模成约束优化问题交给解算器去求。约束优化模型一般长这样变量是每个任务分配到哪个资源、什么时间开始约束是资源容量限制、依赖先后关系、窗口时间限制目标函数是最小化总完成时间或最大化按时交付率。这类问题在学术上叫作业车间调度问题属于NP困难问题规模一大直接求精确解是不现实的一般会用线性规划松弛、遗传算法、模拟退火这些近似方法来解。还有一个方向是预测式调度。它把历史构建记录、测试耗时、资源排队情况喂给机器学习模型预测未来一段时间内任务的变化和资源的需求量提前调整配额或扩容。比如周一早上的提交高峰、版本发布日的构建洪峰完全可以从历史数据里看出来。不过我不建议中小团队一上来就上预测式或约束优化。原因很现实这类系统的维护成本高模型需要持续训练解算器需要调参出了问题还很难排查。大多数团队用启发式算法加一个简单动态扩缩容机制就已经能解决80%的问题。预测式适合超大规模、资源成本压力极大的场景等你的任务量达到每天上万次再考虑也不迟。算法核心逻辑优点缺点适用场景先到先得按提交顺序分配实现简单、公平感强无法区分任务价值资源充足的场景短作业优先预估时间最短先跑平均等待时间低长作业可能饿死测试用例调度优先级按重要性排序关键任务有保障低优先级可能饿死线上构建优先加权轮询按权重配额轮询兼顾公平与重要度权重配置依赖经验多团队共享集群最小松弛度按剩余缓冲时间排序应对截止时间有效依赖截止时间设定发布窗口调度约束优化数学建模求最优理论上限高计算复杂度高、维护难大规模资源规划预测式调度基于历史预测需求提前感知洪峰依赖数据与模型超大规模任务平台3. 算法之外的工程细节资源画像、抢占与指标3.1 资源画像先搞清楚调度对象到底是什么很多团队在调度上踩坑根源不在算法而在资源模型太粗糙。你调度的是构建机池可构建机池里面的机器配置一样吗内存、CPU、磁盘速度、GPU、网络位置完全可能异构。如果调度器不考虑这些差异只按有几台机器来分就很容易出现一个任务被分到一台配置不足的机器上跑了一半失败重来。我的建议是先把资源抽象成槽位而不是机器。一个槽位代表一个能独立执行任务的最小资源单元比如2核CPU 4G内存 20G磁盘 0.25张GPU。一台物理机可以映射成多个槽位。调度器做的事情就是不停地在空闲槽位和等待任务之间做匹配。还有一类资源特别容易被忽视许可证或并发额度。有些商业工具、云服务按并发数收费调度器如果不感知这个额度可能在那一刻同时把多个任务调度到同一个商业服务上导致超过额度直接排队。比如压测工具只允许10个并发调度器却同时启动了15个压测任务剩下5个在工具层面排队白占资源。所以做算法之前先画一张资源清单有哪些资源维度每个维度的容量是多少能不能弹性伸缩是不是异构的。这份清单会直接影响后面所有算法策略的效果。3.2 抢占机制算法只是上半场排队控制才是下半场分配算法决定了任务该不该跑但真正执行时还有一个问题高优先级任务进来时正在运行的低优先级任务怎么办这就涉及到抢占机制。抢占有三种策略。第一种是无条件抢占高优先级任务到达直接杀掉低优先级任务释放资源。这个策略最快但伤害最大被杀的任务可能白跑了半天下次又要重新编译、重新测试。第二种是不抢占只排前正在跑的低优先级任务继续跑完但后续排在它后面的高优先级任务会被插队到它前面。它保护了已经投入的成本但响应速度慢。第三种是检查点抢占把任务运行状态定期打点保存抢占后可以从最近一个检查点恢复。它减少了损失但要求任务本身支持断点续跑不是所有任务都做得到。我实际用下来对构建任务和测试任务采用不抢占只排前是性价比最高的。因为构建和测试大多数是一次性任务杀掉重跑几乎没有任何恢复收益。而发布调度不同发布任务一般是串行执行的批次你要做的是在批次之间切换而不是中途杀掉所以发布场景通常用批次窗口来控制而非抢占。记住一个原则抢占逻辑和分配算法要分层设计。分配算法只负责排序抢占逻辑负责打断两者混在一起排查问题的时候会非常痛苦。3.3 指标评价利用率高了为什么交付还是慢上了调度算法之后怎么判断它有没有效果很多人盯着资源利用率看觉得利用率越高越好。这是个危险的直觉。资源利用率高不等于任务交付快。举个例子有一个异构集群利用率常年90%以上但你去看任务平均排队时间居然高达一小时。原因是短任务频繁和长任务抢占每次切换都引入上下文开销真正用于干活的时间反而少了。利用率体现的是资源有多忙但忙可能忙在切换、等待和重试上而不是有效推进任务。我建议三个核心指标一起看平均排队时间任务从提交到开始执行的等待时长直接反映调度器的响应能力按时交付率在约定截止时间内完成的任务占比直接反映调度目标是否达成资源有效利用率真正在执行任务CPU/内存计算的时间占比剔除调度等待、抢占切换、任务失败重跑的时间。这三个指标的三角关系非常重要。追求平均排队时间最短可能会牺牲按时交付率追求有效利用率最高可能会让排队时间变长。我们的经验是先设定一个可接受的排队时间上限再在这个约束下尽量提高有效利用率。否则调度优化很容易走偏。3.4 离线回放实验如何验证新算法不敢直接上线调度算法直接改在线调度器风险很高。真实场景里任务随时来、随时取消还有各种依赖和重试一个新算法很可能在模拟环境跑得好好的一上线就出问题。所以我强烈建议做离线回放。方法并不复杂把过去一段时间内所有真实调度请求和任务执行结果记录下来保存成一份回放数据集。这份数据集包含每个任务的提交时间、资源需求、预估执行时间、实际执行时间、依赖关系、优先级等字段。然后你新写的调度算法可以在本地回放这份数据模拟调度过程算出如果当时用的是新算法结果会怎样。离线回放的价值有两层。第一层是预演能在不触碰生产环境的情况下发现明显问题第二层是对比拿历史算法和新算法跑同一份数据直接对比排队时间、按时交付率等指标量化收益。比较的时候要注意离线回放毕竟是单线程模拟没法完全模拟真实环境的抢占和并发所以它只能做个参考不能完全替代小流量灰度。我把这套回放脚本写成了一个固定工具流程收集日志清洗字段生成标准化任务事件流再用一套开源的离散事件模拟框架跑场景。整个过程几个小时就能完成性价比极高。4. 我踩过的坑与最终选型建议4.1 把权重当真理结果小任务永远排不上我最早给调度器配置权重时把每个服务的权重直接按业务重要性打分从1到10。看起来合理实际跑了两周就发现问题核心服务权重全是10一堆权重都是10的任务聚在一起调度器完全没法区分先后等于又回到了先到先得而那些权重只有1的实验性任务连续两周一次都没跑上。权重是一个相对值不是绝对值。如果大家都填10分权重就失去了区分度。后来我们改成只允许填3、2、1三档并且强制每档至少有两个服务才把这个局面扭转过来。另一个教训是权重只能表达任务的优先级不能表达这个任务必须在某个时间点完成。紧急程度这件事还是交给松弛度或截止时间机制更靠谱。4.2 追求全局最优反而让任务饿死有一段时间我们尝试在测试调度上用约束优化求解目标是让每天所有测试任务的总体完成时间最短。模型解出来的方案在纸面上非常好看总耗时缩短了20%。但上线之后被测试同学骂惨了有些边缘服务的测试任务连续三天都没排上因为在全局最优的模型里它们执行时间又长、权重又低排在后面最省总体时间但它们也有定期回归的需求。这就是全局最优和个体公平的冲突。后来我明白了一个道理调度系统不是一个单纯的数学优化问题它是一个带公平约束的多目标问题。如果你要优化全局指标一定要同时给最长等待时间或者最小服务保证率加上硬约束。这两个约束加进去之后解算器求出来的才是一个能用的方案。4.3 忽略任务依赖调度结果只是纸面最优还有一个坑更隐蔽任务之间的依赖关系。很多调度算法在算资源分配时默认每个任务是独立的但软件生产链路里的任务几乎都有依赖。集成测试要等依赖服务构建完成发布任务要等镜像推送完成一个任务跑完,下游任务才能启动。如果你在模型里忽略了这些依赖计算出来的最优分配就只是纸面最优。比如有两个任务都排在9点钟理论上应该同时开始跑但任务B依赖任务A的输出所以B最早也要等A完成才能开始。调度器的真实能力应该是不仅要分配资源还要在任务依赖图上去做关键路径分析找到哪些任务是整个链路的瓶颈优先保证这些任务。这个层次的问题后面我会单独写但在设计调度算法时一定记得把依赖关系作为第一类约束放进去。4.4 按团队规模分档的选型思路最后给一个我沉淀下来的选型思路按团队规模分三档。小团队20人以下任务量每天几百次别研究太多算法。直接用先到先得加一个简单的优先级字段就够了。资源也不是很紧张复杂的调度带来的收益远小于维护成本。中等团队50到200人每天几千次任务建议用优先级加加权轮询关键场景套一层最小松弛度。比如把线上构建和发布任务划为高优先级档位对其他任务做加权轮询保证公平在发布窗口内对发布相关任务启用松弛度排序保证它们不会在窗口内被挤掉。这套组合实现不复杂能应对绝大多数场景。大团队500人以上每天上万次任务建议在优先级基础上引入预测式扩容和更精细的资源画像。因为到了这个规模任务到达的时间分布非常不均匀洪峰很常见。与其改造算法本身不如先解决资源弹性的问题预测洪峰提前扩容让调度器在充足资源下做决策压力会小很多。真的到了这一步再考虑约束优化也不迟。调度这块说到底没有什么银弹关键是理解自己的业务约束选一个能维持住底线指标的简单方案再逐步迭代。别忘了把抢占、依赖、公平这些工程细节补齐否则再好的算法也撑不起复杂的软件生产环境。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →