为 Spring Boot 应用程序构建优化的 Docker 镜像

容器已成为打包应用程序及其所有软件和操作系统依赖项,然后将它们交付到不同环境的首选方式。

本文介绍了容器化 Spring Boot 应用程序的不同方法:

  • 使用 Docker 文件创建 Docker 映像,
  • 使用 Cloud-Native Buildpack 从源创建 OCI 映像,
  • 通过使用多层工具将 JAR 的各个部分分成不同的层来优化运行时图像。

 代码示例

本文附有工作代码示例 在GitHub上 .

容器术语

我们将从本文中使用的容器术语开始:

  • 容器镜像: 特定格式的文件。 我们将通过运行构建工具将应用程序转换为容器映像。
  • 容器:容器镜像的可执行实例。
  • 容器引擎:负责运行容器的守护进程。
  • 容器主机:容器引擎运行的主机。
  • 容器注册中心:用于发布和分发容器镜像的一般位置。
  • OCI标准开放式容器倡议(OCI) 是 Linux 基金会内部形成的一个轻量级、开放的治理结构。 OCI 镜像规范定义了容器镜像和运行时格式的行业标准,以确保所有容器引擎都可以运行任何构建工具创建的容器镜像。

为了容器化应用程序,我们将应用程序包装在容器映像中,并将该映像发布到共享注册表。 容器运行时从注册表中检索该映像,将其解压,然后在其中运行应用程序。

Spring Boot 2.3 版本提供了用于创建 OCI 镜像的插件。

码头工人 是最常用的容器实现,并且我们在示例中使用 Docker,因此本文后续所有容器引用都将参考 Docker。

以传统方式构建容器镜像

通过向 Docker 文件添加一些指令,为 Spring Boot 应用程序创建 Docker 映像非常简单。

我们首先创建一个可执行 JAR 文件,并作为 Docker 文件指令的一部分,在应用必要的设置后将可执行 JAR 文件复制到基础 JRE 映像之上。

让我们创建 Spring 应用程序 春季初始化 有依赖关系 weblombokи actuator。 我们还添加了一个休息控制器来提供 API GET方法。

创建 Dockerfile

然后,我们通过添加来容器化该应用程序 Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]

我们的 Docker 文件包含来自 adoptopenjdk,在其上复制 JAR 文件,然后打开端口, 8080它将监听请求。

构建应用程序

首先,您需要使用 Maven 或 Gradle 创建应用程序。 这里我们使用Maven:

mvn clean package

这将为应用程序创建一个可执行 JAR 文件。 我们需要将这个可执行 JAR 转换为 Docker 镜像以在 Docker 引擎上运行。

创建容器镜像

然后,我们通过运行命令将此可执行 JAR 文件放入 Docker 映像中 docker build从包含之前创建的 Dockerfile 的项目根目录:

docker build  -t usersignup:v1 .

我们可以使用以下命令在列表中查看我们的图像:

docker images 

上述命令的输出包括我们的图像 usersignup与基础图像一起, adoptopenjdk在我们的 Docker 文件中指定。

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

查看容器镜像内的层

让我们看看图像内的图层堆栈。 我们将使用 工具  潜水, 查看这些层:

dive usersignup:v1

以下是 Dive 命令的部分输出: 

为 Spring Boot 应用程序构建优化的 Docker 镜像

正如我们所看到的,应用程序层占据了图像大小的很大一部分。 作为优化的一部分,我们希望在以下部分中减小该层的大小。

使用 Buildpack 创建容器镜像

组装包 (构建包)是各种平台即服务 (PAAS) 产品使用的通用术语,用于从源代码创建容器映像。 它由 Heroku 于 2011 年推出,此后已被 Cloud Foundry、Google App Engine、Gitlab、Knative 等多个公司采用。

为 Spring Boot 应用程序构建优化的 Docker 镜像

云构建包的优势

使用 Buildpack 创建映像的主要好处之一是 图像配置更改可以集中管理(构建器)并使用构建器传播到所有应用程序。

构建包与平台紧密耦合。 Cloud-Native Buildpacks 通过支持 OCI 镜像格式提供跨平台标准化,确保镜像可以由 Docker 引擎运行。

使用 Spring Boot 插件

Spring Boot 插件使用 Buildpack 从源代码构建 OCI 映像。 图像是使用创建的 bootBuildImage任务(Gradle)或 spring-boot:build-image目标 (Maven) 和本地 Docker 安装。

我们可以通过指定名称来自定义推送到 Docker 注册表所需的镜像的名称 image tag:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <image>
      <name>docker.io/pratikdas/${project.artifactId}:v1</name>
    </image>
  </configuration>
</plugin>

让我们使用 Maven 来做这件事 build-image创建应用程序和创建容器映像的目标。 我们目前没有使用任何 Dockerfile。

mvn spring-boot:build-image

结果将是这样的:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO] 
[INFO]  > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Adding label 'org.springframework.boot.version'
.. [creator]     *** Images (c311fe74ec73):
.. [creator]           docker.io/pratikdas/usersignup:v1
[INFO] 
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'

从输出中我们看到 paketo Cloud-Native buildpack用于创建工作 OCI 映像。 和之前一样,我们可以通过运行以下命令来查看列为 Docker 镜像的镜像:

docker images 

结论:

REPOSITORY                             SIZE
paketobuildpacks/run                  84.3MB
gcr.io/paketo-buildpacks/builder      652MB
pratikdas/usersignup                  257MB

使用 Jib 创建容器镜像

Jib 是 Google 的一个镜像创建插件,它提供了另一种从源代码创建容器镜像的方法。

配置中 jib-maven-plugin在 pom.xml 中:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.5.2</version>
      </plugin>

接下来,我们使用 Maven 命令运行 Jib 插件来构建应用程序并创建容器映像。 和以前一样,我们在这里没有使用任何 Docker 文件:

mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1

执行上述 Maven 命令后,我们得到以下输出:

[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete

输出显示容器映像已创建并放置在注册表中。

创建优化图像的动机和技术

我们进行优化的主要原因有两个:

  • Производительность:在容器编排系统中,容器映像从映像注册表检索到运行容器引擎的主机。 这个过程称为规划。 从注册表中提取大型映像会导致容器编排系统中的调度时间较长以及 CI 管道中的构建时间较长。
  • 安全:较大的图像也有较大的漏洞区域。

Docker 镜像由一堆层组成,每个层代表 Dockerfile 中的一条指令。 每层代表底层变化的增量。 当我们从注册表中拉取 Docker 映像时,它会分层拉取并缓存在主机上。

Spring Boot 使用 “胖罐子”中 作为默认的打包格式。 当我们查看厚厚的 JAR 时,我们会发现该应用程序只占整个 JAR 的很小一部分。 这是变化最频繁的部分。 其余部分由 Spring 框架依赖项组成。

优化公式的重点是在单独的级别将应用程序与 Spring 框架依赖项隔离。

依赖层构成了厚 JAR 文件的大部分,仅下载一次并缓存在主机系统上。

在应用程序更新和容器调度期间,仅拉取应用程序的薄层。 如下图所示:

为 Spring Boot 应用程序构建优化的 Docker 镜像

在以下部分中,我们将了解如何为 Spring Boot 应用程序创建这些优化的映像。

使用 Buildpack 为 Spring Boot 应用程序创建优化的容器映像

Spring Boot 2.3 通过将厚 JAR 文件的各个部分提取到单独的层中来支持分层。 分层功能默认处于禁用状态,必须使用 Spring Boot Maven 插件显式启用:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <layers>
      <enabled>true</enabled>
    </layers>
  </configuration> 
</plugin>

我们将使用此配置首先使用 Buildpack 构建容器映像,然后在以下部分中使用 Docker 构建容器映像。

让我们开始吧 build-image用于创建容器镜像的 Maven 目标:

mvn spring-boot:build-image

如果我们运行 Dive 来查看生成图像中的各层,我们可以看到与使用 fat JAR 格式得到的内容相比,应用程序层(以红色框出)在千字节范围内要小得多:

为 Spring Boot 应用程序构建优化的 Docker 镜像

使用 Docker 为 Spring Boot 应用程序创建优化的容器映像

我们还可以使用 Docker 文件创建分层的 Docker JAR 映像,而不是使用 Maven 或 Gradle 插件。

当我们使用 Docker 时,我们需要执行两个额外的步骤来提取层并将它们复制到最终镜像中。

使用启用了分层的 Maven 构建后生成的 JAR 内容将如下所示:

META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx

输出显示了一个名为 spring-boot-jarmode-layertoolsи layersfle.idx文件。 此附加 JAR 文件提供分层处理功能,如下一节所述。

提取各个层的依赖关系

要从分层 JAR 中查看和提取层,我们使用系统属性 -Djarmode=layertools跑 spring-boot-jarmode-layertoolsJAR 而不是应用程序:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

运行此命令会生成包含可用命令选项的输出:

Usage:
  java -Djarmode=layertools -jar usersignup-0.0.1-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

输出显示命令 listextractи helpс help设为默认值。 让我们运行命令 list选项:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

我们看到可以作为层添加的依赖项列表。

默认图层:

图层名称

内容

dependencies

版本不包含 SNAPSHOT 的任何依赖项

spring-boot-loader

JAR 加载器类

snapshot-dependencies

版本包含 SNAPSHOT 的任何依赖项

application

应用程序类和资源

层定义于 layers.idx文件按照应添加到 Docker 映像的顺序排列。 这些图层在第一次检索后会缓存在主机中,因为它们不会更改。 仅将更新的应用层下载到主机,由于尺寸减小,速度更快 .

构建一个图像,将依赖项提取到不同的层中

我们将使用名为的方法分两个阶段构建最终图像 多阶段组装 。 在第一步中,我们将提取依赖项,在第二步中,我们将提取的依赖项复制到最终映像中。

让我们修改 Dockerfile 以进行多阶段构建:

# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
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"]

我们将此配置保存在一个单独的文件中 - Dockerfile2.

我们使用以下命令构建 Docker 镜像:

docker build -f Dockerfile2 -t usersignup:v1 .

运行此命令后,我们得到以下输出:

Sending build context to Docker daemon  20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1

我们可以看到,Docker 镜像是使用镜像 ID 创建的,然后被标记的。

最后,我们像以前一样运行 Dive 命令来检查生成的 Docker 镜像内的各层。 我们可以提供图像 ID 或标签作为 Dive 命令的输入:

dive userssignup:v1

正如您在输出中看到的,包含应用程序的层现在只有 11 KB,并且依赖项缓存在单独的层中。 

为 Spring Boot 应用程序构建优化的 Docker 镜像

提取各个层的内部依赖关系

我们可以通过将任何自定义依赖项提取到单独的层中来进一步减小应用程序层的大小,而不是通过在 yml类似的文件名为 layers.idx:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "custom-dependencies":
  - "io/myorg/"
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

在这个文件中 layers.idx我们添加了一个名为的自定义依赖项, io.myorg包含从共享存储库检索的组织依赖项。

结论

在本文中,我们研究了使用云原生 Buildpack 直接从源代码构建容器映像。 这是使用 Docker 创建容器镜像的通常方式的替代方法:首先创建一个厚的可执行 JAR 文件,然后通过在 Docker 文件中指定指令将其打包到容器镜像中。

我们还考虑通过启用分层功能来优化容器,该功能将依赖项拉入缓存在主机上的单独层中,并在容器执行引擎中的调度时加载应用程序的薄层。

您可以在以下位置找到本文中使用的所有源代码 Github上 .

命令参考

以下是我们在本文中使用的命令的快速概述。

上下文清除:

docker system prune -a

使用 Docker 文件创建容器镜像:

docker build -f <Docker file name> -t <tag> .

我们从源代码构建容器镜像(没有 Dockerfile):

mvn spring-boot:build-image

查看依赖层。 在构建应用程序 JAR 文件之前,请确保在 spring-boot-maven-plugin 中启用了分层功能:

java -Djarmode=layertools -jar application.jar list

提取依赖层。 在构建应用程序 JAR 文件之前,请确保在 spring-boot-maven-plugin 中启用了分层功能:

 java -Djarmode=layertools -jar application.jar extract

查看容器镜像列表

docker images

查看左侧容器镜像内(确保已安装dive工具):

dive <image ID or image tag>

来源: habr.com