第 3 章 构建缓存命中率分析

3.1 现象识别:缓存效率低下的典型表现

当构建缓存命中率偏低时,你会在开发过程和 CI 流水线中观察到以下现象:

  • 增量构建时间无明显缩短——即使只改了一行注释,整个编译任务依然重跑。
  • 构建扫描中“缓存未命中”条目占比超过 60%(本地 + 远程合计)。
  • 远程缓存下载/上传频繁,但实际任务输出并没有被复用。
  • 开发环境与 CI 环境构建结果不一致,同一份代码在两个环境都大量重编译。
注意:如果本地缓存命中率接近 100% 但构建仍然很慢,问题可能是 I/O 瓶颈而非命中率;如果远程缓存命中率接近 0%,需优先检查远程缓存服务是否可达以及凭证配置是否正确。

3.2 数据采集:从构建扫描读取命中率

Gradle 构建扫描(Build Scan)是分析缓存命中率的首要工具。使用以下命令生成带有缓存详细信息的扫描:

./gradlew clean build --build-cache --scan
# 或
./gradlew assemble --build-cache -Dscan.tag=CACHE_AUDIT

扫描上传后,在浏览器中打开链接。重点关注 “Performance > Task Execution” 面板:

  • FROM-BUILD-CACHE —— 远程缓存命中(从缓存恢复任务输出)。
  • FROM-LOCAL-BUILD-CACHE —— 本地缓存命中。
  • NOT-CACHED —— 未命中(任务正常执行)。
  • EXECUTED —— 因输入变化而必须执行。
指标名称含义阈值参考
本地缓存命中率(FROM-LOCAL-BUILD-CACHE 任务数) / (总任务数 - 强制跳过任务数)> 80% 良好;< 30% 需干预
远程缓存命中率(FROM-BUILD-CACHE 任务数) / (总任务数 - 强制跳过任务数)CI 场景期望 > 70%;开发环境 > 40%
全局缓存命中率(本地命中 + 远程命中) / 总任务数> 75% 理想;< 50% 需紧急排查
操作指引:如果构建扫描中缺乏“缓存”分类,请确认使用了 Gradle 5.0+ 并加上 --build-cache 参数。也可以直接在 build.gradle 中添加 buildCache { local { enabled = true }; remote { enabled = true } }。

3.3 原因定位:命中率低的核心故障树

将扫描中的“未命中”任务展开,逐级排查。下图展示了故障树结构:

缓存命中率故障树与排查流程 缓存命中率低(扫描结果) 是远程缓存未命中为主? 是 否 远程缓存链路故障 ● 网络超时/防火墙限制 ● 凭证过期或目录错误 ● 缓存服务器不可用 缓存键不匹配 ● 路径含本地环境变量 ● JDK/工具版本不一致 ● 时间戳或随机值参与 计算理想命中率并对比差异 合并后进入对比阶段

排查时使用下方判断规则表快速定位:

扫描中的缓存分类可能原因验证方法
FROM-BUILD-CACHE 数量为 0远程缓存地址错误或网络被阻断curl -I https://cache.example.com/ 测试可达性
FROM-LOCAL-BUILD-CACHE 远高于 FROM-BUILD-CACHE远程缓存未正确配置 push 策略检查 buildCache { remote { push = true } }
NOT-CACHED 大量出现任务输入中包含不可缓存因素(如 fileTree 动态文件)在 build.gradle 中标注 outputs.cacheIf { true }
EXECUTED 比例高增量更改率高,但期望应复用之前缓存检查任务输入是否包含 inputs.property 随意增量

3.4 计算理想命中率

理想命中率是假设所有可缓存任务的输入在两次构建间完全一致时能达到的最高命中率。计算步骤如下:

  1. 确定可缓存任务集合:排除 no-cache 任务(如 clean、check 中部分非幂等任务)。
  2. 统计实际未命中但理论上可命中的任务:查看构建扫描中 NOT-CACHED 任务,通过对比前一次构建的输入摘要来判断是否存在变动。
  3. 公式:
    理想命中率 = (可缓存任务总数 - 理论上不应变动的未命中数) / 可缓存任务总数。
案例:假设构建共 200 个任务,5 个为 no-cache,剩余 195 个可缓存。构建扫描显示 NOT-CACHED 有 80 个,但其中 30 个是因为输入包含当前时间戳。那么理想命中率 = (195 - 30) / 195 ≈ 84.6%。你的实际命中率若低于此值,说明存在可控的缓存键污染。

3.5 修复动作:提升命中率的五项措施

  1. 规范化输入端路径:使用相对路径,禁用 absolutePath 作为任务输入。
  2. 稳定化工具版本:在 gradle.properties 中固定 JDK 和 Gradle 版本,避免 CI 动态更改。
  3. 启用远程缓存 Push:确保 CI 节点在构建时同时推送缓存到远端,见示例:
// settings.gradle
buildCache {
    remote(HttpBuildCache) {
        url = 'https://cache.mycompany.com/cache/'
        push = System.getenv('CI') == 'true'  // 仅 CI 推送
    }
}
  1. 移除不可缓存输入:如 inputs.property("timestamp", System.currentTimeMillis()) 改为固定字符串。
  2. 使用缓存标记(Cache Markers):对大型任务添加 @CacheableTask 注解并显式声明 inputs。
谨慎操作:在开启全部修复前,先保存当前构建扫描作为基线。每项修复后单独运行一次构建并生成新扫描,确保没有引入缓存损坏或错误命中。

3.6 复测:验证命中率提升

修改配置后,执行两次相同的构建,第二次应明显更快。执行以下步骤:

  • 步骤1:./gradlew clean build --build-cache(首次生成缓存)
  • 步骤2:./gradlew build --build-cache(复用缓存)
  • 步骤3:查看构建扫描中的缓存分类,确认 FROM-BUILD-CACHE 占比是否超过 70%。

若仍不达标,重复“数据采集 → 原因定位”循环,重点关注之前未处理的输入差异。

3.7 预防:持续监控缓存命中率

将缓存命中率作为构建健康度指标集成到 CI 面板中。推荐做法:

  • 在 Gradle Enterprise 或自定义脚本中定期导出构建扫描的 API,提取缓存分类数据。
  • 设置警报阈值:全局命中率 < 60% 触发通知。
  • 每周审查一次“未命中频率最高”的任务,并与代码变更关联。
  • 使用 --build-cache 作为默认参数,阻止个别开发者绕过缓存。
自动化示例:利用 Gradle Enterprise API 下载 JSON 格式的扫描数据,编写脚本解析命中率并写入监控系统。例如使用 curl -s "https://ge.company.com/api/scans/<scanId>?taskExecution=true" | jq '.performance.cache'。