2. 任务编排模式:串行编排、并行编排、DAG(有向无环图)编排的核心思想
在分布式开发环境中,异步任务很少孤立执行。任务之间通常存在依赖关系:某个任务必须等待前置任务完成才能启动,或者多个任务可以同时运行。任务编排模式定义了这些依赖关系的组织方式。理解串行、并行和 DAG 编排是构建可靠异步流水线的基础。
2.1 串行编排(Sequential Orchestration)
串行编排是最直观的模式:任务按照严格的先后顺序依次执行。前一个任务完成后,后一个任务才能开始。这种模式适用于有明确数据依赖或状态依赖的场景。
- 特点:执行顺序固定,逻辑简单,易于理解和调试。
- 缺点:整体耗时等于所有任务耗时之和,无法利用并发提升吞吐。
- 典型场景:用户注册流程(先验证手机号 → 再创建账户 → 最后发送欢迎邮件)。
串行编排的适用判断:如果任务 B 必须使用任务 A 的输出结果作为输入,且任务 C 又依赖任务 B 的结果,则串行是唯一正确的选择。强行并行只会引入竞态条件和数据不一致。
2.2 并行编排(Parallel Orchestration)
并行编排允许没有依赖关系的任务同时执行。在异步任务框架中,并行通常通过线程池、协程或分布式节点实现。并行编排能显著缩短整体执行时间,但需要处理资源竞争和结果聚合。
- 特点:吞吐量高,整体耗时由最长的分支任务决定。
- 缺点:需要管理并发控制、资源限制和异常隔离。
- 典型场景:批量数据清洗(同时处理多个分片)、微服务并行调用(同时查询用户信息、订单信息、库存信息)。
| 对比维度 | 串行编排 | 并行编排 |
|---|---|---|
| 执行顺序 | 严格有序 | 无序并发 |
| 总耗时 | 各任务耗时之和 | 最长分支耗时 |
| 资源消耗 | 低(单线程) | 高(多线程/多节点) |
| 错误传播 | 中断后续任务 | 仅影响当前分支 |
| 结果聚合 | 天然有序 | 需要显式合并 |
2.3 DAG(有向无环图)编排
DAG 编排是串行和并行的泛化形式。它将任务建模为图中的节点,将依赖关系建模为有向边,且图中不允许存在环(即不能出现循环依赖)。DAG 编排能够表达任意复杂的任务依赖拓扑,是工业级异步任务框架(如 Airflow、Temporal、DolphinScheduler)的核心模型。
DAG 编排的核心思想包括:
- 节点(Node):代表一个可执行的任务单元,可以是原子操作或子流程。
- 边(Edge):代表依赖方向,从前置任务指向后置任务。
- 入度(In-degree):指向某节点的边的数量,表示该任务有多少个前置依赖。
- 拓扑排序(Topological Sort):将 DAG 节点线性化,保证所有依赖关系被满足。
DAG 与环的约束:环的存在会导致死锁——任务 A 等待 B,B 等待 C,C 又等待 A,没有任何任务可以开始执行。所有 DAG 编排引擎在提交时都会进行环检测,拒绝包含环的图。
下图展示了一个典型的 DAG 编排结构,包含并行分支和汇合点:
2.4 三种编排模式的对比与选择
在实际开发中,一个复杂的异步流程往往混合使用多种编排模式。下表从多个维度对比三种核心模式:
| 维度 | 串行编排 | 并行编排 | DAG 编排 |
|---|---|---|---|
| 依赖表达能力 | 仅支持线性链 | 仅支持无依赖 | 任意有向无环依赖 |
| 执行效率 | 低(顺序执行) | 高(完全并发) | 最优(按拓扑并行) |
| 实现复杂度 | 低 | 中 | 高 |
| 错误定位难度 | 低(线性回溯) | 中(需区分分支) | 中高(需结合拓扑) |
| 链路追踪支持 | 天然支持 | 需 Span 关联 | 需 DAG 上下文传播 |
| 典型框架 | CompletableFuture.thenApply | CompletableFuture.allOf | Airflow / Temporal / DolphinScheduler |
常见误区:不要将所有任务都设计成并行。过度并行会导致资源耗尽、上下文切换开销剧增,反而降低整体吞吐。正确的做法是:先识别依赖关系,再对无依赖的任务进行并行,最后用 DAG 统一管理拓扑。
2.5 代码示例:三种编排的 Java 实现对比
以下代码使用 Java CompletableFuture 展示三种编排模式的实现差异:
// 串行编排:任务依次执行
CompletableFuture.supplyAsync(() -> "步骤1")
.thenApply(result -> result + " → 步骤2")
.thenApply(result -> result + " → 步骤3")
.thenAccept(System.out::println);
// 并行编排:任务同时执行,等待所有完成
CompletableFuture<String> taskB = CompletableFuture.supplyAsync(() -> "任务B");
CompletableFuture<String> taskC = CompletableFuture.supplyAsync(() -> "任务C");
CompletableFuture<String> taskD = CompletableFuture.supplyAsync(() -> "任务D");
CompletableFuture.allOf(taskB, taskC, taskD)
.thenRun(() -> System.out.println("所有并行任务完成"));
// DAG 编排:先执行根任务,再并行执行三个子任务,最后汇合
CompletableFuture<String> taskA = CompletableFuture.supplyAsync(() -> "根任务");
CompletableFuture<String> taskB = taskA.thenApplyAsync(a -> a + " → B");
CompletableFuture<String> taskC = taskA.thenApplyAsync(a -> a + " → C");
CompletableFuture<String> taskD = taskA.thenApplyAsync(a -> a + " → D");
CompletableFuture<Void> taskE = CompletableFuture.allOf(taskB, taskC, taskD)
.thenRun(() -> System.out.println("汇合点 E:所有分支完成"));
CompletableFuture<String> taskF = taskE.thenApplyAsync(v -> "最终任务 F 执行");
taskF.thenAccept(System.out::println);
从代码可以看出:串行编排通过链式调用实现;并行编排通过 allOf 聚合;DAG 编排则通过组合 thenApplyAsync 和 allOf 来表达任意拓扑。在实际生产系统中,DAG 编排通常由专门的框架管理,避免手动维护复杂的 Future 链。