1. 先别急着优化,搞清楚“最优”到底指什么
我做了几年调度相关的系统,发现一个特别有意思的现象:很多人一听到“任务最优调度”,第一反应就是“把任务排个序,让系统跑快点”。听起来没毛病,但真落地做的时候,十个项目里有八个卡在同一个地方——大家嘴里的“最优”根本不是同一个东西。
以前接手过一套订单履约系统,业务方的需求描述很简单:“把任务调度做得更优一些”。等到细化需求的时候才发现,他们内部有三个团队各自有各自的“最优”:运营团队希望订单平均交付时间最短,仓储团队希望设备空转率最低,财务团队希望单均成本压到最低。这三个目标在同一个调度方案里天然互相打架,怎么调都有人不满意。最后项目组花了两周时间做目标拆解和权重协商,才把问题定义清楚。所以做任务调度的第一步,永远不是写算法,而是把“最优”这个词翻译成可量化、可比较、可收敛的明确指标。
那什么是任务调度?往简单了说,就是在一组任务、一组资源和一组约束条件之间,找到一个时间上的安排方案,让某个或某几个指标达到最优。这里的任务可能是数据库里的一批SQL请求,可能是工厂流水线上的加工工件,可能是云平台上的一组计算容器,也可能是项目团队里的一堆开发任务。资源可能是CPU、内存、机器设备、带宽,也可能是人。
我习惯把它拆成三个核心维度来思考,缺一个都会出问题:
- 任务维度:任务的到达时间、处理时长、优先级、相互依赖关系。
- 资源维度:有几台机器或几个执行单元,每个资源能同时处理几个任务,资源之间是否有差异。
- 时间与目标维度:总工期要压到多短,延迟要控制在多少以内,资源空闲率要降到什么水平。
后面所有调度算法的设计、参数调整、效果评估,本质上都是在回答这三个维度的问题。你在做方案之前,先把这三个维度的情况填清楚,后面能少走一半弯路。
再说说“最优调度”这个概念为什么在工程里特别容易被误解。教科书里讲的“最优”,通常是指在一个严格定义的数学模型下求得精确最优解。但工程里的调度问题,规模一大就是NP难问题,等精确解算出来黄花菜都凉了。所以实际工作中说的“最优”,绝大多数情况下是“在可接受时间内找到足够好的可行解”。这一点不提前对齐,项目后期很容易陷入无休止的算法优化中无法自拔。
2. 调度问题怎么建模:先有个结构,再谈优化
2.1 问题要素的抽象方法
不管任务是什么形态,调度问题都可以抽象成几个要素:任务集合、资源集合、约束集合、目标函数。这个抽象看起来简单,但具体到实操是有一套固定流程的。
我习惯先建一张表把任务描述出来。举个例子,假设某个异构计算场景下有5个任务,每个任务有固定的处理时长和资源需求,表格大概长这样:
| 任务编号 | 预估时长(分钟) | 所需CPU | 依赖前置任务 | 交付截止时间 |
|---|---|---|---|---|
| T1 | 20 | 2 | 无 | 60 |
| T2 | 35 | 4 | T1 | 120 |
| T3 | 25 | 2 | T1 | 90 |
| T4 | 40 | 3 | T2, T3 | 150 |
| T5 | 15 | 1 | T4 | 180 |
这张表本身没什么高深的技术含量,但它是后续一切讨论的基础。你会发现,一旦把任务信息量化到表里,很多之前靠“感觉”讨论的事情就变得非常具体了,比如T4依赖T2和T3都完成,而T2预计耗时35分钟,T3是25分钟,那么T4最早能在T1完成后约35分钟时开工,前提是资源充足。
依赖关系这块,我建议用有向无环图(DAG)来表达,节点是任务,边是先后依赖。虽然很多调度框架(比如Airflow、xxl-job、Temporal)都自带DAG建模能力,但你自己心里得有这个概念,尤其在评估调度方案可行性的时候,DAG拓扑结构决定了理论最短工期。关键路径——也就是DAG里最长的那条路径——就是系统能达到的最短完成时间下限,这是后续判断调度方案好坏的一把尺子。
2.2 约束条件的分类与处理优先级
模型中第二重要的是约束条件的分类。我通常把约束分成硬约束和软约束两类。硬约束是必须满足的,违反就是方案不可行,比如“同一台机器同一时刻只能处理一个任务”“任务B必须在任务A完成后才能启动”,这类约束在算法设计里通常作为过滤条件。软约束则是尽量满足但允许偏离的,比如“希望每个任务都在截止时间前完成”“希望各机器负载尽量均衡”,这类约束通常折算进目标函数,用惩罚项来体现。
在工程实现里把约束分好类,能大幅度降低后续算法调试的痛苦。有一年我在做一个云平台任务调度模块的时候,因为没把“某类型的任务只能跑在特定区域的机器上”这个约束明确标成硬约束,导致启发式算法频繁产出不可行方案,一个惩罚项反复调参就是不稳定。后来把这个约束彻底提到前置过滤条件里,算法只负责在满足这个条件的机器子集里做优化,问题一下就清晰了。这个经验我后来一直沿用:能用前置过滤解决的约束,绝不放进目标函数里和权重纠缠。
约束条件的处理优先级也是个值得单独说的事。我一般按“可行性 → 稳定性 → 最优性”这个顺序来:先保证方案能跑通不违约,再保证调度结果对参数扰动不敏感(比如任务时长估算有误差时不会大面积崩盘),最后才谈目标函数值的优化。很多人上来就猛调权重,结果方案在仿真数据上很好看,一上真实环境因为波动就频繁重排,工期反而更慢。
2.3 目标函数的常见定义方式
目标函数是调度问题里最核心也最容易产生分歧的部分。常见的几类定义方式:
- 最小化总完工时间:也叫Cmax或makespan,就是最后一个任务完成的时间,适合流水线型和批处理场景。
- 最小化平均等待时间或平均流转时间:适合用户交互性强的场景,比如订单处理,用户不关心最后一批什么时候结束,只关心自己的订单多久能被处理完。
- 最小化延期惩罚:给每个任务设置交付截止时间,延期越久惩罚越大,适合有SLA承诺的业务系统。
- 最大化资源利用率:让机器和设备尽可能满负荷运转,适合设备折旧成本高的重资产场景。
在实际系统里,往往不是单一目标,而是多个目标的加权和。设定权重这件事,必须由业务方来拍板,技术人员千万不要自作主张。比如“履约时效”和“运营成本”这两个目标,如果时效权重设为0.7,成本权重设为0.3,调度方案会优先保障快;反过来设,则会优先省成本。技术上没有绝对的谁对谁错,只有业务目标不同。
这里我有一个特别想强调的经验:不要在项目一开始就追求目标函数的完整和精确。先用简单的、甚至有点粗糙的目标函数把整个调度链路跑通,再根据实际业务反馈逐步细化。我见过太多团队把大量时间花在构建一个包含十二项权重的目标函数上,最后发现调度引擎连最基本的数据读写都没理顺。工程上永远是先有可用系统,再谈优化。
3. 调度算法的选型:从规则到启发式,再到元启发式
3.1 经典分派规则与适用场景
调度算法这块,是很多人一听到“最优调度”就兴奋的地方。但在实际项目里,算法选型并不是越复杂越好,而是越匹配越好。
经典的分派规则是最容易落地的一类。常见的包括:
- 先到先服务(FCFS):任务按到达顺序执行,实现简单,但短任务可能被长任务堵塞。
- 最短处理时间优先(SJF/SRTF):让处理时长最短的任务先跑,能有效降低平均等待时间,但可能造成长任务饿死。
- 最早截止时间优先(EDD):按截止时间的先后排优先级,适合有明确交付期限的场景。
- 最高优先级优先(HPF):按业务给定的优先级排序,紧急任务能得到优先执行,但低优先级任务可能长期得不到资源。
- 轮转调度(RR):每个任务按时间片轮流执行,适合交互式系统,保证公平性,但上下文切换开销比较大。
这类规则的好处是计算开销极低、行为完全可预期、实现只需几行排序逻辑。在任务规模不大、约束不复杂、波动性不明显的场景下,它们往往在工程上就是最优解——因为省下的计算资源和维护成本,比多跑快的那几分钟更值钱。
我记得早年给一个中小规模的异步任务系统做调度优化,任务量每天大概几千条,资源就是几台固定的worker机器。一开始想得很复杂,打算上遗传算法找最优解。后来冷静下来分析了一下,发现问题很大程度上是“资源分配不均”,有些worker队列积压了上千个任务,而另一些worker在空转。最终用了一个非常简单的规则:新建任务按当前每个worker的积压数量和平均处理速度做加权分发,也就是加权轮询的思想。上线后吞吐量提升了将近一倍,而代码量只增加了不到两百行。这给我留下的印象很深:工程里一半以上的调度问题,其实靠规则就够用了。
3.2 启发式与元启发式:什么时候值得上
当任务规模变大、约束变复杂,比如有成百上千个任务、多台异构机器、复杂的任务依赖关系、动态到达和取消,经典规则就扛不住了。这时候就需要上启发式算法或者元启发式算法,常见的有:
- 贪心构造法:每步从当前可执行任务集里挑一个最“划算”的任务分配出去,比如优先分配关键路径上延迟影响最大的任务。它快,但很容易陷入局部最优。
- 局部搜索:先有一个可行解(初始调度方案),然后在相邻解空间里试探性移动,保留更优的方案,比如交换两个任务的位置。缺点是依赖初始解质量。
- 遗传算法:把一组调度方案编码成“染色体”,通过选择、交叉、变异迭代进化,适合复杂约束和大型搜索空间。
- 粒子群优化(PSO):把调度方案看作一个个“粒子”,在解空间里按惯性、个体最优和群体最优方向飞行更新,适合连续或混合变量问题。
- 模拟退火:以一定概率接受更差的过渡解,从而跳出局部最优,适合对解质量要求高、计算时间允许的场景。
这里我要泼一盆冷水:元启发式算法听起来高大上,但实现成本和对参数调优的敏感性都非常高。遗传算法要选种群大小、交叉率、变异率、收敛条件;模拟退火要定初始温度、降温速率、迭代次数。每个参数都可能让结果大幅波动。如果在项目里没有专门的人力和充裕的预算时间去调这些参数,我强烈建议先从局部搜索或带随机化扰动策略的贪心算法开始。
我负责过一个相对复杂的资源调度模块,任务量到了一天几十万个请求,资源有十多种类型。第一版用的是遗传算法,基因编码做得相当讲究,适应度函数也结合了多个业务指标。结果一跑仿真,效果确实比规则算法好,但有几个问题:一是耗时太长,一个调度周期要跑十分钟,业务方接受不了;二是参数敏感度极高,稍微调整一下任务到达分布,输出质量就肉眼可见地下降。后来我换成“贪心构造 + 局部搜索”的组合策略,构造阶段用规则快速生成一个可行解,局部搜索阶段只做任务交换和插入两种邻域操作,整体效果接近遗传算法,但计算耗时压到了十秒以内,且稳定性好很多。这个案例让我彻底想明白了一件事:调度的价值不在于算法本身有多高级,而在于匹配业务的时延、规模和稳定性要求。
3.3 算法对比维度的客观参考
下面这张表是我在系统选型时常用来和团队对齐的参考,不代表绝对优劣,但能帮你在不同候选方案之间快速建立认知框架:
| 算法类型 | 计算复杂度 | 解质量 | 实现成本 | 参数敏感度 | 适用场景 |
|---|---|---|---|---|---|
| 分派规则 | 极低 | 一般 | 极低 | 低 | 任务量小、约束简单、实时性要求高 |
| 贪心构造 | 低 | 较好 | 低 | 中 | 有明确的贪心策略,如关键路径优先 |
| 局部搜索 | 中 | 较好 | 中 | 中 | 已有可行解,想快速迭代优化 |
| 遗传算法 | 高 | 好 | 高 | 高 | 离线调度、时间充裕、约束复杂 |
| 模拟退火 | 较高 | 好 | 偏高 | 高 | 对解质量要求高、可接受较长计算时间 |
| 强化学习 | 极高 | 潜力好 | 极高 | 极高 | 在线决策、状态空间大、有长期反馈数据 |
我说一点题外话,强化学习这两年在调度领域非常火,也有些不错的落地案例。但它的门槛不止在训练环节,更难的是环境模拟、状态设计和奖励塑形。如果没有充足的仿真环境和历史数据,我建议还是把它当研究方向看待,不要轻易作为第一套生产方案。
4. 实操落地:一套完整调度系统的搭建过程
4.1 一个具体案例:异构资源下的任务编排调度
概念讲了一堆,接下来我用一个实际做过的模拟项目来说明完整落地流程。背景是某公司内部有一个数据处理平台,每天要处理大量不同类型的计算任务,包括数据清洗、特征计算、模型推理、报表生成。计算资源分成CPU密集型机器和GPU密集型机器两种。任务的约束包括:依赖关系、所需资源类型、预估处理时长、优先级。
需求整理下来大概是:
- 任务总数日均约5000个。
- 计算节点共20个,其中CPU节点16个、GPU节点4个。
- 每个节点单次只能处理一个任务,但任务占用时长差异很大,有的几十秒,有的几十分钟。
- 部分任务之间有数据依赖,必须等前置任务跑完才能开始。
- 业务上要求高优先级任务的平均排队时间不超过5分钟,低优先级任务不能无限期等待。
这个案例的核心难点在于:两个资源池互相独立又共享同一批任务流,需要同时满足依赖约束、资源约束和响应时间约束。下面我拆开讲每一步怎么做。
4.2 第一步:数据建模与参数定义
在写任何调度代码之前,我先定义了三个核心对象:
第一个是任务对象。字段包括:任务编号、任务类型、预估时长、所需资源类型、依赖的任务编号列表、业务优先级(1到5,数字越大越紧急)、到达时间。这些字段统一存在配置表里,方便后续算法读取。
第二个是资源对象。字段包括:节点编号、节点类型(CPU或GPU)、状态(空闲中/运行中/维护中)、当前正在执行的任务编号、下次空闲时间。这个下次空闲时间非常关键,调度算法判断能不能插入任务时都要用到它。
第三个是调度方案对象。它不是一个简单的序列,而是一张时间表:每个任务被分配到了哪个节点、预计开始时间、预计结束时间、当前状态(等待中/运行中/已完成/已取消)。
这类建模工作虽然枯燥,但做扎实了,后面写算法和查问题时都会非常顺。我自己的习惯是,先用Python写一个简单的数据类来模拟,结构清晰了再迁移到正式工程语言。下面是一个简化版本的任务数据类示例:
from dataclasses import dataclass, field from typing import List @dataclass class Task: task_id: str duration: int # 预估处理时长,单位分钟 resource_type: str # 可选值:'cpu' 或 'gpu' dependencies: List[str] = field(default_factory=list) priority: int = 3 # 业务优先级,1-5,默认3 arrival_time: int = 0 # 到达时间,相对时刻这个定义看着简单,但它决定了一件事:你的算法和后续所有逻辑都依赖这个结构。如果一开始没把依赖关系和资源类型建模进去,后面想加就得改一大批代码。字段宁多勿少,但每个字段都得有明确用途,避免堆砌无用数据。
4.3 第二步:用拓扑排序加关键路径确定可行性下界
任务之间存在依赖关系,不能想跑哪个就跑哪个。工程上第一步要做的是拓扑排序,也就是把DAG里所有任务排成一个线性序列,保证每个任务都在它的所有前置任务之后。如果DAG里有环,说明依赖关系定义出了逻辑错误,这时要立刻报错。
有了拓扑序之后,我还会顺便算一下关键路径长度。关键路径是整个DAG里从任一入口任务到任一出口任务的最长加权路径长度,这里的权重就是任务的处理时长。它代表在任何资源无限、完全理想的情况下,这批任务理论上的最早完成时间。听起来有点抽象,但你可以把它理解为“这条流水线最快也要这么久才能冒出第一个成品”。这个值作为下界,用来判断后续调度方案的优化空间:如果方案的总完工时间接近关键路径长度,说明资源已经用得很充分了,再优化也榨不出多少油水。
以本文第三节的表为例,任务依赖结构是T1→T2→T4,T1→T3→T4,T4→T5。关键路径是第一或第二条支路加上T4和T5的时长,取较长的一条。具体数值不重要,重要的是这个计算做一次之后,心里就有了底:后续所有调度方案的完工时间都不可能低于这个值。
相关代码逻辑大概是:
from collections import defaultdict, deque def topological_sort(tasks): indegree = {t: 0 for t in tasks} adj = defaultdict(list) for t in tasks: for dep in t.dependencies: adj[dep].append(t.task_id) indegree[t.task_id] += 1 queue = deque([t for t in tasks if indegree[t.task_id] == 0]) result = [] while queue: cur = queue.popleft() result.append(cur.task_id) for nxt in adj[cur.task_id]: indegree[nxt] -= 1 if indegree[nxt] == 0: queue.append(next(t for t in tasks if t.task_id == nxt)) return result用拓扑排序输出的序列,就是调度算法的基础顺序之一。但它本身不负责分配资源到底让谁先跑,而是告诉你哪些任务在逻辑上有资格跑。
4.4 第三步:贪心构造一个初始可行调度
调度引擎的核心是一个“事件驱动”的循环。我会维护两个集合:一个是就绪任务集合,指所有前置依赖已经完成且当前可以分配执行的任务;另一个是空闲资源集合,指当前没有在执行任务的节点。每一次循环里做三件事:
第一,扫描全部未完成的任务,把满足依赖条件的任务加入就绪队列。第二,从空闲资源里取一个节点,按候选规则从就绪队列里选一个最适合的任务分配给它。第三,更新该任务状态和节点状态,推进虚拟时钟到下一个节点空闲或新任务到达的时刻,重复第一件事。
候选规则如果只用简单的“优先级排序”,在高复杂场景下走得不够远。我常用的是加权评分公式,把优先级、预计等待时间、资源匹配度、后续依赖紧迫度都放进去:
score = 优先级权重 * priority_score + 等待权重 * waiting_score + 资源贴近权重 * resource_match_score + 后继关键度权重 * successor_score比如一个低优先级任务已经在队列里等了很久,它的等待分数会随时间上升,最终超过另一个高优先级但刚到不久的任务,这样就避免了低优先级任务饿死。工程上这类“多因子加权”规则比单一排序规则稳定得多,也远好实现。
这里有一个经验值:在项目第一版,权重参数先不要做得太精细,直接把四个分数都设为1:1:0.5:1,跑一轮看效果,再根据短板调整。前期用粗糙的参数找系统的整体行为问题,后期才用精细参数做局部优化,这样调试效率会高很多。
4.5 第四步:局部搜索迭代改进调度方案
贪心构造出来的方案基本可用,但肯定不是最“优”的。要升级方案质量,我一般会在它基础上加一个局部搜索过程。邻域操作最常见的有两种:交换和插入。交换是拿两个任务的位置互换;插入是把一个任务从原位置挪到另一个位置。
具体到调度场景里,还得分情况。同类型节点之间的任务移动是可以随意交换的,但跨资源类型就要小心了。比如一个需要GPU的任务,你把它从GPU节点挪到CPU节点,那就不允许了,因为它根本跑不动。所以每次生成邻域方案后,第一步校验永远是合法性校验:是否满足依赖关系、资源类型是否匹配、是否与已分配的区间冲突。
局部搜索的收束条件,我是这么设计的:连续N轮迭代都没能产生比当前方案更优的解,就停止迭代。N取值我常用任务总数的10%,比如5000个任务就500轮。这个值不用过于纠结,它只影响计算耗时和收敛质量,天平的度量方法只要在可控范围内就可以接受。
实际执行中还有一个特别重要的细节:局部搜索每一步都要重新计算整个时间表的完工时间和其它指标。如果每步都全量重算,任务数一多就会很慢。我的优化做法是只重算受影响的那一段:当你在任务X和任务Y之间做交换时,只需要重算从X到Y以及它们后续链条上的任务的起止时间,不需要从头扫一遍。这个改动,经验上能省下70%以上的计算耗时。
4.6 第五步:动态事件下的重调度机制
真实系统的调度不是一个静态优化问题,任务会随时到达,节点可能故障,执行时长也可能和预估差很多。所以调度引擎必须支持重调度。
我的做法是把调度分为两个层次:一个是周期性的全局调度,比如每隔5分钟调度一次,把当前未完成的任务整体重排一遍;另一个是事件驱动的局部调度,比如某个节点完成任务、新任务到达、某节点故障,都触发一次局部调整。全局调度保证整体趋势是最优的,局部调度保证系统对变化响应迅速。
这里有个容易踩坑的地方:重调度不要把所有运行中的任务都打断重排。任务一旦开始运行,如果频繁打断,会产生大量上下文切换开销,甚至导致计算状态丢失。所以调度算法对“正在运行的任务”要设置一个锁定标记,只有极少数情况才允许抢占。我在系统里默认是禁止抢占,除非高优先级任务等待时间超过了一个阈值且运行任务已执行时长占比超过40%。这个阈值设计需要结合业务容忍度,没有通用标准,但思路是“可抢占但不能随意抢占”。
至于执行时长和预估不一致的问题,我的建议是保留“实际时长记录”。调度引擎根据历史任务的预估偏差不断修正后续任务的预估时长,也就是引入滚动预测的思想。这个优化做得好,可以让调度稳定性大幅提升。具体做法很简单:按任务类型分桶,统计每个桶里过去100个任务的实际时长与预估时长的比值,然后修正未来任务的预估时长。比如某类任务过去平均实际执行时间是预估的1.3倍,那么新任务进来时直接把预估时长乘上1.3再用。
5. 常见问题排查与避坑实录
5.1 调度结果“平均指标很好”但局部体验极差
规划的项目上线后,经常会碰到一种情况:看大盘数据,调度得又快又好,平均排队时间、平均总耗时都在预期范围内。但业务方还是会投诉,仔细一查,发现有相当数量的任务等了特别久。这就是典型的“平均指标掩盖长尾问题”。
我后来在目标函数里显式加了一个惩罚项:单个任务等待时间超过某个阈值时,会触发一个很重的额外惩罚。这样调度算法就不只是优化平均指标了,还会尽量让每个任务都落在可接受的等待范围内。这个经验非常值得分享:设计目标函数时,不仅要看均值,还要看分位数,P95和P99才是用户真实体验的衡量标准。运营人员不会因为平均等待时间短就开心,他们会因为某一个订单等了半天而彻底失去耐心。
5.2 资源状态长时间不一致导致调度死锁
有几次线上事故,表现为一个节点的资源明明显示空闲,但没有新任务被分配过去。排查下来,问题出在资源状态更新和调度决策之间存在竞态条件:节点任务结束事件已经发出,但状态还没更新到调度引擎,调度引擎认为它仍在忙碌,于是跳过了它。
解决方案是在状态更新和调度触发之间引入“最终一致”的机制。具体做法是我把资源状态做成一个单独的队列,所有状态变更都入队,由调度引擎按顺序消费处理。这样虽然会有少量延迟,但彻底避免了因为异步竞争导致的状态不一致。同时,增加一个定时的健康检查机制,定期扫描所有节点状态,发现异常就强制纠正。这套机制上线后,这个类型的bug再没出现过。
5.3 算法测试环境表现优秀但生产环境明显退化
这个问题我遇到不止一次。仿真环境下调出来的参数,一上生产环境效果差了一截。原因基本是仿真的任务到达分布、处理时长分布和真实数据差异太大。仿真里任务到达是均匀的、时长是稳定的,真实场景却是突发的、抖动的。
后来我建立了基于历史数据回放(trace replay)的测试机制:把过去一段时间的真实任务日志原样灌入调度引擎,对比新旧调度算法在相同历史数据下的表现。通过这种方式,算法的生产表现和仿真表现就能对齐了。
我建议做调度系统的朋友,在项目初期就把这套回放机制建好,不要等上线发现问题了再补。它不仅是评估算法优劣的标准工具,也是你做参数回归测试的依托。没有这个回放机制,你可能永远都在盲目调参。
5.4 调度参数调优失控,改一个小参数全线崩盘
调度系统的参数往往非常多:优先级权重、等待惩罚、抢占阈值、周期间隔、时间修正系数等等。有时候改一个看似无关紧要的参数,整个系统的行为就变了。这种参数之间的耦合效应非常难排查。
我个人的做法是:把所有参数统一集中到一个配置中心,每个配置项都写明用途、默认值、调整边界。同时建立一组基准测试集,每次改参数必须跑一遍基准集,对比输出指标曲线。如果某个参数在基准集上会导致某个指标恶化超过5%,就拒绝提交。这套流程保障了参数调整是渐进可回退的,而不是靠一次经验蒙对。
6. 一点个人体会
跑过这么多调度项目之后,我最大的感受是,调度系统的难点从来不在算法公式本身,而在于你对业务目标的理解深度、对工程约束的敬畏、以及对数据质量的掌控。一个再聪明的优化算法,如果喂进去的任务时长数据本身就是错的,结果只会是一本正经地误导业务。
还有一点就是,调度系统的迭代必须小步快跑。先上经典规则,把系统的可靠性和可观测性做扎实;再引入启发式优化,逐步逼近业务目标;最后如果有余力,再考虑元启发式或更复杂的方法。每一步都要留好回退开关,任何时刻都能一键回到上一版方案。这套方法论我用了好几年,稳扎稳打,几乎没出过大岔子。
最后分享一个小技巧:在调度系统的监控面板上,一定要同时展示“当前方案的关键路径长度”和“实际完工时间”这两条曲线。当两条线越接近,说明方案越接近资源利用的理论极限;当它们差距变大,说明调度机会被浪费了,要么是任务等待策略有问题,要么是资源碎片化严重。这是我每次排查调度问题的第一眼落点,也是我觉得性价比最高的一个监控指标。