Gradle 缓存优化
前言
在环境处于持续集成 (CI/CD)和快速开发的强需求下,构建效率成为团队开发迁展的关键因素。Gradle 作为一个强大的构建工具,在多种应用场景下应用广泛,特别是 Android 项目。
然而,随着项目规模增大,构建处理过程越来越复杂,构建时间很容易成为瓶颈。而 Gradle 就通过"构建缓存"系统,有效降低重复构建成本。
Gradle 支持两种缓存:
- 本地构建缓存 (Local Build Cache)
- 远程构建缓存 (Remote Build Cache)
本文将重点分析两种主流远程构建缓存方案:Gradle Enterprise 与自建 HTTP+S3/最低成本方案。
一、Gradle Enterprise: 官方构建性能解决方案
1. 简介
Gradle Enterprise 是 Gradle 官方推出的一整套 CI/CD 构建性能规划平台,提供包括:
- 构建扫描 (Build Scan)
- 构建性能分析
- 远程构建缓存
- 构建同步分析和异常跟踪
2. 优点
- 官方综合解决方案,100%兼容 Gradle
- 构建缓存的命中率统计和调试能力强
- 自动上传 build scan,便于分析瓶颈
- 支持 CI/CD 环境完全接入
3. 配置示例
在 settings.gradle.kts
中:
ini
plugins {
id("com.gradle.enterprise") version "3.16"
}
gradleEnterprise {
server = "https://ge.example.com"
buildScan {
publishAlways()
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
}
buildCache {
local { isEnabled = true }
remote(HttpBuildCache::class) {
isPush = System.getenv("CI") == "true"
url = uri("https://ge.example.com/cache/")
isAllowUntrustedServer = true
}
}
}
二、自建 HTTP 服务器 + S3/私有 MinIO 构建缓存
对于无法使用 Gradle Enterprise 的团队,可选择较低成本且自控的 HTTP 服务器 + S3/MinIO 合达方案。
1. 架构模型
rust
Gradle 端 <--> HTTP 服务器 (NGINX/自定义后端) <--> S3/私有 MinIO
方案核心:经由 HTTP 接口处理 Gradle 缓存请求,转储到 S3/MinIO 对象存储。
2. 后端 Go/软件服务示例
go
http.HandleFunc("/cache/", func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(r.URL.Path, "/cache/")
switch r.Method {
case "PUT":
data, _ := io.ReadAll(r.Body)
s3.PutObject("gradle-cache", key, bytes.NewReader(data))
case "GET":
obj, err := s3.GetObject("gradle-cache", key)
if err != nil {
http.NotFound(w, r)
return
}
io.Copy(w, obj)
}
})
- 推荐加入 gzip 压缩、缓存 TTL
- 可加入签名/鉴权策略
3. Gradle 端配置
ini
buildCache {
local {
isEnabled = true
}
remote(HttpBuildCache::class) {
url = uri("https://your-cache-server/cache/")
isPush = System.getenv("CI") == "true"
isAllowUntrustedServer = true
}
}
4. MinIO 部署建议
- 安装 MinIO server:
minio server /data
- 创建 gradle-cache bucket
- 配置用户权限:只允许读写 /cache/*
- 使用 S3 SDK 或 mc CLI 操作
三、方案对比总结
系统 | 最大优势 | 不足 | 适用场景 |
---|---|---|---|
Gradle Enterprise | 官方支持,全面分析 | 商用费用 | 大型团队 CI/CD |
HTTP + S3/MinIO | 成本低,简单配置 | 无内置分析和统计 | 中小型项目,有 DevOps 能力 |
--- 我是分割线 ---
好了,前面主要介绍 gradle 缓存使用情况,全部都是 gpt 生成的。下面说下我司 gradle 缓存遇到问题以及主要解决方法。
我这边用的是第二种方法,自建缓存服务,用的是 springboot + s3(minio) 方式。 但是一直以来我司的 gradle 缓存是关闭的,问问前面的一代目,主要说是有概率性出现缓存失败情况,或者与预期不一致的行为。
所以我就在夜间执行了定时打包服务,开启缓存,用来排查问题。同时开启 gradle --scan 选项, 可以查看整个编译数据。
通过编译报告(--scan 产生的)发现,确实有概率会导致打包失败,还有会缓存不一致的问题。
- 缓存服务503
通过报告看到,执行http请求,会概率性的出现503情况(http code),但是查看后台服务日志,并没有出现503,同时查看nginx的日志,也没有出现503的情况,猜测是采集率的问题,不太好排查。
如果出现多次503的情况,那么gradle 会认为远程缓存服务有问题,及后续的执行不再请求缓存,同时也不再上传缓存。那么会导致整个编译速度变慢。
我们这边采用了一个本地缓存服务,来绕过这个限制。就是在每个打包机上面部署一个本地的 http服务,做一个代理,当前请求缓存的时候,会直接转发的远程,远程有,那么直接下载,并返回,如果没有直接返回404,如果远程返回非200,非404,是5xxx相关从错误,那么直接降级到404,这样gradle 会认为,远程没有缓存,会继续执行,并上传缓存到本地,本地收到缓存以后,会再次上传到远程。相当于有一个中间层来屏蔽了5xx问题。同时本地也开启了磁盘缓存,相当于与多了一级缓存服务,gradle local缓存为一级,本地代理服务为二级,远程缓存服务为三级。这样也同时降低了远程缓存的请求,对服务器有一定的优化。
在实际的操作过程中,本地代理服务原先用 spring boot 改吧改吧就部署了,相当于把原先后端的服务,部署在打包机上面。后面通过监控来看,实际上的java 服务,在请求远程资源的时候会有一定的失败率,比如解析dns也会出现超时的情况,内存使用也超过了2g , 猜测是 gradle cpu利用率太高,会影响 java 进程。
在此基础上,我们本地服务做了一些优化,直接使用golang重新实现了一遍本地缓存,golang的协诚对于这种io操作极好,内存和cpu使用率也比较大,go启动的服务,内存长期在100m以内,这对于我一个长期使用java的人来说,真的是刷新了我的三观。
- 再谈503问题 对于上面的503问题,我们从gradle插件入手发现也能解决问题,我这边重写了一个缓存插件,感兴趣的同学可以看看
LiushuiXiaoxia/build-cache-ext: Gradle cache extension: more fine-grained configurations.
主要使用方式:
kotlin
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url = uri("https://jitpack.io") }
}
}
plugins {
id("com.github.liushuixiaoxia.extcache") version "$version" apply true
}
buildCache {
// 本地缓存建议禁用,因为 ExtBuildCache 也有本地缓存功能
local {
isEnabled = false
isPush = false
}
remote(com.github.liushuixiaoxia.extcache.ExtBuildCache::class.java) {
// 通用配置
isEnabled = true // 开启缓存
isPush = true // 推送缓存
// 本地缓存配置
// cacheDir = "" // 缓存路径, 默认为 ~/.gradle/cache-ext/cache
// minSize = 0 // 缓存最小大小,默认为0
// maxSize = 0 // 缓存最大大小,默认为1G
// expiredDay = 7 // 本地缓存过期时间,单位为天,默认为7天
// maxTotalSizeG = 100 // 缓存最大总大小,单位为G,默认为100G,按天清理,超过100G的缓存会清理全部
// 远程缓存配置
cacheUrl = "http://localhost:22333/gradle-cache/" // 设置缓存地址, 请替换为实际地址,不设置则不使用远程缓存
// timeout = 10 // 设置缓存请求超时时间,单位为秒,默认为10秒
fallback404 = true // 设置当远程服务请求失败,降级为404,防止异常导致后续无法使用缓存
retryCount = 2 // 设置请求失败重试次数,默认为2次
// 其他配置
// logDetail = true // 显示详细日志信息,默认为true
}
}
- 缓存不一致的问题 从报告来看,还有一些缓存路径错误的问题,比如A机器上面的获取到的缓存文件路,需要B机器上面缓存文件路,猜测可能是插件内部的bug。主要的解决方法是禁用这个task即可,让它每次都执行,从不使用远程缓存。
gradle
tasks.named("yourSpecialTask") {
outputs.cacheIf { false } // 禁用输出缓存
}
- 其他优化
实操过程中,发现某些任务使用缓存会比使用缓存的运行效果还差,比如实际执行可能只需要几秒,但是计算文件指纹和哈希,上传,下载,解压等操作加在一起,比如实际执行还要更长,这个时候,还不如直接禁用缓存。比如Android打包中,merge相关处理,dex相关等。我这边都是直接禁用的,禁用方法,参照上面。
优化结果
gradle运行主要分为三部分,初始化、配置、执行三个阶段,我们这边禁用缓存编译时间为25min。实际当中,初始化大概1min,配置和执行阶段差不多一样是12min。
通过上面的优化,实际执行阶段缓存利用率能够达到99%(当时修改差异不太大的情况下,如果改动差异太大,那么命中率也会降低不少),最终的编译速度能够降低到12-13min。
下面是我们这边编译报告截图。
