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 是单主节点设计。如果主节点挂了,整个服务就不可用了。生产环境建议配合健康检查和自动重启,或者用前端负载均衡做优雅降级。

下图是 Cromwell 整体架构简图,帮你一眼看清各个组件怎么连的。

Cromwell 架构核心组件 Cromwell Server (主节点·状态机+调度器) 数据库层 (MySQL / PostgreSQL / H2) 任务调度器 (Job Scheduler + Backend Router) 文件系统接口 (GCS / S3 / HDFS / Local) Worker 集群 (Spark / K8s / HPC / Batch) 每个 Worker 运行一个 task 实例 说明:实线表示控制流,虚线表示数据流/文件访问。Worker 不直接连接数据库,一切通过 Server 中转。

2.2 任务调度器(Job Scheduler)

调度器是 Cromwell 最聪明的部分。它不只是一个简单的队列,而是一个带状态机的异步路由引擎。

每个 WDL 中的 call 会被拆成多个任务(Task),每个任务被包装成 Job 丢给调度器。调度器干三件事:

  • 排队:维护一个内存+数据库双写的任务队列。
  • 后端路由:根据配置决定扔给哪个 Backend(比如 Local、Google Cloud Life Sciences、AWS Batch)。
  • 重试与超时:如果任务失败就自动重试,支持指数退避。

我记得有一次排查线上性能问题,发现调度器队列堆积了上千个任务。结果是因为数据库连接池配小了,调度器每次写状态都卡住。把 db.max-pool-size 从 5 提到 20,瞬间好了。

一个小技巧:调整调度器并发度,可以在 Cromwell 配置里加 system.job-rate-control.jobs 参数。我一般设成 50 起步,再按 Worker 数量往上调。别搞太大,不然数据库容易背不住。

调度器的工作流程大致这样:

  1. Server 收到新 WDL 请求,解析成 Workflow 实例。
  2. 调度器从 Workflow 的 DAG 中取出可执行的任务。
  3. 把任务包装成 Job,写入数据库(状态:Queued)。
  4. 调度器轮询数据库中状态为 Queued 的 Job,根据 Backend 配置找到对应 Worker。
  5. 将 Job 提交给 Worker(可能通过 HTTP、gRPC 或者云 API)。
  6. Worker 执行完毕后回调 Server 更新状态(Succeeded / Failed)。
  7. 调度器检查 DAG,触发下游任务。

这里关键一点:调度器只负责分发,不负责执行。Worker 返回结果后,调度器的工作就完了。

2.3 数据库层(Database Layer)

Cromwell 的数据库层不是可选组件,是必须有的。没有数据库,工作流状态根本没法持久化。

它默认内置一个 H2 内存数据库,只适合测试。生产环境我建议用 MySQL 5.7+ 或 PostgreSQL 11+。

数据表 存储内容 规模参考
WORKFLOW_STORE 工作流实例、状态、输入输出 每条工作流 1 行
JOB_STORE 每个任务(call)的执行记录 同工作流中的 call 数 * 重试次数
JOB_FAILURES 失败详情、日志路径 每次失败 1 行
QUEUE 等待调度的任务 瞬时排队数
我曾经遇到的坑:数据库默认连接串没有开启 SSL。在公网上暴露 Cromwell 端口,结果数据库被扫描到了。赶紧改配置加 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。

文件系统配置片段示例(application.conf):
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 通过文件系统接口读写数据 → 结果回写数据库。每个组件各司其职,缺一不可。