Spring Boot 重用 Docker 层

1、简介

Docker 是创建独立应用的事实标准。从 2.3.0 版开始,Spring Boot 包含了多项增强功能,可帮助我们创建高效的 Docker 镜像。例如:它允许将应用分解成不同的层。

换句话说,源代码位于自己的层中。因此,它可以独立重建,从而提高效率并缩短启动时间。本文将带你了解如何利用 Spring Boot 重用 Docker 层。

2、Docker 中的分层 jar

Docker 容器由基础镜像和额外的层组成。一旦层构建完成,它们将保持缓存状态。因此,后续的生成速度会更快:

docker 的层

对底层层级的更改也会重新构建上层层级。因此,不经常更改的层级应保持在底部,而经常更改的层级应放在顶部。

同样,Spring Boot 允许将工件(构建产物)内容映射到层中。默认的层映射如下:

spring boot 分层

你可以看到,应用有自己的层。修改源代码时,只会重新构建独立的层。loader 和依赖保持缓存,从而减少了 Docker 镜像的创建和启动时间。接下来看看如何使用 Spring Boot 实现这一点!

3、Spring Boot 创建高效的 Docker 镜像

在传统的构建 Docker 镜像的方式中,Spring Boot使用的是 “fat jar” 方法。一个工件就包含了所有依赖项和应用源代码。因此,源代码的任何变化都会迫使我们重建整个层。

3.1、Spring Boot 分层配置

Spring Boot 2.3.0 版引入了两个新功能来改进 Docker 镜像的生成:

  • Buildpack 支持提供了应用的 Java 运行时环境,因此现在可以跳过 Dockerfile,并自动构建 Docker 镜像。
  • 分层 JAR 可以帮助我们最大限度地利用 Docker 层的生成

在本文中,我们将对分层 JAR 方法进行扩展。

首先,在 Maven 中设置分层 JAR 。在打包工件时,生成层。

们检查一下JAR文件:

jar tf target/spring-boot-docker-0.0.1-SNAPSHOT.jar

你可以看到,在 fat jar 内的 BOOT-INF 文件夹中创建了新的 layers.idx 文件。当然,它将依赖、资源和应用源代码映射到独立的层:

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/"

3.2、与层互动

列出工件内部的层:

java -Djarmode=layertools -jar target/docker-spring-boot-0.0.1.jar list

结果是 layers.idx 文件内容的简单视图:

dependencies
spring-boot-loader
snapshot-dependencies
application

还可以将层提取到文件夹中:

java -Djarmode=layertools -jar target/docker-spring-boot-0.0.1.jar extract

接下来,可以在 Dockerfile 中重用文件夹:

$ ls
application/
snapshot-dependencies/
dependencies/
spring-boot-loader/

3.3、Dockerfile 配置

要充分利用 Docker 的功能,需要在镜像中添加层。

首先,将 Fat Jar 添加到基础镜像中:

FROM adoptopenjdk:11-jre-hotspot as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar

其次,提取工件的层:

RUN java -Djarmode=layertools -jar application.jar extract

最后,复制提取的文件夹,添加相应的 Docker 层:

FROM adoptopenjdk:11-jre-hotspot
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

通过这种配置,当更改源代码时,只会重建应用层。其他部分将保持缓存状态。

4、自定义层

看起来一切都运行得很顺利。但是,如果仔细观察,就会发现依赖层并没有在构建之间共享。也就是说,它们都在一个单独的层级中,甚至包括内部依赖。因此,如果我们更改了内部库的类,将会重新构建所有的依赖层级。

4.1、Spring Boot 自定义层配置

在 Spring Boot 中,可以通过单独的配置文件来调整自定义层:

<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-2.3.xsd">
    <application>
        <into layer="spring-boot-loader">
            <include>org/springframework/boot/loader/**</include>
        </into>
        <into layer="application" />
    </application>
    <dependencies>
        <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>

如上,将依赖和资源映射并排序到层中。此外,还可以添加任意数量的自定义层。

将文件命名为 layers.xml。然后,在 Maven 中,配置该文件以自定义层:

<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>

打包工件,结果类似于默认行为

4.2、添加新的层

创建一个内部依赖(internal-dependencies),添加应用类:

<into layer="internal-dependencies">
    <include>com.baeldung.docker:*:*</include>
</into>

排序新的层:

<layerOrder>
    <layer>internal-dependencies</layer>
</layerOrder>

如果列出 Fat Jar 内的层,就会出现新的 internal-dependencies

dependencies
spring-boot-loader
internal-dependencies
snapshot-dependencies
application

4.3、Dockerfile 配置

提取后,就可以将新的 internal 层添加到 Docker 镜像中:

COPY --from=builder internal-dependencies/ ./

如果生成镜像,就会看到 Docker 是如何将 internal-dependencies 构建为一个新层的:

$ mvn package
$ docker build -f src/main/docker/Dockerfile . --tag spring-docker-demo
....
Step 8/11 : COPY --from=builder internal-dependencies/ ./
 ---> 0e138e074118
.....

然后,就可以在历史记录中查看 Docker 镜像中各层的组成:

$ docker history --format "{{.ID}} {{.CreatedBy}} {{.Size}}" spring-docker-demo
c0d77f6af917 /bin/sh -c #(nop)  ENTRYPOINT ["java" "org.s… 0B
762598a32eb7 /bin/sh -c #(nop) COPY dir:a87b8823d5125bcc4… 7.42kB
80a00930350f /bin/sh -c #(nop) COPY dir:3875f37b8a0ed7494… 0B
0e138e074118 /bin/sh -c #(nop) COPY dir:db6f791338cb4f209… 2.35kB
e079ad66e67b /bin/sh -c #(nop) COPY dir:92a8a991992e9a488… 235kB
77a9401bd813 /bin/sh -c #(nop) COPY dir:f0bcb2a510eef53a7… 16.4MB
2eb37d403188 /bin/sh -c #(nop)  ENV JAVA_HOME=/opt/java/o… 0B

可以看到,该层现在包含了项目的 internal dependencies。

5、总结

本文介绍了如何使用 Spring Boot 创建分层 Jar,以高效地生成 Docker 镜像。


Ref:https://www.baeldung.com/docker-layers-spring-boot