2. Cromwell架构解析
这一章我们聊聊 Cromwell 的骨架。说白了,就是把引擎拆开看看它怎么干活。
很多刚接触 Cromwell 的朋友,一上来就被它的分布式设计搞晕。其实核心就四个组件:主节点(Server)、任务调度器(Job Scheduler)、数据库层(Database Layer)、文件系统接口(File System Interface)。这四个东西搭配好了,整个流程才能顺滑。
2.1 主-从架构(其实准确说是“主-工作节点”)
Cromwell 采用经典的 Master-Worker 模型。但你别被“主从”两个字吓到,它跟数据库的主从复制完全是两码事。
- Server(主节点):负责接收 WDL 请求、解析工作流、管理状态机、调度任务。它不做计算。
- Worker(工作节点):实际跑任务的地方。可以是本地进程,也可以是云上的虚拟机、K8s Pod、或者 HPC 集群的作业。
我习惯把 Server 比作“项目经理”,Worker 是“搬砖工人”。项目经理只负责派活和看进度,绝对不去搬砖。一个项目经理可以管几十上百个工人。
有意思的是,Cromwell 天生支持热插拔 Worker。曾经我在生产环境遇到一批节点因为网络波动失联了,任务调度器自动把它们标记为“失败”并重试到其他节点上。整个过程我没改一行配置,只是重新拉起那几个节点,工作流就继续跑了。
下图是 Cromwell 整体架构简图,帮你一眼看清各个组件怎么连的。
2.2 任务调度器(Job Scheduler)
调度器是 Cromwell 最聪明的部分。它不只是一个简单的队列,而是一个带状态机的异步路由引擎。
每个 WDL 中的 call 会被拆成多个任务(Task),每个任务被包装成 Job 丢给调度器。调度器干三件事:
- 排队:维护一个内存+数据库双写的任务队列。
- 后端路由:根据配置决定扔给哪个 Backend(比如 Local、Google Cloud Life Sciences、AWS Batch)。
- 重试与超时:如果任务失败就自动重试,支持指数退避。
我记得有一次排查线上性能问题,发现调度器队列堆积了上千个任务。结果是因为数据库连接池配小了,调度器每次写状态都卡住。把 db.max-pool-size 从 5 提到 20,瞬间好了。
system.job-rate-control.jobs 参数。我一般设成 50 起步,再按 Worker 数量往上调。别搞太大,不然数据库容易背不住。
调度器的工作流程大致这样:
- Server 收到新 WDL 请求,解析成 Workflow 实例。
- 调度器从 Workflow 的 DAG 中取出可执行的任务。
- 把任务包装成 Job,写入数据库(状态:Queued)。
- 调度器轮询数据库中状态为 Queued 的 Job,根据 Backend 配置找到对应 Worker。
- 将 Job 提交给 Worker(可能通过 HTTP、gRPC 或者云 API)。
- Worker 执行完毕后回调 Server 更新状态(Succeeded / Failed)。
- 调度器检查 DAG,触发下游任务。
这里关键一点:调度器只负责分发,不负责执行。Worker 返回结果后,调度器的工作就完了。
2.3 数据库层(Database Layer)
Cromwell 的数据库层不是可选组件,是必须有的。没有数据库,工作流状态根本没法持久化。
它默认内置一个 H2 内存数据库,只适合测试。生产环境我建议用 MySQL 5.7+ 或 PostgreSQL 11+。
| 数据表 | 存储内容 | 规模参考 |
|---|---|---|
WORKFLOW_STORE |
工作流实例、状态、输入输出 | 每条工作流 1 行 |
JOB_STORE |
每个任务(call)的执行记录 | 同工作流中的 call 数 * 重试次数 |
JOB_FAILURES |
失败详情、日志路径 | 每次失败 1 行 |
QUEUE |
等待调度的任务 | 瞬时排队数 |
useSSL=true&requireSSL=true。另外,千万记得做定时备份,我用的是 mysqldump --single-transaction 每周一次。
数据库层的核心价值在于容错。如果主节点意外重启,它会从数据库读回未完成的工作流状态,继续往下走。不会丢任务——除非你连数据库也一起崩了。
2.4 文件系统接口(File System Interface)
Cromwell 把文件系统抽象成一层接口,叫 FileSystemProvider。每个 Backend 都需要注册一个 Provider,告诉 Cromwell 怎么读写文件。
支持的常见类型:
- Local:本地磁盘,小规模测试用。
- GCS:Google Cloud Storage,云上最常用。
- S3:AWS S3,与其他云兼容。
- HDFS:Hadoop 分布式文件系统。
- ABFS:Azure Blob File System。
为什么要抽象一层?因为 WDL 里的文件路径都是统一的 URI 格式。比如:
gs://my-bucket/inputs/ref.fa
s3://my-bucket/inputs/ref.fa
file:///data/ref.fa
你写 WDL 时只用关心 URI 是什么,不用管底层是 GCS 还是 S3。Cromwell 会自动根据前缀调用对应的 Provider 去下载、上传、拷贝。
我曾经踩过一个坑:任务输出文件特别大(几十 GB),结果 Worker 直接往本地磁盘写,再上传到 GCS,导致磁盘空间打满。后来我改配置让 Cromwell 用流式上传,边写边传,磁盘占用从 50GB 降到 200MB。
backend {
providers {
GCS {
config {
root = "gs://my-cromwell-bucket/workflows"
file-system {
gcs {
project = "my-gcp-project"
auth = "application-default"
}
}
}
}
}
}
文件系统接口另一大作用是缓存。如果两个任务的输入文件完全一样,Cromwell 会复用之前的结果文件,跳过计算。这个叫 Call Caching,其底层就是靠文件系统接口比较文件内容哈希实现的。
好了,四个组件都讲清楚了。你想想看,一个 WDL 跑在 Cromwell 上的完整路径就是:用户提交工作流 → Server 解析并存库 → 调度器分发任务 → Worker 通过文件系统接口读写数据 → 结果回写数据库。每个组件各司其职,缺一不可。