Java编程细节——Docker 优化 Spring Boot 应用程序

Docker功能强大且易于使用。它允许开发者为他们创建的软件创建可移植的、自包含的镜像。这些镜像能被可靠且可重复的部署。你可以轻松的从 Docker 检索值,但为了充分利用 Docker,你需要理解一些重要的概念。当你惊醒持续集成和持续交付时,如何构建 Docker 镜像会产生显著的影响。在本文中,我将重点介绍在进行迭×××发和部署时如何更有效的为 Spring Boot 应用程序构建 Docker 镜像。标准方法有一些缺点,所以在这里我们看看它们是什么以及如何做的更好。

Docker 关键概念

Docker 有四个关键概念在起作用:image、layer、Dockerfile 和 Docker 缓存。简单来说,Dockerfile 面熟如何构建 Docker image。一个 image 由数个 layer 组成。Dockerfile 以基础镜像开始并添加其他 layer。将新的内容添加到 image 时会生成新的 layer。构建的每个 layer 都被缓存了,因此可以在后续构建中重复使用。当 Docker 构建运行时,它可以从缓存中重用任何已存在的 layer。这减少了每次构建所需的总时间和空间。任何已更改或之前未被构建的内容都将根据需要构建。

img

Layer 内容的重要性

这就是 layer 的重要性发挥作用的地方。只有当 layer 的内容未改变时,Docker 缓存中已存在的 layer 才能被使用。Docker 构建间更改的 layer 越多,Docker 重建 image 所需的工作就越多。layer 的顺序也很重要。如果所有的父 layer 没有改变才能重用 layer。最好在将频繁改变的 layer 放置在后面,以便于对它们进行更改影响更少的子 layer。

layer 的顺序和内容很重要。当你将你的应用程序打包为 Docker image 时,最简单的方法是将整个应用程序放进一个单独的 layer。但是,如果应用程序包含大量静态库依赖,当你改动最少量的代码时,整个 layer 需要被重建。这最终会在 Docker 缓存中浪费大量构建时间和空间。

Layer 对部署的影响

部署 Docker 镜像时,layer 也很重要。部署 Docker image 前。它们将被推送到远程 Docker 存储库。改存储库充当所有部署 image 的源,并且经常包含同一镜像的许多版本。Docker 非常高效,每个 layer 只保存一次。但是,对于经常部署且有大量不断重新构建的 layer 的 image,此能力无法工作。大量 layer,即使其内部的变化很小,也必须单独存储在存储库中并通过网络推送。这会对部署时间产生负面影响,因为需要为没有改变的部分移动和存储完全一样的位(bits)。

Docker 中的 Spring Boot 应用程序

使用超级 jar 方式的 Spring Boot 应用程序都是独立的部署单元。此模型非常适合在虚拟机或在构建包上部署,因为应用程序拥有它需要的一切。但是,这是 Docker 部署的一个劣势:Docker 已经提供了打包依赖的方法。将整个 Spring Boot JAR 放进 Docker image 是非常常见的。但是,这导致 Docker image 的应用程序 layer 中有太多不变的位(bits)。

img

Spring 社区正在讨论如何在运行 Spring Boot 应用程序时减少部署大小和时间,特别是在 Docker 中(参考:1234)。在我看来,这最终是在简单性和效率之间进行权衡。为 Spring Boot 应用程序构建 Docker image 最常用的方法是我称之为“单独 layer”的方法。这在技术上并不正确,因为实际上 Dockerfile 创建了多个 layer,但它足以满足本次讨论的目的。

单独 layer 方法

让我们来看看单独 layer 方法。单独 layer 方法快速、直接、易于理解和使用。针对 Docker 的 Spring Boot 指南列出了构建你的 Docker 镜像的 Dockerfile:

FROM openjdk:8-jdk-alpine VOLUME /tmp ARG JAR_FILE COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

最终结果是一个正常工作的 Docker image,其运行方式和你期望的 Spring Boot 应用程序的方式完全相同。但是,它受到分层效率问题的困扰,因为它基于整个应用程序 JAR。随着应用程序源的更改,整个 Spring Boot JAR 会被重建。下一次构建 Docker image 时,整个应用程序 layer 会被重建,包括所有未更改的库依赖。

更深入的查看单独 layer 方法

单独 layer 方法使用 Spring Boot JAR 构建 Docker image,Open JDK 基础 image 之上的 Docker layer 如下所示:

$ docker images REPOSITORY                    TAG         IMAGE ID            CREATED             SIZE springio/spring-petclinic     latest      94b0366d5ba2        16 seconds ago      140MB

生成的 Docker image 为 140 MB。你可以使用 docker history 命令检查 layer。你可以看到 Spring Boot 应用程序 JAR 已复制到大小为 38.3 MB 的 image 中。

$ docker history springio/spring-petclinic IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT 94b0366d5ba2        52 seconds ago      /bin/sh -c #(nop)  ENTRYPOINT ["java" "-Djav...   0B 213dff56a4bd        53 seconds ago      /bin/sh -c #(nop) COPY file:d3551559c2aa35af...   38.3MB bc453a32748e        6 minutes ago       /bin/sh -c #(nop)  ARG JAR_FILE                 0B 7fe0bb0d8026        6 minutes ago       /bin/sh -c #(nop)  VOLUME [/tmp]                0B cc2179b8f042        8 days ago          /bin/sh -c set -x  && apk add --no-cache   o...   97.4MB <missing>           8 days ago          /bin/sh -c #(nop)  ENV JAVA_ALPINE_VERSION=8...   0B <missing>           8 days ago          /bin/sh -c #(nop)  ENV JAVA_VERSION=8u151       0B <missing>           8 days ago          /bin/sh -c #(nop)  ENV PATH=/usr/local/sbin:...   0B <missing>           8 days ago          /bin/sh -c #(nop)  ENV JAVA_HOME=/usr/lib/jv...   0B <missing>           8 days ago          /bin/sh -c {   echo '#!/bin/sh';   echo 'set...   87B <missing>           8 days ago          /bin/sh -c #(nop)  ENV.UTF-8             0B <missing>           5 months ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B <missing>           5 months ago        /bin/sh -c #(nop) ADD file:093f0723fa46f6cdb...   4.15MB

下次构建 Docker image 时,将重建整个 38 MB layer,因为 JAR 文件已重新打包。

在此示例中,应用程序大小相对较小,仅基于 spring-boot-starter-web 和其他如 spring-actuator 的依赖。实际上,这些大小通常更大一些,因为它们不仅仅引入 Spring Boot 库,还包括其他第三方库。根据我的经验,实际的 Spring Boot 应用程序的大小范围可以从 50 MB 到 250 MB,如果不更大。

仔细观察应用程序,应用程序 JAR 只有 372 KB 时应用程序代码。剩余的 38 MB 是库依赖。这意味着只有 0.1% 的 layer 实际改变。剩余的 99.9% 未改变。

layer 生命周期

这里说明分层的基本考虑:内容的生命周期。layer 的内容应该有相同的生命周期。Spring Boot 应用程序的内容有两种不同的生命周期:不经常改动的库依赖和经常改动的应用程序的类(class)。

每次应用程序的代码改动导致 layer 重建时,都包括未改变的二进制文件。在应用程序代码不断变化和重新部署的快速应用程序开发环境中,这种附加成本会变得非常昂贵。

设想一个应用程序团队在 Pet Clinic 上进行迭代。团队每天更改并重新部署应用程序 10 次。这 10 个新 layer 的成本将为 383 MB,每天。使用更多的实际尺寸,每天可达 2.5 GB 甚至更多。这最终会浪费构建时间、部署时间和 Docker 存储库空间。

这种快速、渐进的开发和交付时在权衡变得重要的时候。你必须继续使用简单的单独 layer 方法或采用更有效的替代方法。

拥抱 Docker,走向双 layer

在简单和效率之间的权衡中,我觉得正确的选择是“双 layer”方法(更多 layer 是可以的,但是太多 layer 可能是有害的,并且违反了 Docker 最佳实践)。在双 layer 方法中,我们构建 Docker image 以便于 Spring Boot 应用程序的库依赖存在于应用程序代码下面的 layer 中。这样,layer 遵循内容的不同生命周期。通过将不经常更改的库依赖推送到分离的 layer 并将将应用程序类保留在最上面的 layer,迭代重建和重新部署将更快。

img

双 layer 方法加速迭×××发构建,并最小化部署时间。结果因应用程序而异,但平均而言,这会减少 90% 的应用程序部署大小,并相应的减少部署周期时间。

喜欢的点点关注点点赞呗!

猜你喜欢

转载自blog.51cto.com/13941961/2287647