2. Cromwell架构概述

说实话,我第一次接触 Cromwell 的时候,也被它这套架构整得有点懵。后来用久了才发现,它的设计其实很清晰。我个人习惯把 Cromwell 理解成一个「翻译官」——你把 WDL 写好的工作流扔给它,它负责跟底层的计算资源打交道,不管你是用本地 Docker、HPC 集群还是云上的批量计算服务。

这一章我们就把 Cromwell 的架构拆开,看看它到底是怎么工作的。

2.1 整体架构:Cromwell 把活分成几块?

Cromwell 的核心模块其实就三个:入口服务、作业调度器、后端执行器。给你举个具体例子——你提交一个 WDL 脚本,入口服务先收下,然后调度器把 WDL 里的每个 task 拆成独立作业,再交给后端去跑。后端可能是你的本地机器,也可能是 Google Cloud Life Sciences API。

一句话记住:Cromwell = 入口 + 调度 + 后端。后端决定了你的工作流跑在哪儿。

模块 职责 我遇到过的问题
入口服务 接收 WDL / CWL 脚本、管理 HTTP API、处理用户请求 接口超时多半是并行数太高
作业调度器 拆解工作流、管理依赖关系、生成任务队列 依赖写错了会死循环,别问我怎么知道的
后端执行器 真正执行任务,写中间文件,返回结果 本地后端最稳,云端后端配置容易漏

2.2 两种运行模式:Server vs. Single

Cromwell 有两种启动方式,这一点你最好一开始就搞清楚。为什么?因为在项目里选错了模式,后面切换环境会很痛苦。

  • Server 模式:Cromwell 以 HTTP 服务形式常驻后台,你可以持续提交作业,查询状态,管理多个工作流。适合生产环境。
  • Single 模式(也叫 Run 模式):一次性提交一个工作流,跑完就退出。适合调试或单次小任务。

我在项目中最常用的就是 Server 模式。举个例子,我们团队的 WDL 脚本调试阶段先用 Single 模式跑个小样本,确认无误后切到 Server 模式批量提交全基因组数据。

# Server 模式启动(后台常驻)
java -jar cromwell-85.jar server --config cromwell.conf

# Single 模式启动(跑完即止)
java -jar cromwell-85.jar run my_workflow.wdl --inputs my_inputs.json

2.3 核心模型:Workflow → Task → Command → Call

Cromwell 的执行单元是层级嵌套的。我从上到下给你捋一遍:

  1. Workflow:你写的 WDL 文件描述了一个完整的工作流流程。
  2. Task:工作流里的每个独立步骤,比如比对、排序、变异检测。
  3. Command:Task 内部的 shell 命令——这就是真正要运行的代码。
  4. Call:一次实际的执行实例。同一个 Task 可以产生多个 Call,比如对多个样本分别跑比对。

嗯,这里要注意:同一个 Task 被并行调用多个 Call 时,每个 Call 都是独立的运行环境。数据隔离靠输入文件路径来保证。

# 一个简单的工作流结构示意
workflow my_pipeline {
  call align
  call sort { input: bam = align.out_bam }
}

task align {
  command {
    bwa mem ref.fa read1.fq read2.fq > aligned.sam
  }
  output {
    File out_bam = "aligned.sam"
  }
}

我个人经验:写 Task 时尽量把 command 短一点。太长不但难调试,而且一旦报错,定位具体行数很麻烦。我习惯把一个复杂命令拆成 3 ~ 5 行的脚本。

2.4 工作流执行的生命周期

Cromwell 执行一个工作流时会经历以下几个阶段:

  • 提交:入口服务收到 WDL + 输入 JSON。
  • 解析与校验:检查 WDL 语法、变量类型、输入文件是否存在。
  • 调度:调度器根据依赖关系,决定哪些 Call 可以并行,哪些必须等前序。
  • 执行:将每个 Call 的 command 发给后端执行器。
  • 监控:不断轮询任务状态,直到完成或失败。
  • 输出收集:把任务输出文件路径记录到 metadata 中。

为什么会分成这么细?因为每个阶段都可能出错。我以前在解析阶段就踩过坑——输入 JSON 里写错一个变量名,Cromwell 会直接报错退出,而不会继续执行后续步骤。

2.5 架构核心逻辑:一张图看懂

讲到这里,我觉得有必要给你看一张整体的架构逻辑图。你可以把它当成学习 Cromwell 的思维导图。

Cromwell 架构核心逻辑 入口服务 作业调度器 本地后端 集群后端 云端后端 执行器:任务拆解与监控 数据管理:Call Cache / 输入输出代理

图中你可以看到一条清晰的主线:用户提交请求 → 入口服务 → 调度器 → 后端 → 执行器 → 数据管理。每个箭头都代表数据或指令的流动。说实话,我当初学 Cromwell 时自己画了这张图,此后遇到问题都按这个思路排查,很少再卡住。

2.6 本地 vs 云端:架构上的区别

搞清楚通用架构后,我们聊聊本地和云端在架构层面的核心差异。

维度 本地后端 云端后端
调度器 直接在本机调用系统命令 调用云服务 API(如 Batch / Life Sciences)
数据存取 读取本地文件系统 读写对象存储(GCS / S3)
资源限制 受本机 CPU / 内存 / 磁盘限制 按需申请,吞吐量大
Call Cache 基于本地文件哈希 基于云端对象哈希
成本 设备折旧 + 电费 按用量付费

避坑提醒:我曾经在本地调试时一切正常,切到云端后端后所有任务都失败。查了半天才发现是云端的执行环境没有安装 bwa。所以,云端后端的镜像和软件依赖一定要提前确认。

2.7 数据库的角色:Cromwell 的「记忆」

Cromwell 默认使用 H2 数据库(嵌入式),也支持 MySQL / PostgreSQL。它的作用是什么呢?说白了就是 Cromwell 的「记事本」——记录每个工作流、每个 Call 的状态、输入输出、日志路径。

为什么要提这个?因为如果在生产环境里长时间运行,H2 会撑不住。我有个朋友的项目跑了一个月,H2 文件涨到 50G,启动一次要 10 分钟。后面换成 MySQL,整个世界清静了。

# MySQL 配置示例
database {
  driver = "com.mysql.cj.jdbc.Driver"
  url = "jdbc:mysql://localhost:3306/cromwell?useSSL=false"
  user = "cromwell_user"
  password = "your_password"
  db {
    connectionTimeout = 5000
    numRetries = 3
  }
}

2.8 本章小结

Cromwell 的架构不复杂,但理解它是用好 Cromwell 的前提。记住三点:

  • 入口 + 调度 + 后端,三层各司其职。
  • 本地和云端的主要差异在后端和数据存取方式。
  • 数据库选型影响长期稳定性,别忽视。