4. 缓存命中与未命中的判断逻辑

聊完缓存指纹,我们终于可以聊聊最核心的问题了:Cromwell 到底怎么判断一次任务是不是能命中缓存?

我以前刚接触这个时,觉得这应该是个黑盒,要么中要么不中。后来跟它斗智斗勇久了,才发现判断逻辑其实非常清晰。说白了就是 —— 拿着你新任务的指纹,去数据库里找一模一样的记录。

找到了,就是命中。找不到,就是不中。很直接,对吧?

但这里面有几个关键的“小机关”,不注意就容易翻车。我一个个拆给你看。

4.1 缓存命中的硬性条件

要命中缓存,必须同时满足下面三个条件。一个都不能少:

条件 说明 我的经验
指纹完全匹配 新任务的指纹,和在数据库里查到的指纹一模一样 字节差一位都不行,我见过因为换行符不同导致的未命中
任务元数据一致 runtime参数、Docker镜像、命令模板等基本信息必须一致 换一个镜像tag,哪怕内容一样,也会判为未命中
输出文件存在且完整 缓存记录的输出文件,还在原来的路径下,而且没被删 我就干过手动清理临时目录导致缓存无效的蠢事

嗯,这里要特别提一下第三个条件。很多人以为数据库里有记录就行,其实不是。Cromwell 在返回缓存结果前,会确认输出文件是否还在。如果文件被清除了,它会老老实实重新跑任务,而不是给你一个空链接。

注意: 文件系统的变动(清理、迁移、权限变更),都会让已有的缓存记录失效。这不是Bug,是安全机制。

4.2 判断逻辑的两种路径

Cromwell 的判断逻辑,可以分成两条路:Cache Hit(命中)和Cache Miss(未命中)。

我们先看命中这条路:

  • 第一步: 任务启动时,Cromwell 计算当前任务的指纹
  • 第二步: 去 Cromwell 的元数据库(MySQL/PostgreSQL)里查找这个指纹
  • 第三步: 找到匹配记录后,检查对应的输出文件是否存在且完整
  • 第四步: 通过了,直接复用输出文件,不调度计算资源

再看未命中的路:

  • 第一步: 计算指纹,查找数据库
  • 第二步(分叉1): 数据库里根本没这个指纹 → 未命中
  • 第二步(分叉2): 指纹找到了,但输出文件缺失/损坏 → 未命中
  • 第三步: 正常提交任务,让Worker执行

你想想看,这两个分叉在实践中哪个更容易出现?我个人经验,分叉2(文件问题)比分叉1(新任务)要隐蔽得多。至少有三四次,我在排查“为什么缓存没生效”时,最后发现都是文件被清理了,而不是指纹本身有问题。

核心判断逻辑可以概括为:
指纹匹配 + 文件存在 = 命中
指纹不匹配或文件不存在 = 未命中

4.3 判断优先级:哪个属性权重更高?

有一次在团队内部讨论时,有人问我:“如果指纹和元数据冲突了,Cromwell 听谁的?”

这个问题问得很好。但实际情况是,Cromwell 的缓存机制里不存在属性冲突。指纹本身就是元数据的哈希结果,它们是绑定的。

不过,从排查顺序上来说,我建议你按这个优先级来排查:

  1. 先看命中率日志 —— Cromwell 会在日志中输出 cache hit/miss 统计
  2. 再查具体指纹 —— 找到未命中的任务指纹,和缓存记录对比
  3. 最后看文件 —— 去输出路径确认文件是否完好

这样一层层排除,效率最高。我曾经教过一个实习生按这个顺序排查,15分钟就定位到问题了。

4.4 实战中的“假未命中”

什么叫“假未命中”?就是你以为应该命中,但实际没命中。

最常见的场景,是 Workflow 版本更新。你更新了 WDL 文件,加了一行注释,或者改了一个参数名。这时任务的指纹就会变,缓存自然不命中。

我遇到过最夸张的一次,是因为在一个 WDL 里多加了一个空格。是的,一个空格导致了缓存未命中。因为 Cromwell 的指纹是按文件内容严格计算的。

避坑指南: 如果你只是临时调试,不想每次都重新计算,可以在提交 Cromwell 任务时加参数 --write-cache false。不过这个仅限测试环境使用,生产环境别这么干。
java -jar cromwell.jar run myworkflow.wdl --write-cache false

另一个常见的“假未命中”是不同的调用路径。同一个 WDL,如果是从不同的子 Workflow 里调用的,虽然任务本身一模一样,Cromwell 也会判为不同。

4.5 用日志快速判断命中状态

查缓存问题,最快的方法是看 Cromwell 的日志。我一般直接 grep 关键词:

grep -i "cache" cromwell.log | grep -i "hit\|miss"

输出会像这样:

[INFO] [2025-04-15 10:23:12] Cache hit for job my_task:1bb5d3c
[INFO] [2025-04-15 10:23:15] Cache miss for job my_job:4f2a8e1 (no matching record)
[INFO] [2025-04-15 10:23:18] Cache miss for job my_job:7c3b9d2 (output file missing)

看到了吧?未命中时,Cromwell 会告诉你为什么未命中。如果是“no matching record”,说明指纹不存在,是新任务。如果是“output file missing”,说明指纹有,但文件丢了。

这个区别非常关键。我在现场给客户排查缓存问题时,就是靠这个日志快速定位的。有一次,客户说“缓存一直不生效”,我一看日志,全是“output file missing”,再一问,原来是运维同事刚清理了临时目录。

4.6 一次完整的判断流程

我把一次完整判断的流程画在了下面,你可以对照着看:

任务启动 计算指纹 查找数据库 指纹不匹配 指纹匹配 检查输出文件 未命中 命中 数据库查询结果 文件缺失 文件完整

4.7 现场排查小记

我记得有一次在生产环境排查缓存问题,用户反馈“改了输入文件后缓存不更新”。我当时第一反应是:换输入文件当然不会命中缓存了,因为指纹变了啊。

但用户坚持说“输入文件没变,只是改了输出路径”。我仔细一看,果然,用户输出路径改了。这导致文件完整性检查时,Cromwell 找不到旧路径的文件,以为缓存失效了。但用户的想法是“我要换一个地方存结果,但要用缓存的计算结果”。

这里有个原则需要搞清楚:缓存命中的前提是输出路径不变。如果你想要缓存的计算结果,但放到不同的输出路径,目前不支持。你需要先把结果从缓存目录复制过去。

快速检查技巧: 想知道某个任务为什么没有命中缓存,去 Cromwell 的日志里直接搜 Cache result for call,它会告诉你完整的判断过程和原因。
grep "Cache result for call" cromwell.log | tail -20

好了,缓存判断逻辑就这些。抓到关键点其实很简单:指纹 + 文件 = 命中。记住这个公式,排查起来就有方向了。