介绍

Gradle 与 Maven 类似都是作为项目构建工具使用,相比于 Maven 繁琐的 XML 配置,Gradle 使用简洁的 Groovy 语言来进行配置,具有更强的灵活性。

最近刚好有几个小项目要使用 Gradle 构建就顺便把 gradle 添加到了集成环境中,gradle整体使用下来还是比较顺畅的,构建速度感觉相比maven也有一定的提升。之前使用 Maven 的时候,会使用 maven 的缓存来避免重复下载依赖,以提高项目的构建速度。gradle 同样也可以使用缓存来提高速度。

这里多说一句,maven 可以通过跳过测试代码 -Dmaven.test.skip=true和使用多线程编译参数 -Dmaven.compile.fork=true提高构建速度。

下面我们介绍一下 gradle 缓存的使用以及 jenkins-slave 容器如何在并行项目构建时使用缓存。

Gradle 缓存锁


Gradle 的依赖缓存能最大程度地减少在依赖项解析中发出的远程请求的数量,缓存在内部仓库不完善、网络状况不佳的时候能极大提高构建速度。但是在使用的过程中发现 gradle 的缓存存在锁机制,gradle官方对于 Cache Locking 的描述如下:

Cache Locking
The Gradle dependency cache uses file-based locking to ensure that it can safely be used by multiple Gradle processes concurrently. The lock is held whenever the binary metadata store is being read or written, but is released for slow operations such as downloading remote artifacts.

This concurrent access is only supported if the different Gradle processes can communicate together. This is usually not the case for containerized builds.
Gradle依赖项缓存使用基于文件的锁定来确保多个Gradle进程可以安全地同时使用它。每当读取或写入二进制元数据存储时,都会保留该锁,但是会为缓慢的操作(例如下载远程仓库的构件)而释放该锁。

仅当不同的 gradle 进程能通信时,才支持同时并发使用缓存。对于容器化的构建来说通常都不支持并发访问缓存

刚好我们的集成环境都是使用的容器来做的构建,构建任务之间都是相互独立的,对于默认的缓存目录同一时间只能有一个 gradle 进程使用,这对于并发构建来说极为不利。下面我们结合基于 kubernetes 的集成环境,来解决 gradle 下的并发构建问题。

Gradle 并发构建


先介绍一下集成环境的框架结构,简要示意图如下:

集成环境
集成环境

Jenkins Master 通过 JNLP 在kuberntes 上动态创建 agent pod,agent 执行制定好的流水线,完成集成部署任务。多个agent pod 之间通过挂载 nfs 目录实现脚本配置文件和缓存的共享。对于 gradle 进程来说由于共享了缓存目录,而进程之间又相互独立,所以必然会出现缓存锁带来的不能并发构建冲突问题。构建时产生的错误一般如下:

"It is currently in use by another Gradle instance"

解决这个问题的方法其实也很简单,因为我们的并发构建都是基于不同的项目,我们只需要让不同的项目的 gradle 缓存目录分开即可,这样对于每一个项目来说其缓存目录都是独享的。

gradle 官方提供了两个可选命令参数 -g --project-cache-dir 来定义缓存目录:

-g, --gradle-user-home
Specifies the Gradle user home directory. The default is the .gradle directory in the user’s home directory.

--project-cache-dir
Specifies the project-specific cache directory. Default value is .gradle in the root project directory.

下面以 pipeline 脚本为例,介绍一下参数的使用方式:

# gradle 构建模块
stage('Gradle Build') {
    container('deploytools') {
        if ("${params.stage_choice}" == 'Full' || "${params.stage_choice}" == 'Build') {
            echo '[INFO] start build process'
            if ("${params.deploy_env}" == 'test') {
                sh """
                    gradle4.10 -g /root/.gradle/${JOB_NAME} clean build ${params.GRADLE_ARGS}                        
                """       
            }
            else if ("${params.deploy_env}" == 'release') {
                sh """
                    gradle4.10 -g /root/.gradle/${JOB_NAME} clean build ${params.GRADLE_ARGS}                         
                """                       
            }
            else {
                echo '[ERROR] <deploy_env> parameters error'
            }  
        }      
        else {
            echo '[INFO] skip Build'
        }   
    }
}

直接使用 -g 参数在 pipeline 中指定 agent pod 的/root/.gradle/${JOB_NAME}目录为当前项目的 gradle 缓存目录。在 jenkins 中对于每一个项目来说其 ${JOB_NAME} 都是不相同的,这样就达到了将项目缓存目录分开的效果。

一个小技巧 gradle 命令中的 ${env.GRADLE_ARGS} 参数交互式输入可以用于构建调试。

问题:

当我们在项目中引用了自己开发的依赖库时,Gradle 缓存可能会导致更新的依赖库版本未同步的问题。解决办法也很简单:构建时更新依赖缓存即可。

解决:

使用 gradle CLI 参数 --refresh-dependencies 强制更新依赖库,这里 ${env.GRADLE_ARGS} 参数就会发挥作用了:

gradle4.10 -g /root/.gradle/${JOB_NAME} clean build --refresh-dependencies