背景

Spring Boot 2.3(截至目前版本为 M3)为容器化部署提供了一个新特性 Layered Jar。通常 Spring Boot 程序都是以 fat jar 的方式构建的,文件大小动辄 50M、100M 这样子,对 docker image 其实很不友好。Docker image 本身是分层结构,如果某一层没有变化在 pull 时就不必上传,一旦有变化就要上传整层。一个程序中,程序自身代码、资源的变更频率要远大于依赖库的变更频率,大多数时候因为几行代码变化导致上传整个 jar 文件,无论是存储占用还是时间效率上都是很大的浪费,后者在国内网速下更是一个严重问题。

Layered Jar

新特性 layered jar 为不同变更频率内容分离提供了支持工具,在此基础上分层构建 docker image 就变得很容易了。本质上这个特性是 org.springframework.boot:spring-boot-maven-plugin 提供的一种新的 layout,当使用新 layout 打包时,一个 spring-boot-layertools jar 会打包到 fat jar 中,新特性是由这个 jar 提供的。这里不深入解析实现细节,而是重点关注如何模式化使用这个特性获得收益。

要使用这个新特性需要做的事情非常少,首先是在 pom 中增加 layout 配置,我猜未来这个选项会成为默认值,从而不需任何显式配置。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <layout>LAYERED_JAR</layout>
      </configuration>
    </plugin>
    ...
  </plugins>
  ...
</build>

新增的部分就是这 3 行

<configuration>
  <layout>LAYERED_JAR</layout>
</configuration>

增加配置之后 package 打包,然后可以执行以下命令验证配置正确与否

java -Djarmode=layertools -jar <target.jar>

一个示例输出:

Usage:
  java -Djarmode=layertools -jar metis-server-0.1.0-SNAPSHOT.jar

Available commands:
  list     List layers from the jar that can be extracted
  extract  Extracts layers from the jar for image creation
  help     Help about any command

这是 maven 上所有需要做的事情,gradle 参照 maven 也可以类似处理。然后需要在 Dockerfile 中再做一点处理,这用到了 docker 的多阶段构建功能(17.05 增加的功能,几乎 3 年前,希望没人说这版本太新~)。一个示例 Dockerfile

FROM azul/zulu-openjdk:13 as builder                                #01
                                                                    #02
WORKDIR application                                                 #03
COPY /maven/${project.build.finalName}.jar application.jar          #04
RUN java -Djarmode=layertools -jar application.jar extract          #05
                                                                    #06
FROM azul/zulu-openjdk:13                                           #07
                                                                    #08
WORKDIR /opt/bin/                                                   #09
USER 1000:1000                                                      #10
EXPOSE 8080                                                         #11
                                                                    #12
COPY --from=builder application/dependencies/ ./                    #13
COPY --from=builder application/snapshot-dependencies/ ./           #14
COPY --from=builder application/resources/ ./                       #15
COPY --from=builder application/application/ ./                     #16
                                                                    #17
ENV TZ=Asia/Shanghai                                                #18
                                                                    #19
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]  #20

对上面的 Dockerfile 逐行解释一下:

  1. #01#07 2 个 FROM 保持统一为运行时使用的基础镜像即可;
  2. #04 处 COPY 的源根据使用的 docker 插件不同会有所变化,本文使用的插件为 io.fabric8:docker-maven-plugin:0.33.0。目的文件保持 application.jar 即可,它要跟 #05 的 jar 文件名一致;
  3. #05 如果报错需要检查是否遗漏了参数,比如 #20 需要 --enable-preview 的话 #05 也需要加 --enable-preview
  4. #09#11#18 跟本文无关,但是算是 Spring Boot 容器化的良好实践,可以照抄,日后有机会专文再讲;
  5. #10 也是良好实践——非 root 用户运行,如果遇到相关问题可以删除此行;
  6. #13#14#15#16#20 4 个 COPY 1 个 ENTRYPOINT 照抄即可,这是本文新特性的关键。

然后,用这个 Dockerfile 构建出来的镜像内容已经是拆解分成多层的镜像了,当变更源码、资源、快照版依赖、正式版依赖时会依次影响更多层镜像,从而实现每次构建上传仓库时存储和传输耗时最小化。

总结

无论从易用程度还是达到的效果上讲,layered jar 都非常值得使用。要使用该特性只需要开启 pom 中一个配置项并使用一个几乎不需要任何修改的 Dockerfile 模板,几乎零负担获得可观的收益。

与本文所涉及内容有所关联的另外 2 篇帖子可供参考
[每日短篇] 23 – 动态给容器指定 Java 启动参数
[每日短篇] 0 – Linux 的 timezone 设置

最后把不带行号的 Dockerfile 再重复一次以方便复制

FROM azul/zulu-openjdk:13 as builder

WORKDIR application
COPY /maven/${project.build.finalName}.jar application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM azul/zulu-openjdk:13

WORKDIR /opt/bin/
USER 1000:1000
EXPOSE 8080

COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/resources/ ./
COPY --from=builder application/application/ ./

ENV TZ=Asia/Shanghai

ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]