前几篇讲的都是清算算什么,这篇讲清算怎么跑。一个券商的日终批量,小几十个任务,大的能做到上百个:文件解析、一级清算接收、二级清算、费用计算、利息计提、总分核对、对账、交收文件生成……它们之间有严格的先后约束——费用依赖成交,总分核对依赖费用,交收依赖核对。把这些约束画出来是一张有向无环图(DAG),清算调度器的全部工作就是让这张图在满足依赖的前提下尽量并行。
依赖从哪来:业务语义,不是运维习惯
任务依赖的唯一合法来源是数据依赖:任务 B 读了任务 A 写的表,A→B 就是一条真实依赖。判断标准是看数据流向,而不是看"历来先跑 A 再跑 B"。这点听起来像废话,但生产事故里相当大一类是"时间依赖"伪装成"数据依赖"造成的:配置里写"20:30 跑 A,20:45 跑 B",中间留了 15 分钟富余,某天 A 的数据量翻倍跑了 25 分钟,B 就踩着 A 写了一半的表开工了。
干净的做法是把依赖显式建模成 DAG,调度器只做一件事:当一个任务的全部前驱完成时触发它。时点在图里消失,只保留给"必须在某时刻之后才能开始"的少数外部约束(比如中登文件 19:00 才下发,"等待文件到达"本身就是一个前置任务)。这样 A 跑多久都不影响正确性,影响的只是整体完成时间。
关键路径:批次的物理下限
DAG 定了,批次的最早完成时间由关键路径决定——所有从起点到终点的路径中,任务耗时之和最长的那条。记任务 $i$ 的耗时为 $w_i$,路径 $P$ 的长为 $\sum_{i \in P} w_i$,则:
$$ T_{\min} = \max_{P:\, \text{起点}\to\text{终点}} \;\sum_{i \in P} w_i $$
加机器、加线程只能压缩非关键路径上的等待,压不动关键路径本身。所以清算性能优化的第一步永远不是并发调优,而是把关键路径画出来,看哪个环节是大头。经验上,文件解析、大表关联的明细清算、和数据库交互密集的核对任务最常占据关键路径;优化手段也各有侧重——解析可以并行切文件,明细清算可以按账户分片(后面账务性能篇展开),核对可以裁剪比对范围。
顺便说一句,关键路径随数据量漂移。月初、季末、新股申购日的数据分布和平时不同, critical path 可能换人,这也是为什么调度系统要记录每个任务的实际耗时分布,而不是只盯一个静态图。
栅栏同步:批次的"整点报时"
DAG 调度解决了任务间的依赖,但日终批量还有一个整体节奏:某些资源要求一批任务全部完成才能动,比如总分核对要求所有记账任务收工,对账闸门要求文件解析和内部清算都收工。这种"全到齐才放行"的同步点就是栅栏(barrier)。
工程上有两种栅栏实现,语义差别要分清:
- 计数栅栏:下游任务声明"我等 N 个前驱",调度器每完成一个前驱减一,归零放行。语义精确,但 N 是配置出来的——漏配一个前驱不会报错,只会带着洞跑,所以 N 必须能从 DAG 自动推导,不能手填;
- 阶段闸门(phase gate):把批次切成阶段(清算阶段、核对阶段、交收阶段),阶段之间用持久化的状态标记过渡,未通过闸门的阶段不允许任何下游任务启动。这种粗粒度栅栏的好处是提供了一个全局一致的检查点:核对阶段不过,交收阶段一个任务都起不来。
大多数成熟系统是两者混用:DAG 调度驱动任务级并行,阶段闸门承载"整批对平才放行"的业务约束。测试策略也因此分两层——单任务用黄金文件对比(清算回归篇展开),调度层用"断一个任务看下游是否全部被拦"的故障注入,专门验证栅栏的封闭性。