理解并行度:并行度的真正含义
并行度这个概念,说白了就是“同一时间能跑多少个任务”。很多初学者以为调大并行度就一定能加速,嘿嘿,我在项目里可没少吃这个亏。有一次我把并行度从 4 直接拉到 32,结果集群直接 OOM,作业全挂了。
我们先拆开看:并行度(Parallelism) 不等于 并发(Concurrency)。并发是逻辑上的“同时在做”,并行是物理上的“真的同时跑”。在 Cromwell 里,并行度就是 Workflow 引擎允许同时运行的任务数量上限。
一句话总结:并行度控制的是“跑任务的车道数”,不是“车的总量”。车道再多,车堵在入口也没用。
Cromwell 的并行模型
Cromwell 用的是 Master-Agent 架构。Master 负责任务调度,Agent(实际是 Backend 对应的执行器)负责跑任务。我个人习惯把 Cromwell 的并行度拆成两个层面:
- 全局并行度:配置文件里的
concurrent-job-limit,限制整个 Cromwell 实例能同时调度的 job 数。 - Workflow 内部并行度:每个 workflow 默认允许并行跑的 call 数量,由
max-concurrent-workflows等参数控制。
你想想看,如果全局设了 100,但每个 workflow 内部只允许 1 个任务跑,那实际上也跑不上去。反过来,全局只给 10,workflow 内部设 50 也没用,因为总闸细了。
我曾经遇到过一个问题:全局并发设了 200,后端是本地模式(Local backend),结果磁盘 IO 直接打满,swap 狂涨。因为本地模式没有分布式资源管理,所有任务挤在同一台机器上,并行度再高也是自欺欺人。
任务并发与数据并发
这里要分清楚两个概念,很多文档混着说,我觉得有必要掰扯清楚:
| 维度 | 任务并发(Task-level) | 数据并发(Data-level) |
|---|---|---|
| 是什么 | 不同 task(call)同时执行 | 同一个 task 的多个输入分片同时处理 |
| WDL 体现 | scatter 或正常 call 之间的依赖并行 | scatter 内部的元素并行 |
| 控制参数 | concurrent-job-limit |
default-runtime-attributes 里的 cpu/memory 以及 scatter 的 batchSize |
| 典型瓶颈 | 调度器压力、队列长度 | 输入输出带宽、shuffle 容量 |
举个例子,你在 WDL 里写了一个 scatter (item in items),里面调用了 process task。如果 items 有 1000 个元素,Cromwell 默认会尝试同时跑 1000 个 process 任务——这就是数据并发。但实际上,受限于 backend 的资源池,可能只有 50 个 slot 可用,那你就得手动限制 scatter 的并行度。
我的小技巧:在 WDL 的 runtime 里加一个 maxRetries,并且给 scatter 设置 batchSize = 50。这样既能控制数据并发粒度,又能在某个分片失败时自动重试,不会把整个 workflow 拖垮。
Cromwell 并行调度流程
我把 Cromwell 的并行调度画成了一张图,方便你理解数据流和控制流的关系:
如何设置合理的并行度
说实话,没有一套参数能通吃所有场景。我一般遵循下面几个原则:
- 先看 backend 资源池大小。SLURM 集群有多少个 cpu 核心?Google Cloud 的配额是多少?并行度不要超过可用资源的 80%,留点余量给调度和 IO。
- 再按 workflow 特点调整。如果 workflow 里有很多短的、IO 密集型的 task(比如
gzip),可以适当提高并行度;如果是长计算型 task(比如bwa mem),并行度反而要降一点,避免争抢内存。 - 用 Cromwell 的元数据监控。我习惯在 Cromwell 的 config 里开启数据库监控,跑几轮后看看
cromwell_workflow_success和cromwell_job_execution_time指标,哪里排队久就调哪里。
一个典型的调优配置片段(cromwell.conf):
system {
max-concurrent-workflows = 10
max-workflow-launch-count = 100
}
backend {
default = "Local"
providers {
Local {
actor-factory = "cromwell.backend.impl.sfs.LocalBackendLifecycleActorFactory"
config {
concurrent-job-limit = 20
# 每个 job 默认分配 1 cpu, 2GB mem
runtime-attributes {
cpu: "1"
memory: "2 GB"
}
}
}
}
}
我曾经在一个 48 核的服务器上跑 NGS 流程,开始设了 concurrent-job-limit = 48,结果磁盘 IO 飙升到 99%,每个任务都在读写临时文件。后来降到 24,配合 local-allocation-strategy = round-robin,整体吞吐反而提高了 15%。
避坑指南
- 别把并行度和线程数搞混。Cromwell 的并行度是 workflow 级别的任务调度,不是 JVM 线程。JVM 线程由
-Dcromwell.io-threads控制,一般保持默认就行,别乱改。 - Scatter 的大爆炸。如果 scatter 的输入列表很大(比如几万个样本),又不设
batchSize,Cromwell 会瞬间创建海量 job 元数据,数据库可能扛不住。建议给每个 scatter 加个batchSize限制。 - 小心全局并发数过低导致死锁。比如一个 workflow 有 3 个阶段,第二阶段需要 5 个并行 job 全部完成后才能进入第三阶段,但全局并发数只有 4——那第二阶段永远跑不完。这种情况我遇到过,最后把全局并发数调大一个数量级才解决。
归纳总结
并行度不是越大越好。它是一根水管,后端资源是水池,水池不够大,水管再粗也没用。
- 任务并发:多个独立的 call 同时跑,依赖 DAG 解析结果。
- 数据并发:同一个 scatter 里的分片同时跑,靠 batchSize 控制粒度。
- 控制点:全局
concurrent-job-limit+ 单个 workflow 的max-concurrent-workflows+ scatter 的batchSize。 - 最佳实践:先粗调全局并发到资源池的 70%,再根据实际队列深度微调。
特别提醒:如果你用的是 PAPI 或 Google Cloud 等按量付费的 backend,并行度过高可能导致并发费用爆炸。我有一次没注意,一个 workflow 跑了 200 个并行 job,云账单直接多了 300 美元——就因为你少设了一个参数。