- 前言
容器技术已经成为当前软件开发的技术标准,我们周边的项目也越来越多选择容器化,而Docker已然成为我们不可或缺的软件开发工具。我们通常通过DockerFile将Spring Boot应用打包为Docker镜像,在业务初期,这一切看起来都很美好。然而随着业务的发展与需求的快速响应,任意的代码变动,镜像都需要重新的构建,导致我们的CI/CD耗时很久,效率低下。 今天,将为大家介绍如何使用Spring Boot的新功能来复用Docker Layers,从而提升CI/CD效率。
- 背景
作为以微服务为核心架构的技术团队,我们几乎每天都需要和Spring Boot、Docker打交道,这种技术架构为我们带来了极大的灵活性和可拓展性。但是随着我们业务的发展,慢慢的我们发现应用的镜越来越大,消耗的带宽、磁盘、构建时间越来越多,我们的CI/CD效率从刚开始的几分钟慢慢的增长到几十分支。即使是一个微小的改动,上线时间也是惊人的“长”。看起来这些问题都是不起眼的,但却真实的影响着每位研发人员的开发效率。一些新的技术正在悄悄的改变这一现状,是的,我们的老朋友Spring Boot为大家带来了以“隔离”为核心思想的解决方案。
-
Docker 架构
- Clients - 使用户可以和Docker交互
- Daemon - Docker守护进程,管理Docker镜像、容器、网络、存储等;监听DockerAPI请求;
- Image - 用于构建容器的只读的二进制模板,包含了容器的功能、需要的元数据等
- Container - 容器是镜像可运行的实例,提供了一种轻量级的且独立于操作系统的可以运行应用程序的方式;
- Repository - 镜像仓库
-
分层机制
如果想要对Docker镜像优化,我们首要的是要了解镜像的分层机制
- 分层机制
* Docker镜像由很多“层”组成,每一“层”代表了DockerFile中的一个命令,每一层都是基于基础层变化的增量,增量的构建自下而上。
* 当我们构建Docker镜像时,这些“层”会缓存在宿主机中,这些“层”是可以复用的,也就给我们优化的空间和机会。
-
优化原理
- 根据上面Docker Image构建的原理,如果我们把易变化的“层”放在底部,那么我们每次构建它,都需要把它的顶层去掉,重新构建;相反,如果我们易变化的放在顶层,就可以减少工作量了,这就是我们优化Image的核心思想。
-
Spring Boot Jar优化
-
Spring Boot fat jar
FROM openjdk:11-jre-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]
- 优化思路
-
根据上述的Docker Image的优化原理,如果我们把Spring Boot fat jar,拆分为多个“层”,并重用宿主机缓存中的“复用层”,那么它的构建效率也会大大提升。
-
同时,根据应用的变化频率,我们可以对Spring Boot应用进行上下分层。
-
如下所示,应用程序有自己的“层”,我们修改源码时,只需要重新构建独立的“应用层”,其他“层”则使用“可复用层”,从而减少了Docker Image的构建和启动时间。
-
application - 核心业务层及配置,最经常变动 - 最高
-
snapshot-dependencies - 业务层依赖,时常变动 - 高
-
spring-boot-loader - spring-boot jar loader,不经常变动 - 低
-
dependencies - 基础依赖,不经常变动 - 低
-
Spring Boot镜像优化
-
生成“分层”Fat Jar
-
Maven 配置 Spring-Boot >=2.3.0
-
Spring Boot将生成“分层”的fat jar(将分层工具加入到Jar)。
-
我们使用Spring Boot “分层”工具将“层”提取到Docker 镜像中。
-
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<includeLayerTools>true</includeLayerTools>
</layers>
</configuration>
</plugin>
</plugins>
</build>
- “分层”工具 - spring-boot-jarmode-layertools
- 生成fat jar - mvn package
- 分层展示 - java -Djarmode=layertools -jar app.jar list
- 分层提取 - java -Djarmode=layertools -jar my-app.jar extract
/dependencies
/spring-boot-loader
/snapshot-dependencies
/application
- 索引文件
jar tf target/application.jar
BOOT-INF/layers.idx
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
- 自定义“层”的划分
// maven configuration
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
// custom layer configuration
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-3.0.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="application">
<includeModuleDependencies />
</into>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
- Docker 镜像构建
- Multi-stage Docker build
- Stage1 - 基础构建
FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
- Stage2 - 分层构建
FROM openjdk:11-jre-slim
WORKDIR application
COPY – from=builder application/dependencies/ ./
COPY – from=builder application/spring-boot-loader/ ./
COPY – from=builder application/snapshot-dependencies/ ./
COPY – from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
- 生成Docker File
FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM openjdk:11-jre-slim
WORKDIR application
COPY – from=builder application/dependencies/ ./
COPY – from=builder application/spring-boot-loader/ ./
COPY – from=builder application/snapshot-dependencies/ ./
COPY – from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
- 构建日志
Sending build context to Docker daemon 41.87MB
Step 1/12 : FROM openjdk:11-jre-slim as builder
– -> 973c18dbf567
Step 2/12 : WORKDIR application
– -> Using cache
– -> b6b89995bd66
Step 3/12 : ARG JAR_FILE=target/*.jar
– -> Using cache
– -> 2065a4ad00d4
Step 4/12 : COPY ${JAR_FILE} application.jar
– -> c107bce376f9
Step 5/12 : RUN java -Djarmode=layertools -jar application.jar extract
– -> Running in 7a6dfd889b0e
Removing intermediate container 7a6dfd889b0e
– -> edb00225ad75
Step 6/12 : FROM openjdk:11-jre-slim
– -> 973c18dbf567
Step 7/12 : WORKDIR application
– -> Using cache
– -> b6b89995bd66
Step 8/12 : COPY – from=builder application/dependencies/ ./
– -> Using cache
– -> c9a01ed348a9
Step 9/12 : COPY – from=builder application/spring-boot-loader/ ./
– -> Using cache
– -> e3861c690a96
Step 10/12 : COPY – from=builder application/snapshot-dependencies/ ./
– -> Using cache
– -> f928837acc47
Step 11/12 : COPY – from=builder application/application/ ./
– -> 3a5f60a9b204
Step 12/12 : ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
– -> Running in f1eb4befc4e0
Removing intermediate container f1eb4befc4e0
– -> 8575cc3ac2e3
Successfully built 8575cc3ac2e3
Successfully tagged svc1:latest
- 观察“分层镜像”
// really useing cache ?
docker history svc1
IMAGE CREATED CREATED BY SIZE
8575cc3ac2e3 About a minute ago /bin/sh -c #(nop) ENTRYPOINT ["java" "org.s… 0B
3a5f60a9b204 About a minute ago /bin/sh -c #(nop) COPY dir:0cea19e682012ea7b… 54.1kB
f928837acc47 4 hours ago /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba… 0B
e3861c690a96 4 hours ago /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8… 224kB
c9a01ed348a9 4 hours ago /bin/sh -c #(nop) COPY dir:124320f4334c6319e… 41.5MB
b6b89995bd66 5 hours ago /bin/sh -c #(nop) WORKDIR /application 0B
// change source codes
IMAGE CREATED CREATED BY SIZE
b328f4d5f61a 6 seconds ago /bin/sh -c #(nop) ENTRYPOINT ["java" "org.s… 0B
aca4b7a5f92a 7 seconds ago /bin/sh -c #(nop) COPY dir:7a586cf8680e2bd04… 55.7kB
f928837acc47 4 hours ago /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba… 0B
e3861c690a96 4 hours ago /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8… 224kB
c9a01ed348a9 4 hours ago /bin/sh -c #(nop) COPY dir:124320f4334c6319e… 41.5MB
b6b89995bd66 5 hours ago /bin/sh -c #(nop) WORKDIR /application 0B
- 仅有application层重新构建 54.1KB -> 55.7KB
- 使用了宿主机“可复用层”,即缓存“层”
- 镜像优化
- 通过开启“分层”构建Spring Boot 应用,可以明显感到推送镜像速度快了很多
- 从远程拉去镜像,也只需要拉取变化的“层”,速度明显更快
- 每次部署,可以节省更多的空间
- 对比
- 解决了什么问题?
- “可复用”的镜像“层”
- 一次构建,缓存在宿主机,重复使用
- 没有解决什么?
- 代码的编译
- 优化总结
- 分层镜像构建总览
- 分层构建的优点
- 减少了因业务代码变更带来的全量构建所需时间,提高了业务上线的效率
- 减少了构建时所需的带宽、磁盘空间,提高了宿主机缓存的利用率
- 提高了镜像仓库推送、拉取的效率
- 提高了CI/CD的性能和效率
- 业务试点
- bill-scheduler 配置Maven,package生成layers.idx
- 编译时间 - Total time: 42.382 s
- 分层Jar包
- 提取分层Jar包
- 创建Dockerfile
FROM openjdk:8-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} antispam-bill-scheduler-2.20230202.67960-SNAPSHOT.jar
RUN java -Djarmode=layertools -jar antispam-bill-scheduler-2.20230202.67960-SNAPSHOT.jar extract
FROM openjdk:8-jre-slim
WORKDIR application
COPY – from=builder application/dependencies/ ./
COPY – from=builder application/spring-boot-loader/ ./
COPY – from=builder application/snapshot-dependencies/ ./
COPY – from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
- 镜像构建
- 耗时30s左右 - 31.19s
- 代码变动
- 耗时 - 5s
- 镜像变更记录
- 节省空间: 300M
- 传统镜像构建
- 耗时: 15.8s
- 性能提升
- 镜像拉取
- 优化前
- 优化后
- 性能提升20s左右
- 镜像推送
- 优化前
- 优化后