2. 架构解析
说实话,刚接触Cromwell那会儿,我也被它这堆组件绕晕过。主节点、数据库、各种后端……它们到底怎么配合的?
我习惯把Cromwell理解为一套「工作流调度系统」。你想想看,它本质上在做三件事:接任务、派任务、收结果。但这三个动作背后,藏着五个核心组件的精密协作。
先看一张整体架构图,有个直观印象。
主节点:大脑与调度中心
主节点就是Cromwell服务器进程。它负责解析WDL工作流描述文件,构建一个有向无环图(DAG)。什么意思呢?就是把你的工作流拆成一个个任务,再搞清楚哪些任务可以并行,哪些必须等上一步完成。
我早期犯过一个错——以为主节点是单点的,挂了就全完了。其实Cromwell支持主节点的高可用部署,只是需要配合共享数据库和外部队列来搞。嗯,这个后面会单独讲。
主节点的核心职责其实就几个:
- 解析工作流:吃进WDL,吐出DAG
- 状态管理:跟踪每个任务处于Submitted、Running、Failed还是Done
- 调度决策:判断哪个任务可以投递到计算后端
- 错误重试:任务失败后决定是否重试
数据库:一切状态的记录者
数据库存的是元数据,不是数据本身。这概念我一开始也混淆过。Cromwell把工作流的每一步状态、每个任务的输入输出路径、运行日志位置都记在数据库里。
你想想看,如果一个工作流跑了两天,中间主节点重启了。没有数据库的话,所有进度全丢了,得从头再来。有了数据库,主节点重启后一查数据库,就知道:「哦,这个任务已经跑完了,那个任务还在队列里」,接着干就行。
我在项目中遇到过数据库连接池耗尽的问题。Cromwell默认的数据库连接池配置比较保守,高并发场景下容易卡死。后来调大连接数才解决。具体配置长这样:
database {
profile = "slick.jdbc.MySQLProfile$"
db {
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://your-host/cromwell_db?useSSL=false&rewriteBatchedStatements=true"
user = "cromwell"
password = "your_password"
connectionTimeout = 5000
numThreads = 20
maxConnections = 50
minConnections = 5
}
}
存储后端:数据的仓库
存储后端管的是实际数据。包括输入文件、中间结果、最终输出、运行日志。主节点不直接读写这些文件,它只记录文件路径。真正干活的是各个计算节点。
跨云部署时,存储后端的选择就变得很关键。我见过一个团队把GCS和AWS S3混着用,结果数据在不同云之间传来传去,光流量费就顶一台服务器了。后来规整成统一用S3,才把成本降下来。
典型配置:
engine {
filesystems {
s3 {
auth = "default-credential"
}
gcs {
auth = "application-default"
}
}
}
| 存储后端 | 适用场景 | 注意事项 |
|---|---|---|
| 本地文件系统 | 单机测试、小规模运行 | 不跨节点,慎用 |
| GCS | Google Cloud环境 | 注意Bucket权限和请求限频 |
| AWS S3 | AWS环境 | 小心跨区域传输成本 |
| HDFS | 已有Hadoop集群 | 配置稍复杂,性能不错 |
计算后端:真正的苦力
计算后端负责执行具体任务。Cromwell支持非常多的后端实现:AWS Batch、Google Cloud Life Sciences、HPC集群、Kubernetes、本地Docker……
我选计算后端有个原则:看你的基础设施在哪。如果你公司已经在用Kubernetes了,那就别折腾HPC。我在一个项目里硬着头皮把K8s环境的数据科学工作流迁到了AWS Batch,仅仅因为运维团队说「Batch我们熟」。结果迁移成本远超预期。所以我的建议是——选你最熟悉、最稳定的后端,性能优化可以慢慢做。
计算后端在配置里长这样:
backend {
default = "AWSBatch"
providers {
AWSBatch {
actor-factory = "cromwell.backend.impl.sfs.config.ConfigBackendLifecycleActorFactory"
config {
root = "s3://my-bucket/cromwell-executions"
docker {
image = "ubuntu:20.04"
}
backend.job-shell = "/bin/bash"
}
}
}
}
任务队列:解耦的关键
任务队列是最容易被忽视的组件。很多人觉得没有队列也能跑,确实可以——默认情况下Cromwell用内存队列。但一旦任务量上去,内存队列就成了灾难。
为什么?因为内存队列意味着主节点得直接往计算后端塞任务。如果计算后端响应慢了,主节点的线程就被占着,新任务进不来。整个系统的吞吐量瞬间被卡死。
我经历过一次线上事故:一个WDL工作流生成了3000多个并行任务,内存队列直接撑爆了主节点的内存,GC频繁到服务假死。后来切到Redis做外部队列,才算稳定下来。
配置外部队列(以Redis为例):
cromwell {
job-queue {
type = "redis"
redis {
host = "my-redis-cluster.amazonaws.com"
port = 6379
timeout = 5
}
}
}
协作流程:一次完整的工作流运行
知道了每个组件的角色,咱们来看看它们怎么配合的。一个WDL工作流跑完,内部的协作顺序大概是:
- 用户提交工作流 —— 通过REST API或CLI把WDL传给主节点
- 主节点解析WDL —— 构建DAG,确定任务依赖关系
- 主节点写数据库 —— 记录工作流ID、初始状态、输入文件路径
- 主节点投递任务到队列 —— 把"可执行的任务"放入任务队列
- 计算后端从队列拉取任务 —— 轮询或事件驱动,拿到任务描述
- 计算后端从存储后端读取数据 —— 根据任务描述中的路径,拉取输入文件
- 计算后端执行任务 —— 跑Docker容器、执行命令
- 计算后端写回结果 —— 输出文件写回存储后端
- 计算后端通知主节点 —— 更新任务状态(Done/Failed)回数据库
- 主节点检查DAG —— 看有没有下一个任务可以投递,重复步骤4~9
- 全部任务完成后 —— 主节点汇总最终状态,用户通过API查询结果
这个过程里,数据库和队列是解耦的。主节点只管写状态和投递任务,具体怎么执行、在哪里执行,它完全不关心。这正是Cromwell能够跨云部署的根本原因——你把计算后端指向AWS,存储后端指向S3,数据库用RDS,一个跨云工作流就搭起来了。
一句话总结:主节点是大脑,数据库是记忆,队列是缓冲区,计算后端是手脚,存储后端是仓库。五者配合,各司其职。
一份值得收藏的备忘卡片
我个人习惯把核心协作关系做成一张对照表,贴在项目Wiki首页。分享给你:
| 组件 | 核心职责 | 故障影响 | 高可用建议 |
|---|---|---|---|
| 主节点 | 解析、调度、状态管理 | 停止调度,已有任务继续执行 | 主备部署 + 共享数据库 |
| 数据库 | 持久化元数据 | 工作流无法提交,运行中任务状态丢失 | 主从复制 / 云托管数据库 |
| 存储后端 | 存放输入、输出、日志 | 任务无法读取/写入数据 | 多区域冗余 / 版本控制 |
| 计算后端 | 执行实际计算任务 | 任务卡住,无法响应 | Auto Scaling + 备用计算集群 |
| 任务队列 | 解耦调度与执行 | 任务积压,新任务无法投递 | Redis集群 / 消息队列持久化 |
这张表我每次搭新环境都会拿出来对照一遍。你部署之前,最好也过一遍——每个组件的高可用方案都落实了吗?如果其中一个挂了,影响范围有多大?