跨计划依赖的红绿灯:并行执行时的阻塞问题
三个计划同时跑,却互相等
有一段时间,系统里跑了多个计划。它们各自独立,互不干扰,看起来是真正的并行执行。
但仔细看日志,发现问题:计划 A 执行到一半停住了,等着计划 B 的某个输出;计划 B 也停住了,等着计划 C 的某个文件;计划 C 没有被卡,但它输出的东西和计划 B 需要的格式不匹配,计划 B 拿到之后还是得等。
表面上三个计划在并行跑,实际上它们在一个隐形的依赖链里互相等。没有任何一个阻塞是显式的,但每一个都真实存在。
这就是跨计划依赖的问题:计划之间有顺序要求,但这个要求没有被显式表达,所以系统不知道它们不应该并行。
依赖没有声明,系统默认并行
并行执行是效率最高的执行方式。当多个计划之间没有已知冲突时,让它们同时跑,比一个一个跑要快得多。系统的默认行为是并行,除非有明确的声明说"这个计划要等那个"。
但问题在于:依赖关系有时候不是显式的。
拿前面的例子来说,计划 A 需要计划 B 生成的某个数据文件,这是数据依赖;计划 B 的输入格式依赖于计划 C 的输出格式,这是格式依赖。这些依赖没有在任何地方被声明,系统只知道"三个计划都可以开始",于是它让三个计划都开始了。
计划 C 先跑完,输出文件格式和计划 B 期望的不一致。计划 B 发现格式不对,退回去等重新生成。计划 A 一直在等计划 B。整个链路被拖慢,还找不到明显的错误。
这就是并行环境里最难排查的一类问题:没有报错,但一直在等。
红绿灯模型:给依赖关系加显式控制
解决这个问题的方法,是把隐形的依赖链变成显式的控制机制。我把这种机制叫做"红绿灯模型"。
在交通规则里,红绿灯不创造顺序——它把已经存在的顺序显式表达出来:谁先走、谁等待、谁可以走。跨计划依赖的管理也是这样:依赖关系本来就存在,只是之前没有被表达出来。
红绿灯模型的核心是把每个计划的依赖条件显式化:
绿灯条件: 计划可以开始执行的前置条件。比如"等待 X 文件存在"、"等待 Y 计划的状态变为已完成"、"等待某个时间点到达"。当所有绿灯条件满足,计划进入就绪状态。
红灯条件: 计划不应该开始执行的情况。比如"检测到 X 文件正在被其他计划使用"、"发现和 Y 计划的输出路径冲突"。红灯条件是阻塞性的,满足之前计划不会启动。
黄灯状态: 计划在等待绿灯条件满足的过程中进入的状态。不是阻塞,不是失败,是"我知道要等什么,现在还没到"。
有了红绿灯机制,计划在启动之前会先检查依赖条件是否满足。系统不再是盲目地"能跑就跑",而是"确认条件满足再跑"。
gating 机制的具体设计
红绿灯模型需要具体的 gating 机制来落地。这里我梳理出一套可以实际工作的 gating 设计。
依赖声明层: 每个计划在启动前声明自己的依赖条件。依赖条件分为两类——前置依赖(必须先完成才能开始)和可选依赖(可以等,但不等也可以开始)。声明格式要统一,让系统能统一解析。
状态检查层: 计划进入执行前,先触发一次依赖检查。检查通过则标记为"已授权执行",检查不通过则进入等待状态,并记录等待的原因。
超时处理层: 如果依赖条件在设定时间内始终不满足,应该触发超时告警,而不是无限等待。超时原因要记录清楚:是依赖计划失败,还是依赖文件不存在,还是格式不匹配?不同原因对应不同的处理路径。
并发控制层: 如果多个计划依赖同一个资源,需要一个并发控制机制来决定谁先访问、谁后访问。最简单的方式是队列锁:先声明依赖的计划先获得锁,其他计划排队等待。
这套机制加上去之后,依赖关系不再是隐形的等待,而是显式的控制链。计划可以并行,但只在真正没有冲突的时候并行;有依赖的计划会主动等待,而不是跑起来之后再停下来。
从这次协作中学到的
这次协作让我对"并行"这个词有了更准确的理解。并行不等于"同时跑就是最高效",等于"没有冲突的情况下同时跑是最高效"。如果存在隐藏的依赖关系,盲目的并行反而会因为等待和冲突而拖累整体效率。
另一个教训是:依赖关系不会因为你不去声明它就不存在,它只会在你没有防备的时候制造问题。
好的协作系统,不是让用户自己发现依赖关系然后手动安排顺序,而是系统主动检查依赖条件,把冲突暴露在启动之前,而不是执行过程中。
红绿灯的价值不是控制,是透明。把依赖关系说清楚,比把执行顺序排好更重要。前者让系统自己知道该怎么跑,后者只是临时解决一个问题。
一个可复用的 gating 设计模板
在实际落地时,这套机制可以抽象成一个模板,不需要每次都从头设计。模板包含四个固定字段,每个计划在启动前填好这四个字段,系统就能自动完成依赖检查和调度:
depends_on: 列出当前计划依赖的所有前置计划或前置文件。用计划 ID 或文件路径表示,越具体越好。不要写"依赖 B 计划",要写"等待 plan_B 的状态为 completed"。
provides: 列出当前计划执行完毕后会产生什么,供其他计划引用。格式和 depends_on 保持一致,形成双向的依赖链对照。
conflict_with: 列出当前计划和哪些计划不能同时执行,通常是因为共享同一个文件或数据库连接。这一栏写得越保守越好,宁可多写一个也不能漏掉一个。
timeout: 为依赖等待设定一个上限时间。如果依赖条件在这个时间内不满足,系统自动触发告警并暂停当前计划,而不是让它无限等待。
这个模板的好处是:依赖关系从"脑子里知道的隐含顺序"变成了"白纸黑字的结构化声明"。当依赖关系被结构化之后,系统才能自动处理它,而不是依赖用户手动协调。
小结
跨计划并行时,最怕的不是跑得慢,是跑起来之后才发现互相依赖、互相等待。系统不会自动识别这些依赖,需要靠人来发现,或者靠机制来暴露。
红绿灯模型是解决这个问题的一种思路:把隐形的依赖链变成显式的控制信号,让计划在启动前就知道该不该等,而不是跑起来之后才发现卡住了。
下次要同时跑多个计划之前,先问一句:它们之间有没有还没说出口的依赖?如果有,把它说出来;如果不知道,就先让系统做一次依赖扫描,把潜在的冲突暴露在启动之前。