基于Gradle和Spring Boot的分层Jar Docker镜像打包
背景
笔者负责的项目部署在某某云之上,内外网完全隔离,通过一个带宽10M的小水管访问项目服务器,项目后端使用Spring Boot + Docker构建,构建后的服务运行在k8s中;应用构建使用公司本地服务器的Jenkins实现(需要访问公司的Gitlab、Nexus仓库和外网的Maven镜像),镜像构建使用项目内网环境中部署的Jenkins实现(构建完成后推送到Harbor以供k8s使用)。
工具和框架版本
| 名称 | 版本 |
|---|---|
| Gradle | 7.5.1 |
| Spring Boot | 2.6.13 |
| JDK | Open JDK 11 |
痛点
背景中描述的CICD流程虽然可以正常运作,也能通过某些手段进行自动化操作,但是带宽10M的小水管导致每次构建流程运行时间都超过10分钟(多个流程同时运行这个时间甚至会超过30分钟直到超时运行失败);10分钟左右的发布流程时长在平时影响不大,但是在周四的集中发版窗口和紧急线上BUG修复时极大影响工作效率,导致周四加班过长和线上BUG修复缓慢。
解决方案
查阅官方文档后,得知分层Jar镜像特性可以解决上述问题,下面记录下详细的操作步骤。
修改build.gradle
在build.gradle文件中的顶层新增以下内容:
jar {
// 不需要输出pure jar
enabled = false
}
bootJar {
// 不输出version和classifier以便于后续文件定位(最终打包输出jar名称为app.jar)
archiveBaseName = 'app'
archiveVersion = ''
archiveClassifier = ''
layered {
enabled = true
application {
intoLayer("spring-boot-loader") {
include "org/springframework/boot/loader/**"
}
intoLayer("application")
}
dependencies {
intoLayer("application") {
includeProjectDependencies()
}
intoLayer("snapshot-dependencies") {
include "*:*:*SNAPSHOT"
}
intoLayer("dependencies")
}
layerOrder = ["dependencies", "spring-boot-loader", "snapshot-dependencies", "application"]
}
}
dependencies层为release版本的依赖,spring-boot-loader层为Spring Boot的启动类,snapshot-dependencies层为快照版本的依赖,application层为服务自己的代码。执行BUILD后进入build\libs目录,执行java -Djarmode=layertools -jar app.jar extract命令解压fat jar查看分层效果:

可以看到分层后服务中会经常变化的部分已经被分离出来了,下面进行Dockerfile的编写。
修改Dockerfile
使用Docker的分阶段构建特性,实现分层镜像构建:
# 使用openjdk11作为基础镜像
FROM your-repo/openjdk:11-arthas as builder
# 工作目录
WORKDIR /app
# 将fat jar复制到工作目录下
COPY build/libs/app.jar ./app.jar
# 使用分层jar工具解压fat jar
RUN java -Djarmode=layertools -jar app.jar extract
# 使用openjdk11作为基础镜像
FROM your-repo/openjdk:11-arthas
# 工作目录
WORKDIR /app
# Spring Boot生效配置文件环境变量
ENV SPRING_PROFILES_ACTIVE=your-config
# 暴露端口
EXPOSE 8080
# 首先复制上一步分层jar中的普通依赖,该层一般不会变动
COPY --from=builder /app/dependencies/ ./
# 然后复制Spring Boot的启动类
COPY --from=builder /app/spring-boot-loader/ ./
# 接着复制快照版本依赖,该层变动比较频繁
COPY --from=builder /app/snapshot-dependencies/ ./
# 最后复制应用代码,该层变动最为频繁
COPY --from=builder /app/application/ ./
# 暴露启动命令给容器
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
需要注意的是,Spring Boot 3.x版本的启动类变成了org.springframework.boot.loader.launch.JarLauncher。下面是笔者生产环境中的一次发版Jenkins日志镜像推送部分截图:

可以看到本次镜像推送复用了大部分镜像层,仅推送了业务代码变动的一层,整体流程耗时降低到了1分钟出头:

镜像推送耗时仅不到10秒,相比之前的上传180m左右的fat jar的方案,带宽占用极大降低、构建流程并发极大提升、构建稳定性极大提升。
总结
Spring Boot的分层jar特性能够极大缩短服务部署到k8s中的资源消耗和带宽消耗,有效提升发版效率。不过该种方式也存在部分不足,如release版本依赖变动时耗时会比直接上传fat jar还要长;但是考虑到修改基础依赖的频率较修改业务代码的低很多,这个不足是可以接受的。

浙公网安备 33010602011771号