3 构建缓存失效排查
排查口诀:先查标志是否开,再看哈希变没变;键值计算逐层对,远程元数据是标尺。
3.1 现象识别
构建缓存失效的典型表现包括:
- 二次构建时间无改善:完全相同的源码和配置,第二次构建依然全量编译。
- 远程缓存命中率为 0%:构建扫描(Build Scan)中
cacheable任务数量正常,但命中率始终为 0。 - 跨机/CI 环境构建不共享缓存:同一份代码在不同机器上各自全量构建,没有从远程缓存拉取结果。
- 任务输出不一致:部分任务从缓存恢复,部分任务重新执行,导致构建时间异常波动。
3.2 数据采集
执行以下命令收集缓存失效的第一手数据:
# 开启 --build-cache 并输出 INFO 级别日志
./gradlew clean assemble --build-cache --info 2>&1 | tee build-cache.log
# 生成 Build Scan(用于后续分析缓存键)
./gradlew clean assemble --build-cache --scan
# 查询远程缓存状态(以 Gradle Enterprise 为例)
curl --header "Authorization: Bearer ${ACCESS_}" \
"https://ge.example.com/api/build-cache/v1/cache/${CACHE_KEY}"
数据采集清单:
| 数据项 | 来源 | 关键字段 |
|---|---|---|
| 构建日志 | --info 输出 | Cache miss / Cache hit / Cache entry not found |
| Build Scan | --scan 报告 | Task cacheability、Cache key breakdown、Input file fingerprints |
| 远程缓存元数据 | HTTP 响应头 | X-Gradle-Cache-Key、Content-Length、ETag |
| 本地缓存目录 | ~/.gradle/caches/ | build-cache-* 目录、.lock 文件 |
3.3 原因定位
常见陷阱:即使开了
--build-cache,若 build.gradle 中使用了 outputs.upToDateWhen { false } 或 outputs.cacheIf { false },任务依然不会进入缓存路径。
3.3.1 --build-cache 标志验证
检查构建命令是否真的启用了构建缓存:
# 方法1:查看 Gradle 启动参数
./gradlew --version | grep "Build cache"
# 方法2:从 --info 日志中搜索
grep -i "build cache" build-cache.log
# 预期输出: "Using build cache ..." 或 "Build cache is enabled"
如果日志出现 Build cache is disabled,则检查:
- 命令行是否省略了
--build-cache。 - 项目根目录下
gradle.properties是否设置了org.gradle.caching=false。 - 环境变量
GRADLE_OPTS或JAVA_TOOL_OPTIONS是否覆盖了缓存设置。
3.3.2 缓存键(Cache Key)计算差异
每个可缓存任务都有一个唯一的缓存键。当缓存键不匹配时,即使构建输入完全相同,缓存也无法命中。
| 缓存键组成部分 | 常见差异源 | 排查方法 |
|---|---|---|
| 任务类名 | 插件版本不同导致类路径变化 | Build Scan → Task details → Class |
| 输入文件的路径与哈希 | 构建根目录不同、符号链接替换 | 对比两台机器的 gradle/user-id.values |
任务参数(如 --release) | gradle.properties 默认值不一致 | 打印 getOptions().getAll() 到日志 |
| Gradle 版本号 | 跨版本不兼容 | ./gradlew --version |
| 操作系统 / 架构 | macOS 与 Linux 差异 | 检查 Build Scan → Build environment → OS |
排查动作:在 Build Scan 中点击“Cache key breakdown”链接,展开每个任务的缓存键计算树。重点关注
inputFileProperties 和 taskProperties 两个子项的哈希。如果两个成功命中的机器在此树上的任一节点哈希不一致,则缓存键必然不匹配。
3.3.3 输入文件哈希变化追踪
即使代码没有变更,以下操作会导致输入文件哈希变化:
- CI 中 checkout 时间戳不同:Gradle 在 7.x 版本中默认使用
fileHash,但某些插件仍依赖fileLastModified。 - 构建缓存目录被部分清理:其他并发构建进程删除了
.gradle/caches/build-cache-1/下的文件。 - 依赖解析结果变化:即使
build.gradle不变,远程仓库的maven-metadata.xml可能已更新,导致依赖版本静态哈希不同。
使用以下脚本对比两台机器的输入文件指纹:
# 在机器A和机器B分别运行
./gradlew :app:compileJava --build-cache --info 2>&1 | \
grep -E "(Input file|file hashes|fingerprint)" > fingerprints_A.log
# 对比差异
diff fingerprints_A.log fingerprints_B.log | head -30
3.3.4 远程缓存元数据比对
当本地缓存缺失,且远程缓存无法命中时,需要比对远程缓存元数据:
# 获取本地预期的缓存键(来自 Build Scan)
EXPECTED_KEY="1a2b3c4d..."
# 查询远程缓存状态
curl -I "https://ge.example.com/api/build-cache/v1/cache/${EXPECTED_KEY}"
# 如果返回 404,则远程无此键
# 如果返回 200,检查 Content-Length 是否为空
# 如果返回 304(未修改),说明缓存键已存在但本地缓存过期
元数据判断规则:
- 本地无缓存条目 + 远程 404 = 缓存从未被写入。
- 本地无缓存条目 + 远程 200 + Content-Length = 缓存键有效,可从远程恢复。
- 本地有缓存条目 + 远程 200 = 元数据一致,检查本地缓存是否损坏(尝试
- 本地无缓存条目 + 远程 404 = 缓存从未被写入。
- 本地无缓存条目 + 远程 200 + Content-Length = 缓存键有效,可从远程恢复。
- 本地有缓存条目 + 远程 200 = 元数据一致,检查本地缓存是否损坏(尝试
--no-build-cache 清空本地缓存后重试)。
3.4 修复动作
根据定位的根因,选择对应的修复方案:
| 根因 | 修复动作 | 验证方法 |
|---|---|---|
| --build-cache 未生效 | 在 gradle.properties 添加 org.gradle.caching=true,或修改 CI 脚本确保每次传递 --build-cache | 重建后日志输出 "Build cache is enabled" |
| 缓存键计算差异 | 锁定 Gradle 版本、插件版本、JDK 版本;规范构建根目录路径 | Build Scan 中两台机器的缓存键 breakdown 完全一致 |
| 输入文件哈希变化 | 移除 outputs.upToDateWhen 条件;设置 fileHashes 为稳定实现;清理 CI 工作区的路径差异 | 对比两次构建的 inputFileProperties 哈希保持一致 |
| 远程缓存元数据不一致 | 检查远程缓存配额和清理策略;确保所有构建节点使用相同的远程缓存 URL 和认证令牌 | 手动查询远程缓存键返回 200 且 Content-Length 非空 |
极端情况:如果修复后依然失效,可以尝试将
org.gradle.caching.debug=true 写入 gradle.properties,然后在日志中搜索 Cache key for task 和 inputs.files 的完整指纹树。逐行对比两个构建的指纹树,定位到最小不一致节点。
3.5 复测
执行以下标准复测流程:
- 清理本地缓存:
rm -rf ~/.gradle/caches/build-cache-* - 构建一次:
./gradlew clean assemble --build-cache(预期全量构建,填充本地缓存) - 立即重建:
./gradlew assemble --build-cache --info > rebuild.log - 验证命中率:从
rebuild.log统计Cache hit行数,预期所有可缓存任务均命中 - 远程验证:在另一台机器或在相同机器上使用
GRADLE_USER_HOME指向空目录,重复步骤2-3,确保远程缓存命中
复测结果判断:
- 成功:第二次构建时间缩短 80% 以上,任务日志显示
FROM-CACHE。 - 部分成功:部分任务命中、部分 Miss → 定位未命中的任务,重复 3.3 节的排查流程。
- 完全失败:回到 3.1 节重新识别现象。
3.6 预防
预防清单:
- 版本锁定:将 Gradle 版本、插件版本、JDK 版本写入
gradle-wrapper.properties和 CI 配置。 - 规范化构建环境:使用 Docker 容器或统一构建镜像,确保
user.dir、os.name等系统属性一致。 - 输入文件稳定性:避免在构建过程中修改
src目录下的文件;生成文件的输出目录使用build/,不要与源文件混淆。 - 远程缓存监控:在 CI 中定期运行缓存命中率检查脚本,当命中率连续下降超过 10% 时触发告警。
- 定期审计:每两周执行一次全量构建并生成 Build Scan,对比上一周期的缓存键分布。
对于持续集成的场景,建议在 buildSrc 或 settings.gradle.kts 中预制缓存键差异检测逻辑:
// settings.gradle.kts 片段:缓存键版本标记
buildCache {
local {
isEnabled = true
removeUnusedEntriesAfterDays = 7
}
remote {
url = uri("https://ge.example.com/cache/")
isPush = System.getenv("CI") == "true"
// 缓存键版本前缀,确保跨构建不意外匹配
credentials {
username = "build-cache"
password = System.getenv("GRADLE_CACHE_PASSWORD") ?: ""
}
}
}