2. 性能监控与指标:CPU、内存、磁盘、网络、任务队列

Cromwell 跑得慢,你别急着怀疑代码。先问自己一个问题——你监控了吗?

我在团队里带过不少新人,上来就说「老师作业卡住了」。我让他们先看一眼监控面板,结果十有八九自己就找到原因了。说白了,性能优化不是猜谜游戏,而是数据驱动的事。

这一章我们就聊聊 Cromwell 跑 WDL 时,你该盯哪些指标。CPU、内存、磁盘、网络、任务队列——这五个维度,缺一不可。

2.1 CPU 指标

Cromwell 本身是 Scala 写的,跑在 JVM 上。它的调度线程、HTTP 服务器、数据库连接池都吃 CPU。最容易出问题的不是 Cromwell 进程本身,而是它提交的计算任务。

先看看怎么查 Cromwell 进程的 CPU 占用:

top -p $(pgrep -f cromwell) -bn1 | tail -1 | awk '{print $9}'

如果 %CPU 持续超过 80%,你就要留心了。我曾经遇到过一个案例:Cromwell 的数据库连接池配得太小,任务结果回写时大量线程阻塞,CPU 全花在上下文切换上了。你想想看,CPU 飙升不一定意味着工作量大,可能只是系统在「空转」。

我一般的排查顺序:
  1. 先看负载均值(load average)——如果远高于 CPU 核数,说明有进程在抢 CPU
  2. 再用 perf top 看热点函数——是不是 GC 线程在狂跑?
  3. 最后查 Cromwell 的日志,看有没有 java.lang.OutOfMemoryError 或频繁的 Full GC

对于 Cromwell 提交的计算任务(比如批量 BWA、GATK),你还需要监控每个任务的 CPU 使用率。如果任务跑的是单线程程序,你给了 16 核的请求资源,那 CPU 利用率上不去是很正常的。别盲目加核,先看任务本身是不是并行友好的。

# 查看某个任务的 CPU 亲和性和使用率
htop -p $(pgrep -f "java.*Cromwell.*task")
避坑指南: 我曾经在生产环境遇到过 Cromwell 实例的 CPU 被未知线程吃满,最后发现是另一个运维脚本在循环 ssh。嗯,监控不能只看 Cromwell 本身,整机的 CPU 分布也要看。

2.2 内存指标

Cromwell 的 JVM 堆内存、任务容器的内存——这两块是经典「重灾区」。

Cromwell 自身的内存配置:

# 启动 Cromwell 时常用
java -Xms4g -Xmx8g -jar cromwell.jar server

-Xms 是初始堆,-Xmx 是最大堆。我建议 -Xms 和 -Xmx 设成一样,避免 JVM 动态调整堆大小带来的性能抖动。具体设多大?看你的工作流复杂度:如果你同时在跑上千个 task,工作流图(workflow graph)在内存里会很大,我通常给 8-16G。

怎么监控 JVM 内存?

# 用 jstat 看 GC 情况
jstat -gc $(pgrep -f cromwell) 1000 5

关注 YGC(Young GC)和 FGC(Full GC)的频次和耗时。如果 Full GC 每秒都在跑,那内存肯定不够用了。我曾经遇到一个 32G 堆的 Cromwell,Full GC 每两秒一次,任务调度全部卡住。加了一行 GC 日志发现是老年代里存了大量已完成的 task 元数据——最后是把 workflow-options 里的 delete-workflow-files 打开,才释放了内存。

任务容器的内存监控更直接:用 docker stats 或 cgroup 看。如果任务配置了 4G 内存,但实际只用到 2G,那还好。如果任务的物理内存(RSS)接近甚至超过 cgroup 限制,就会被 OOM kill。

# 查看某个 docker 容器的内存
docker stats --no-stream $(docker ps -q --filter name=cromwell)
关键检查清单:
  • JVM 堆使用率:低于 70% 是安全的,超过 85% 就要警惕
  • GC 暂停时间:平均 < 200ms,STW 次数不要太多
  • 任务容器 RSS 与 cgroup 限制的比值:超过 90% 容易 OOM

2.3 磁盘指标

Cromwell 的磁盘开销主要来自三处:

  • 工作流输入/输出文件——尤其是大规模基因组数据的中间结果
  • call cache 数据库——如果开启了 call caching,磁盘上会存很多 hash 和元数据
  • 日志——Cromwell 自身的日志和 task 的 stdout/stderr

我一般先看磁盘空间:

df -h /cromwell_root /var/log/cromwell

如果分区使用率超过 85%,Cromwell 写文件可能会失败。我曾遇到一个经典场景:某个任务写了几百 GB 的临时 BAM 文件,磁盘直接打满,然后 Cromwell 报 No space left on device,所有后续任务全部挂起。后来我给 /tmp 挂了一个独立的 SSD 分区,并限制了每个 task 的临时文件大小。

磁盘 I/O 指标同样重要——用 iostat -x 1 看:

iostat -x 1 5

如果 %util 持续接近 100%,说明磁盘已经成了瓶颈。特别是你用了 HDD 做 Cromwell 的工作目录,那读大文件时 IOPS 根本跟不上。我建议把 Cromwell 的工作目录、数据库、临时文件都放在 SSD 上——这可能是性价比最高的优化手段。

避坑指南: 我曾经没注意 docker-compose 里 Cromwell 容器的数据卷挂载,结果把所有内容写到了 overlay2 的容器层,重启后全丢了。嗯,记得挂载持久化卷。

2.4 网络指标

Cromwell 和数据库通信、和 backend(比如 AWS Batch、Google Cloud Life Sciences)的 API 调用、文件上下载——这些都需要网络。

网络延迟太高,Cromwell 的 HTTP 连接池会被占满。我一般用 netstat 或 ss 看连接数:

ss -tan | grep :8000 | wc -l

Cromwell 的 HTTP 端口(默认 8000)上的连接数如果持续很高,说明客户端轮询或者 callback 流量太大。我见过一个案例:几百个任务同时回调 Cromwell,导致 TCP 连接队列溢出,新连接被拒绝。解决方法是在 application.conf 里调大 akka.http.server.request-timeout 和 max-connections。

对于文件传输(比如 S3、GCS 的上传下载),我习惯看带宽使用:

# 用 nload 或 iftop 监控实时流量
nload eth0

如果上行/下行带宽打满,而 Cromwell 自己的 CPU 很低,那大概率是网络 I/O 拖慢了整个 pipeline。我一般会建议:

  • 使用并行上传(gsutil -m cp)
  • 把 Cromwell 的 backend 放在同一个云区域/可用区
  • 避免跨区域、跨 AZ 频繁传输大文件

2.5 任务队列指标

Cromwell 内部有一个任务调度队列。这个指标最容易被忽略,但往往是瓶颈的核心。

你可以通过 Cromwell 的 API 获取队列状态:

curl -s http://localhost:8000/api/workflows/v1/query |
    jq '.results | length'

如果查询结果中 status 为 Running 或 Queued 的 workflow 数量持续增长,说明 Cromwell 的生产速度超过了 backend 的消费速度。

为什么队列会堆积?我总结了几种常见原因:

现象 可能原因 排查方向
队列积压,CPU 空闲 backend 资源不足(比如 HPC 排队、云 Batch 实例不够) 检查 backend 的并发数限制
队列积压,Cromwell CPU 高 Cromwell 自身调度逻辑卡顿 看 GC 日志、akka 线程池状态
队列很少,任务仍慢 任务本身有依赖等待(比如前序任务未完成) 查 workflow DAG 中每个 task 的延迟
我个人习惯: 在 Cromwell 的 metrics 端口(如果开启了 Prometheus 端点)暴露 workflow_count 和 task_count 指标,做成时间序列图。这样一眼就能看出队列积压是从什么时候开始的——往往能关联到前几分钟的某个配置变更。

说到 metrics,Cromwell 支持 Dropwizard Metrics,你可以配置输出到 Graphite、Prometheus 等。举个配置片段:

metrics {
  prometheus {
    enabled = true
    port = 9095
  }
}

然后就能在 http://localhost:9095/metrics 看到 JVM 和 Cromwell 内部的各种指标。非常方便。

Cromwell 性能监控五大维度 Cromwell 性能监控五大维度 Cromwell 引擎 CPU 负载 & GC JVM 堆 & 容器内存 磁盘空间 & IOPS 网络带宽 & 延迟 任务队列积压 监控不是事后诸葛,是跑步时的呼吸——要持续看、对比看、历史看 五个维度缺一不可,结合 Prometheus + Grafana 更佳

2.6 综合监控实践建议

最后我总结一下实际落地时怎么做:

  1. 先装一个 Prometheus + Grafana 栈——Cromwell 的 metrics 配置很简单,十分钟就能搭好
  2. 建立基准线——让 pipeline 正常运行三天,记录常规的 CPU、内存、磁盘、网络、队列深度
  3. 设定告警阈值——比如:
    • JVM 堆 > 80% 持续 5 分钟 → 告警
    • 磁盘使用率 > 85% → 告警
    • 队列中 task 数 > 200 且持续增长 → 告警
  4. 每次变更配置后,对比监控曲线——看看改了 backend 并发数之后,队列是不是降下来了
核心要记住一句话: 如果你没有监控数据,就不要谈性能优化。我见过太多人在群里问「为什么 Cromwell 跑得慢」,结果一问三不知——CPU 多少?不知道。磁盘满没满?不知道。那只能靠猜,猜出来的优化方案十有八九是错的。