Gradle 缓存优化

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。

下面是我们这边编译报告截图。

相关推荐
弗锐土豆1 分钟前
一个基于若依(ruoyi-vue3)的小项目部署记录
前端·vue.js·部署·springcloud·ruoyi·若依
Hilaku4 分钟前
我为什么放弃了“大厂梦”,去了一家“小公司”?
前端·javascript·面试
Codebee14 分钟前
OneCode 组件服务通用协议栈:构建企业级低代码平台的技术基石
前端·前端框架·开源
Running_C14 分钟前
常见web攻击类型
前端·http
jackyChan15 分钟前
ES6 Proxy 性能问题,你真知道吗?🚨
前端·javascript
lichenyang45315 分钟前
快速搭建服务器,fetch请求从服务器获取数据
前端
豆苗学前端19 分钟前
从零开始教你如何使用 Vue 3 + TypeScript 实现一个现代化的液态玻璃效果(Glass Morphism)登录卡片
前端·vue.js·面试
光影少年21 分钟前
react16-react19都更新哪些内容?
前端·react.js
奇舞精选28 分钟前
用 AI 提效的新方式:全面体验 Google Gemini CLI
前端·google·ai编程