Cromwell 运行机制与日志生成原理
想用好日志分析工具,首先得弄明白日志是怎么来的。
Cromwell 不是个黑盒子,它内部有一套清晰的执行逻辑。我刚开始接触时也犯过迷糊——日志文件一大堆,不知道看哪个,也不知道每个字段代表什么。后来花时间捋了一遍它的运行机制,嗯,再回头看日志就豁然开朗了。
这一章,我就带你拆开 Cromwell 看看。你搞清楚它怎么干活,日志就自然能读懂了。
工作流程的核心拆解
Cromwell 本质上是一个工作流引擎。它不直接跑任务,而是帮你协调任务。你写一个 WDL 文件,描述好“先做什么、再做什么、用什么镜像、传什么参数”,Cromwell 就按这个剧本去调度后端(比如本地机器、HPC 集群或者云上的 Batch 服务)。
我个人习惯把它的执行过程分成四个阶段:
- 解析阶段:读取你的 WDL 和 JSON 输入文件,构建成一张内部的任务依赖图(DAG)。说白了,就是把你的“工作流剧本”翻译成引擎能懂的数据结构。
- 调度阶段:根据 DAG 决定什么时候跑哪个任务。如果任务 B 依赖任务 A 的输出,Cromwell 会等 A 跑完才启动 B。
- 执行阶段:把具体的 bash 命令或 Docker 命令扔给后端去跑。这一步最耗时,也最容易出问题。
- 收尾阶段:收集每个任务的退出码、标准输出、标准错误,更新工作流的最终状态。
日志就在这四个阶段里源源不断地产生。
关键认知:每一阶段的日志记录点不同。解析失败查 WDL 语法日志;调度卡住查数据库连接日志;执行报错查后端日志和任务 stderr。你按阶段去定位,效率最高。
日志到底从哪来?
日志不是凭空生成的。每一条 Cromwell 日志,都对应着引擎内部某个组件的动作。我整理了一张图,帮你直观地理解日志的来源和流向。
图里有个细节我想强调一下:日志产生后,最终会写入两个地方——数据库(存储工作流元数据)和文件系统(存储 stdout、stderr、cromwell.log)。所以排查问题时,这两个来源都得查。
日志的时间线与状态机
每条日志都带一个时间戳,这很好理解。但 Cromwell 内部有一个更重要的东西——工作流状态机。
工作流和任务的状态流转是固定的。我列了一个最常见的状态变化表,你对照着看日志里的状态字段,基本就知道当前卡在哪个环节了。
| 状态 | 含义 | 常见日志关键词 |
|---|---|---|
| Submitted | 工作流/任务已提交,等待调度 | WorkflowSubmitted, JobSubmitted |
| Running | 正在执行中 | WorkflowRunning, JobRunning |
| WaitingForOutputs | 任务已完成,等待收集输出 | WaitingForOutputs |
| Failed | 执行失败,退出码非零 | WorkflowFailed, JobFailed, exitcode |
| Succeeded | 执行成功,退出码为零 | WorkflowComplete, JobSucceeded |
| Aborted | 被用户或系统终止 | Aborted, abort |
我印象很深的一次,有个任务一直是 Running 状态却不往下走。我查了日志,发现它卡在 WaitingForOutputs 阶段——原因是任务脚本里有一个大文件输出,Cromwell 在等它落盘,但磁盘满了。这个案例让我养成了一个习惯:先查任务的状态,再去看对应阶段的日志,缩小范围比你逐行翻日志快得多。
日志文件的层次结构
Cromwell 的日志文件不是随便扔的。默认情况下,你会在工作目录下看到这些文件:
cromwell.log— 引擎自身运行日志,包含解析、调度、连接数据库等全局信息。workflow.log— 按工作流实例生成的日志,通常在 metadata 输出目录下。job-{hash}/stdout— 单个任务的标准输出。job-{hash}/stderr— 单个任务的标准错误,这是最常看的文件。job-{hash}/rc— 任务的退出码,纯数字。job-{hash}/script— 实际执行的那段 bash 脚本,用于核对命令是否正确。
我的一个小技巧:别每次都进目录翻。直接用 grep 配合时间范围查询。例如:grep "WorkflowExecutionState" cromwell.log | grep "Failed",一秒定位所有失败任务的状态变更。
为什么日志有时不完整?
这个问题我经常被问到。
Cromwell 默认有一个日志缓冲区,不是每产生一行日志就立刻刷到磁盘。这么做是为了性能。但如果 Cromwell 进程突然崩溃(比如被 kill -9 杀掉),缓冲区里的日志就丢了。
我曾经踩过一个大坑:一个跑了 3 天的 WDL 任务突然挂了,我跑去查日志,发现最后一条记录停在任务运行中,没有任何错误信息。后来才发现是宿主机 OOM,Cromwell 直接被系统 kill 了,缓冲区里的错误日志根本没来得及写。
所以,生产环境中建议开启即时刷写。配置方式很简单,在启动 Cromwell 时加上 JVM 参数:
-Dlogback.statusListenerClass=ch.qos.logback.core.status.OnConsoleStatusListener
-Dlogback.configurationFile=/path/to/logback.xml
然后在 logback.xml 里把 <appender> 的 immediateFlush 设为 true:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>cromwell.log</file>
<immediateFlush>true</immediateFlush>
...
</appender>
开了这个之后,日志实时落盘。性能会有轻微影响,但在绝大多数场景下完全可以接受。与其在崩溃后一头雾水,不如牺牲一点点 IO 换取及时的可观测性——这笔账我觉得很划算。
日志里的时间到底是谁的时间?
嗯,这个问题不说清楚,你会被时间戳搞疯。
Cromwell 会在日志里记录多个时间:
- 系统时间(System.currentTimeMillis): 也就是引擎所在服务器的时间,用于记录事件发生时刻。这是最主要的时间戳。
- 工作流时间(WorkflowStart / WorkflowEnd): 从 WDL 元数据里提取的时间,由 Cromwell 自己计算。
- 后端任务时间: 比如在 HPC 集群上,任务实际开始和结束的时间可能和 Cromwell 记录的不一致(因为调度队列有延迟)。
我个人习惯以 Cromwell 日志里的系统时间为准来分析事件顺序。后端任务的实际时间只用来判断执行时长,不作为事件排序的依据。因为后端任务有排队等待时间,按 Cromwell 日志排出来的顺序才是真正的调度顺序。
举个例子你就明白了:你提交了两个任务 A 和 B,A 分到了空闲节点,B 排队等了 10 分钟。后端日志里 B 的开始时间比 A 晚 10 分钟,但在 Cromwell 日志里,A 和 B 的 Submitted 时间只差几秒。这时候如果你按后端时间分析流程,会误以为调度有问题。其实没问题,只是队列延迟。
注意:如果你的 Cromwell 服务器和后端执行器不在同一台机器上,且时间不同步,日志里的时间对比会出现偏差。建议全集群使用 NTP 同步时钟。我踩过的坑里,有一半是因为时间没同步导致的“灵异事件”。
小结一下
这一章我们拆了 Cromwell 的“黑盒子”,明白了一条日志产生背后的四个阶段:解析、调度、执行、收尾。你看,每个阶段产出的日志都不同,状态机帮你定位阶段,时间戳帮你还原事件顺序,日志文件的层次结构告诉你去哪里找具体信息。