第2章 Call Caching 的工作原理
2.1 什么是 Call Caching
Call Caching 说白了就是让 Cromwell 记住它做过的事情。如果你提交的作业和以前一模一样,直接拿上次的结果给你,省得再跑一遍。
我刚开始用 Cromwell 的时候,对它的理解就是“一个 WDL 执行引擎”。后来有个任务因为参数写错,跑了一天才发现不对。改完参数重新提交,又傻等了一天。那时候我才意识到,要是它能自动识别“同样的输入,同样的命令,同样的环境,就不跑了”该多好。嗯,Call Caching 就是干这个的。
它的核心逻辑其实很简单:每个任务在执行前,Cromwell 会算出一个缓存键。这个键会记录任务的输入、命令、运行时属性、以及使用的 Docker 镜像。如果后面有完全相同的键,它就跳过执行,直接返回之前的结果。
2.2 工作流程:命中与未命中
Call Caching 的流程可以分成两条路:命中(cache hit)和未命中(cache miss)。我画了一张图,你先看一眼整体结构,再往下看细节。
具体步骤如下:
- 提交作业:Cromwell 接到 WDL 的工作流,开始解析。
- 计算缓存键:针对每个 task,Cromwell 收集输入、命令、运行时属性、Docker 镜像等,然后算出一个 SHA-256 哈希值。
- 查询缓存:Cromwell 去数据库(默认是 H2,生产环境一般用 MySQL)里找有没有相同的键。
- 命中:找到就直接把上一轮的结果文件拷贝过来,跳过执行。
- 未命中:没找到就正常跑,跑完后把结果和键存进数据库,下次就能命中了。
关键点:Call Caching 不仅仅是“跳过执行”,它还会把输出文件硬链接(hard link)或拷贝到新结果目录。你看到的是完整的输出,但 Cromwell 根本没用计算资源。
2.3 缓存键的构成
缓存键到底包含哪些东西?我记得有一次排查缓存不命中的问题,费了好大劲才发现是 Docker 镜像的 tag 从 latest 变成了具体的哈希值。虽然镜像内容一样,但 Cromwell 认为键不同。
下表列出了缓存键的所有组成部分:
| 组件 | 说明 | 举例 |
|---|---|---|
| 命令(command) | WDL 中 command 块里的完整 shell 命令 | |
| 输入(inputs) | 所有 task input 的取值(文件内容或值) | |
| 运行时属性(runtime) | cpu、memory、disk、docker 等 | |
| 表达式输出(expression outputs) | task 中 declaration 的变量值 | |
| 后端配置(backend) | Cromwell 配置中定义的后端名称和属性 | |
我曾经踩过的坑:在 WDL 里写死了绝对路径的中间文件,结果两次任务的临时目录不同,缓存永远不命中。后来把中间文件改成相对路径或用 write_lines() 函数,才解决。
2.4 哈希计算与校验
缓存键的哈希计算是 Cromwell 内部完成的。你可以在 Cromwell 的日志里看到它算出来的哈希值。比如这样:
2025-03-21 10:22:15,123 INFO - Call 'Workflow.SampleProcess' cache hit:
hash: a1b2c3d4e5f6...
results found in database
如果想自己验证一下当前任务的缓存键,可以用 Cromwell 提供的 API 接口。我个人习惯在测试阶段开启 call-caching 的 debug 模式,能看到详细的计算过程:
// Cromwell 配置中启用 debug 日志
call-caching {
enabled = true
debug-logging = true
}
开启后日志里会多出类似这样的信息:
DEBUG - Computing cache key for 'Workflow.SampleProcess'
- command: "samtools sort -o output.bam input.bam"
- inputs: [File(gs://bucket/input.bam)]
- runtime: {cpu: 4, memory: 8 GB, docker: ubuntu:20.04}
- hash result: a1b2c3d4e5f6...
如果发现哈希值和预期的不一样,就去检查这些组件项。最常见的差异是 Docker 镜像不一致,或者输入文件的路径换了。
2.5 生效条件与常见陷阱
Call Caching 不是随便就能生效的。它有几个硬性条件:
- 缓存必须开启:Cromwell 配置中
call-caching.enabled = true(默认就是 true,但有些生产环境会关闭)。 - 后端支持:所有官方后端(Local、PAPIv2、AWS Batch、SLURM)都支持,但自定义后端可能需要额外实现。
- 文件内容一致:Cromwell 对比输入文件时,用的是文件内容的 MD5,而不是路径。所以同一个文件放在不同路径下,内容一样就能命中。
- 运行时属性一致:包括 Docker 镜像、CPU、内存、磁盘等。哪怕只差 1 GB 内存,缓存也不会命中。
- 工作流版本一致:Cromwell 版本不同可能会改变缓存键计算逻辑,升级 Cromwell 后之前缓存的键可能失效。这是个大坑,我建议升级前先确认兼容性。
我的一个小技巧:在开发阶段,可以在 WDL 里给每个 task 加一个 runtime { ... } 的注释,比如 # cache-version: 1,方便后续排查。虽然 Cromwell 不认注释,但你自己回来查日志时能快速定位。
总结一下:Call Caching 的原理不复杂,但实际用起来细节很多。记住三个核心词:键、哈希、命中。键决定了能不能命中,哈希决定了键的唯一性,命中决定了是不是跳过执行。后面几章我会带你深入看日志、配置文件,以及如何手动调试缓存问题。