架构解析:主节点、数据库、存储后端、计算后端、任务队列的协作关系

说实话,刚接触Cromwell那会儿,我也被它这堆组件绕晕过。主节点、数据库、各种后端……它们到底怎么配合的?

我习惯把Cromwell理解为一套「工作流调度系统」。你想想看,它本质上在做三件事:接任务、派任务、收结果。但这三个动作背后,藏着五个核心组件的精密协作。

先看一张整体架构图,有个直观印象。

Cromwell 核心架构协作关系 用户 / REST API / CLI Cromwell 主节点(Server) 工作流解析 · 状态机 · 调度决策 任务队列 Redis / 内存队列 元数据数据库 MySQL / MariaDB / PostgreSQL 计算后端 AWS Batch / HPC / K8s 存储后端 GCS / S3 / HDFS / 本地 关键协作路径 ① 主节点解析 WDL → ② 拆分为任务写入队列 → ③ 计算后端轮询拉取 → ④ 计算节点从存储后端读取输入 / 写出中间结果 → ⑤ 状态回写数据库

主节点:大脑与调度中心

主节点就是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
  }
}
我个人的习惯:生产环境一定用MySQL或PostgreSQL。HSQLDB只适合本地测试,别往生产上放。另外,记得给数据库加索引,不然万级任务量下查询会越来越慢。

存储后端:数据的仓库

存储后端管的是实际数据。包括输入文件、中间结果、最终输出、运行日志。主节点不直接读写这些文件,它只记录文件路径。真正干活的是各个计算节点。

跨云部署时,存储后端的选择就变得很关键。我见过一个团队把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
    }
  }
}
注意:用了外部队列,意味着主节点和计算后端之间的耦合度降低了。但是队列本身也要考虑高可用。Redis哨兵模式或集群模式是生产标配,单点Redis千万别用。

协作流程:一次完整的工作流运行

知道了每个组件的角色,咱们来看看它们怎么配合的。一个WDL工作流跑完,内部的协作顺序大概是:

  1. 用户提交工作流 —— 通过REST API或CLI把WDL传给主节点
  2. 主节点解析WDL —— 构建DAG,确定任务依赖关系
  3. 主节点写数据库 —— 记录工作流ID、初始状态、输入文件路径
  4. 主节点投递任务到队列 —— 把"可执行的任务"放入任务队列
  5. 计算后端从队列拉取任务 —— 轮询或事件驱动,拿到任务描述
  6. 计算后端从存储后端读取数据 —— 根据任务描述中的路径,拉取输入文件
  7. 计算后端执行任务 —— 跑Docker容器、执行命令
  8. 计算后端写回结果 —— 输出文件写回存储后端
  9. 计算后端通知主节点 —— 更新任务状态(Done/Failed)回数据库
  10. 主节点检查DAG —— 看有没有下一个任务可以投递,重复步骤4~9
  11. 全部任务完成后 —— 主节点汇总最终状态,用户通过API查询结果

这个过程里,数据库和队列是解耦的。主节点只管写状态和投递任务,具体怎么执行、在哪里执行,它完全不关心。这正是Cromwell能够跨云部署的根本原因——你把计算后端指向AWS,存储后端指向S3,数据库用RDS,一个跨云工作流就搭起来了。

一句话总结:主节点是大脑,数据库是记忆,队列是缓冲区,计算后端是手脚,存储后端是仓库。五者配合,各司其职。

一份值得收藏的备忘卡片

我个人习惯把核心协作关系做成一张对照表,贴在项目Wiki首页。分享给你:

组件 核心职责 故障影响 高可用建议
主节点 解析、调度、状态管理 停止调度,已有任务继续执行 主备部署 + 共享数据库
数据库 持久化元数据 工作流无法提交,运行中任务状态丢失 主从复制 / 云托管数据库
存储后端 存放输入、输出、日志 任务无法读取/写入数据 多区域冗余 / 版本控制
计算后端 执行实际计算任务 任务卡住,无法响应 Auto Scaling + 备用计算集群
任务队列 解耦调度与执行 任务积压,新任务无法投递 Redis集群 / 消息队列持久化

这张表我每次搭新环境都会拿出来对照一遍。你部署之前,最好也过一遍——每个组件的高可用方案都落实了吗?如果其中一个挂了,影响范围有多大?