(给ImportNew加星标,提高Java技能)

编译:ImportNew/唐尤华

phauer.com/2019/no-fat-jar-in-docker-image/

在 Docker 容器中存放 fat jar 是一种对存储空间、带宽和时间的浪费。幸运的是,可以利用 Docker 镜像分层和 registry cache 实现增量构建和小型 artifact。例如,可以把新建 artifact 的大小从75MB缩减到只有1MB!最好有一个 Maven 和 Gradle 插件可以把这些搞定。

jre11 docker镜像 jar docker镜像_Docker

摘要 

  • 通常,一个 fat jar 包含的所有依赖在不同的 release 之间不会改变。但是,这些依赖项会反复拷贝到每个 fat jar 中,浪费了存储空间、带宽和时间。
  • 例如,某个 Spring Boot 应用的 fat jar 大小为72 MB,其中只包含2MB代码。通常,只有代码会发生改变。
  • 幸运的是,可以利用 Docker 镜像分层技术:通过把依赖和资源放在不同的层级可以实现重用,并且实现只对 artifact、release 更新代码。
  • Jib插件可以方便地实现上述功能,支持 Maven 和 Gradle。无需手动编写 Dockerfile。

问题:Fat Jar中的依赖 

Docker 的分层机制功能非常强大。如果所有应用使用相同 base image(例如 openjdk:11.0.4-jre-slim),那么 Docker 会复用 OS 层和 JRE 层。在 Docker registry 中保存内容,由于传输的内容更少(Docker 只传输 registry 中新注册的 layer),可以加快 registry 上传和下载速度。

糟糕的是,许多应用并没有充分利用这种强大的机制,因为它们在 Docker 容器中使用了 fat jar。

jre11 docker镜像 jar docker镜像_jre11 docker镜像_02

每个 release 都会创建一个新的 Docker layer 大小72MB。

假设 Spring Boot 应用打包为一个 fat jar。这个 fat jar 大小为72MB,被添加到 Dockerfile 的最后一行。这意味着每个新 release 会占用72MB存储空间,并且会上传到 registry 然后下载。

现在,仔细看一下72MB的内容:

jre11 docker镜像 jar docker镜像_Docker_03

fat jar 的内容,其中大部分很少更改,但是会被反复拷贝到每个 artifact 中。

一个 fat jar 包含三部分:

  • 依赖项:大部分内容为 library 且很少变化。大多数情况下,创建 release 只修改代码,不涉及依赖项。尽管如此,依赖项仍然会被拷贝到每个 release 中。
  • 资源:基本上与上面的问题类似。尽管资源(像 HTML、CSS、图片、配置文件等)改变的频率比依赖项高,但是不会受到代码修改的影响。资源在每个 release 中也会重复。
  • 代码:虽然代码只占 fat jar 很小一部分(300KB - 2MB),但却是修改最频繁的部分。

因此,通常一个新的 release 代码修改只有几MB。每个 artiface 会重复拷贝所有资源和依赖项,这是对存储空间、带宽和时间的浪费。

如果为每个 commit 创建唯一、可部署的 artifact (使用 git commit hash 作为 artifact 的版本号),浪费会更加严重。对持续交付来说这种方式很有意义,但由于每次 commit 都会额外占用72MB内存,消耗的存储空间越来越大。

有哪些工具可以用来分析 docker 镜像,把 fat jar 在 docker 镜像中的影响可视化?Dive 和 docker history。

jre11 docker镜像 jar docker镜像_jre11 docker镜像_04

Dive是一个交互式命令行工具,可以显示是一个交互式命令行工具,可以展示 fat jar layer。

docker history 也可以展示 fat jar layer:

~ ❯❯❯ docker history registry.domain.com/neptune:latest
IMAGE           CREATED          CREATED BY                     SIZE
44e77fa110e5    2 minutes ago    /bin/sh -c #(nop) COPY dir:…65.5MB
...
8 months ago     /bin/sh -c set -ex;   if [ …   217MB
...8 months ago     /bin/sh -c #(nop) ADD file:…55.3MB

解决方案:为依赖、资源和代码使用不同 layer

所幸可以利用 Docker 的分层机制,类似操作系统和 JRE 层。为依赖、资源和代码引入不同的 layer 进行拓展,然后根据变化频率对 layer 进行排序。

jre11 docker镜像 jar docker镜像_jar_05

按照依赖、资源和代码把应用程序拆分成三个不同的 Docker layer。这样 release 只占2MB而不是之前的72MB。

现在,由于可以重复利用资源和依赖,如果创建的 release 只包含代码更改,只需要2MB存储空间。这些资源和依赖已经存储在 registry 中,不需要重复提交。

使用Google Jib插件实现 

好消息:不需要为 Java 应用程序手动编写 Dockerfile,使用 Google Jib 即可。Jib 插件让 Java 容器化更容易,支持 Maven 和 Gradle。这篇 Google 博客介绍了 Jib,其中最重要的一个特性:Jib 会扫描 Java 项目并为依赖、资源和代码创建不同的 layer。这种开箱即用的形式很棒。

实现步骤:

1)在 pom.xml 中添加插件依赖:

com.google.cloud.toolsjib-maven-plugin1.6.1openjdk:11.0.4-jre-slimdomain.com/${project.artifactId}:latest${git.commit.id}-serverbuild-and-push-docker-imagepackagebuild

2)用法

# 执行完整构建,然后把镜像推送到 registry
mvn package

# 只创建并推送镜像
mvn jib:build
# 注意 `jib:build` 没有守护进程,不会在机器上创建镜像
# 直接与 registry 交互使用 `docker pull` 获取已创建的镜像

# 只通过 Docker daemon 创建和推送镜像
mvn jib:dockerBuild

3)好处Dive和码docker history充分展示了优秀的分层结构。

jre11 docker镜像 jar docker镜像_jar_06

使用 Jib 构建的 docker 镜像中依赖、资源和代码三个不同的 layer

~ ❯❯❯ docker history registry.domain.com/neptune:latest
IMAGE          CREATED         CREATED BY                SIZE     COMMENT
a759771eb008   49 years ago    jib-maven-plugin:1.6.1    1.22MB   classes
49 years ago    jib-maven-plugin:1.6.1    297kB    resources49 years ago    jib-maven-plugin:1.6.1    64.6MB   dependencies
...8 months ago    /bin/sh -c set -ex; ...217MB
...8 months ago    /bin/sh -c #(nop) ADD...55.3MB

Clean-Up(可选) 

Clean-up 1)禁用maven-deploy-plugin、maven-install-plugin 和 maven-jar-plugin。不需要执行上述步骤,即使开发人员出于习惯执行 mvn deploy 也不应当执行。

org.apache.maven.pluginsmaven-deploy-plugintrue
org.apache.maven.pluginsmaven-install-plugintrue
org.apache.maven.pluginsmaven-jar-plugindefault-jarnone

Clean-up 2)如果使用 Spring Boot,需要移除 spring-boot-maven-plugin。不需要再创建一个 fat jar。

运行部署相关配置 

Jib 支持在 pom.xml 中配置 JVM flag 和程序参数,但是,通常我们不想在构建时设置,而是根据部署环境(本地、QA、生产)进行配置。可以在这里设置 Spring 配置和 JVM 堆大小。

  • JVM flag:使用 JAVA_TOOL_OPTIONS 环境变量添加 heap size 这样的 JVM flag。
  • Spring 配置:把部署相关的外部配置文件加载到 Docker 容器,文件的位置作为程序参数传入。也可以使用环境变量:
docker run -p 1309:1309 --net=host \
-e JAVA_TOOL_OPTIONS='-Xms1000M -Xmx1000M' \
-v /home/phauer/dev/app/app-config.yml:/app-config.yml \
registry.domain.com/app:latest \
--spring.config.additional-location=/app-config.yml