用单层镜像方法优化Docker中的Springboot

1. Docker关键概念

Docker有四个关键概念:images, layers, Dockerfile 和 Docker cache 。简而言之,Dockerfile描述了如何构建Docker镜像。镜像由许多层组成。Dockerfile从基础镜像开始,并添加了其他层。当新内容添加到镜像时,将生成一个新层。所构建的每个层都被缓存,因此可以在后续构建中重复使用。当Docker构建运行时,它可以从缓存中获取重复使用任何已有层。这就减少了每次构建所需的时间和空间。任何已更改或以前尚未构建的内容都将根据需要进行构建。

在这里插入图片描述

2. 镜像层内容很重要

镜像各层的重要性。Docker缓存中的现有层,只有当改镜像层内容没有变更时,才能被使用。在Docker构建期间更改的层越多,Docker需要执行更多的工作来重建镜像。镜像层顺序也很重要。如果某个图层的所有父图层均未更改,则该图层就能被重用。因此,最好把比较频繁更改的图层放在上面,以便对其更改会影响较少的子图层。镜像层的顺序和内容很重要。当你把应用程序打包为Docker镜像时,最简单的方法是将整个应用程序放置到一个单独的镜像层中。但是,如果该应用程序包含大量静态库依赖,那么即使更改很少的代码,也需要重新构建整个镜像层。这就需要在Docker缓存中,花费大量构建时间和空间。

3. 镜像层影响部署

部署Docker镜像时,镜像层也很重要。在部署Docker镜像之前,它们会被推送到Docker远程仓库。该仓库是所有部署镜像的源头,并且经常包含同一镜像的许多版本。Docker非常高效,每个层仅存储一次。但是,对于频繁部署且具有不断重建的大体积层的镜像,这就不行了。大体积层的镜像,即使内部只有很少的更改,也必须单独存储在仓库中并在网络中推送。因为需要移动并存储不变的内容,这就会增加部署时间。

4. Docker中的Springboot应用

使用uber-jar方法的Spring Boot应用程序本身就是独立的部署单元。该模型非常适合在虚拟机或构建包上进行部署,因为该应用程序可带来所需的一切。但是,这对Docker部署是一个缺点:Docker已经提供了打包依赖项的方法。将整个Spring Boot JAR放入Docker镜像是很常见的,但是,这会导致Docker镜像的应用程序层中的不变内容太多。
在这里插入图片描述
Spring社区中正在进行有关减少运行Springboot应用程序时的部署大小和时间的讨论,尤其是在Docker中。在我看来,这最终是在简单性与效率之间进行权衡。为Springboot应用程序构建Docker镜像的最常见方法是我所说的“单层”方法。从技术上讲,这不是正确的,因为Dockerfile实际上创建了多个层,但是对于讨论来说已经足够了。

5. 单层方法

让我们看一下单层方法。单层方法快速,简单,易于理解和使用。Docker的Spring Boot指南 列出了单层Dockerfile来构建你的Docker镜像:

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镜像,其运行方式与你期望Spring Boot应用程序运行的方式完全相同。但是,由于它基于整个应用程序JAR,因此存在分层效率问题。随着应用程序源的更改,整个Spring Boot JAR都会被重建。下次构建Docker镜像时,将重新构建整个应用程序层,包括所有不变的依赖库。让我们看一个具体的例子, Spring Pet Clinic。

6. 更深入地研究单层方法

单层方法使用Open Boot JDK基础镜像之上的Spring Boot JAR作为Docker层构建Docker镜像:

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

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

$ 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 
7fe0bb 0 d8         8 days ago          /bin/sh -c set -x  && apk add --no-cache   o…   97.4MB 
bc453a32748e        8 days ago          /bin/sh -c #(nop)  ENV JAVA_ALPINE_VERSION=8…   0B 
94b0366d5ba2        8 days ago          /bin/sh -c #(nop)  ENV JAVA_VERSION=8u151       0B 
bc453a32748e        8 days ago          /bin/sh -c #(nop)  ENV PATH=/usr/local/sbin:…   0B 
94b0366d5ba2        8 days ago          /bin/sh -c #(nop)  ENV JAVA_HOME=/usr/lib/jv…   0B 
bc453a32748e        8 days ago          /bin/sh -c {   echo '#!/bin/sh';   echo 'set…   87B 
94b0366d5ba2        8 days ago          /bin/sh -c #(nop)  ENV LANG=C.UTF-8             0B 
bc453a32748e        5 months ago        /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B 
94b0366d5ba2        5 months ago        /bin/sh -c #(nop) ADD file:093f0723fa46f6cdb…   4.15MB 

下次构建Docker镜像时,将重新创建整个38 MB的层,因为重新打包了JAR文件。在此示例中,应用程序的大小相对较小(因为仅基于spring-boot-starter-web和其他依赖项,例如spring-actuator)。在实际开发中,这些大小通常要大得多,因为它们不仅包括Spring Boot库,还包括其他第三方库。根据我的经验,实际的Springboot应用程序的大小范围可能在50 MB到250 MB之间(如果不是更大的话)。仔细观察该应用程序,应用程序JAR中只有372 KB是应用程序代码。其余38 MB是依赖库。这意味着实际上只有0.1%的层在变化。其余99.9%不变。

7. 镜像层生命周期

这是基于镜像层的基本考虑:内容的生命周期。镜像层的内容应具有相同的生命周期。Springboot应用程序的内容有两个不同的生命周期:不经常更改的依赖库和经常更改的应用程序类。每次由于应用程序代码更改而重建该层时,也会包含不变的二进制文件。在快速的应用程序开发环境中,不断更改和重新部署应用程序代码,这种附加成本可能变得非常昂贵。想象一个应用团队在Pet Clinic上进行迭代。团队每天更改和重新部署应用程序10次。这10个新层的成本为每天383 MB。如果使用更多实际大小,则每天最多可以达到2.5 GB或更多。最终将浪费大量的构建时间,部署时间和Docker仓库空间。快速迭代的开发和交付是决定我们是继续使用简单的单层方法,还是采用更有效的替代方法。

8. 拥抱Docker,进入双层

在简单性和效率之间进行权衡时,我认为正确的选择是“双层”方法。(可以有更多的层,但是太多的层可能有害,并且违反了 Docker最佳实践)。在双层方法中,我们构建Docker镜像,以使Springboot应用程序的依赖库,存在于应用程序代码下方的一层中。这样,各层将遵循内容的不同生命周期。通过将不经常更改的依赖库推入一个单独的层,并仅将应用程序类保留在顶层,那么迭代重建和重新部署就会更快。
在这里插入图片描述

9. Java Springboot双层

双层方法加快了迭代开发的速度,并最大程度地缩短了部署时间。当然实际效率因应用程序而异,但是平均而言,这将使应用程序部署大小减少90%,同时相应地缩短了部署周期。

10. 双层方法

在双层方法中,我们构建这样的Docker镜像:以使Spring Boot应用的依赖存在于应用代码下方的一层中。通过将不经常更改的依赖推入一个单独的层,并且仅将应用类保留在顶层,这样迭代重建和重新部署就会更快。

为了做到这一点,我们需要一种将Spring Boot应用拆分为这些独立组件的工具,例如:springBootUtility。

  • springBootUtility
    springBootUtility是Open Liberty中的新工具,它将把Spring Boot应用分为两部分:依赖库(例如Spring Boot启动程序和其他第三方库)以及应用代码。依赖库放置在缓存中,应用代码用于构造一个精简后的应用程序。精简后的应用程序包含一个文件,该文件引用类路径上所需的库。然后可以将此精简应用程序部署到Open Liberty,它将从库缓存中生成完整的类路径。

  • Docker多阶段构建
    用于构建双镜像层的Dockerfile,可以使用多阶段构建。多阶段构建允许单个Dockerfile创建多个镜像,其中一个镜像的内容可以复制到另一个镜像中,从而丢弃临时内容。这使你可以大幅度减小最终镜像的大小,而无需涉及多个Docker文件。我们使用此功能在Docker构建过程中拆分Spring Boot应用。

  • Docker镜像
    Docker镜像可以使用带有Open J9和 Open Liberty的Open JDK。Open JDK为开源Java技术提供了坚实的基础。与 Open JDK附带的默认Java虚拟机相比, Open J9 带来了一些性能改进。Open Liberty是一个多程序模型运行时,支持Java EE,MicroProfile和Spring。这就方便开发团队可以使用具有运行时堆栈一致的各种编程模型。

11. Dockerfile示例
  • Dockerfile(我们将逐步介绍它的每一步操作)。
FROM adoptopenjdk/openjdk8-openj9 as staging
​
ARG JAR_FILE
ENV SPRING_BOOT_VERSION 2.0
​
# Install unzip; needed to unzip Open Liberty
RUN apt-get update \
    && apt-get install -y --no-install-recommends unzip \
    && rm -rf /var/lib/apt/lists/*
​
# Install Open Liberty
ENV LIBERTY_SHA 4170e609e1e4189e75a57bcc0e65a972e9c9ef6e
ENV LIBERTY_URL https://public.dhe.ibm.com/ibmdl/export/pub/software/openliberty/runtime/release/2018-06-19_0502/openliberty-18.0.0.2.zip
​
RUN curl -sL "$LIBERTY_URL" -o /tmp/wlp.zip \
   && echo "$LIBERTY_SHA  /tmp/wlp.zip" > /tmp/wlp.zip.sha1 \
   && sha1sum -c /tmp/wlp.zip.sha1 \
   && mkdir /opt/ol \
   && unzip -q /tmp/wlp.zip -d /opt/ol \
   && rm /tmp/wlp.zip \
   && rm /tmp/wlp.zip.sha1 \
   && mkdir -p /opt/ol/wlp/usr/servers/springServer/ \
   && echo spring.boot.version="$SPRING_BOOT_VERSION" > /opt/ol/wlp/usr/servers/springServer/bootstrap.properties \
   && echo \
'<?xml version="1.0" encoding="UTF-8"?> \
<server description="Spring Boot Server"> \
  <featureManager> \
    <feature>jsp-2.3</feature> \
    <feature>transportSecurity-1.0</feature> \
    <feature>websocket-1.1</feature> \
    <feature>springBoot-${spring.boot.version}</feature> \
  </featureManager> \
  <httpEndpoint id="defaultHttpEndpoint" host="*" httpPort="9080" httpsPort="9443" /> \
  <include location="appconfig.xml"/> \
</server>' > /opt/ol/wlp/usr/servers/springServer/server.xml \
   && /opt/ol/wlp/bin/server start springServer \
   && /opt/ol/wlp/bin/server stop springServer \
   && echo \
'<?xml version="1.0" encoding="UTF-8"?> \
<server description="Spring Boot application config"> \
  <springBootApplication location="app" name="Spring Boot application" /> \
</server>' > /opt/ol/wlp/usr/servers/springServer/appconfig.xml
​
# Stage the fat JAR
COPY ${JAR_FILE} /staging/myFatApp.jar
​
# Thin the fat application; stage the thin app output and the library cache
RUN /opt/ol/wlp/bin/springBootUtility thin \
 --sourceAppPath=/staging/myFatApp.jar \
 --targetThinAppPath=/staging/myThinApp.jar \
 --targetLibCachePath=/staging/lib.index.cache
​
# unzip thin app to avoid cache changes for new JAR
RUN mkdir /staging/myThinApp \
   && unzip -q /staging/myThinApp.jar -d /staging/myThinApp
​
# Final stage, only copying the liberty installation (includes primed caches)
# and the lib.index.cache and thin application
FROM adoptopenjdk/openjdk8-openj9
​
VOLUME /tmp
​
# Create the individual layers
COPY --from=staging /opt/ol/wlp /opt/ol/wlp
COPY --from=staging /staging/lib.index.cache /opt/ol/wlp/usr/shared/resources/lib.index.cache
COPY --from=staging /staging/myThinApp /opt/ol/wlp/usr/servers/springServer/apps/app
​
# Start the app on port 9080
EXPOSE 9080
CMD ["/opt/ol/wlp/bin/server", "run", "springServer"]
  • Dockerfile细节解读

使用Docker的多阶段构建和Open Liberty中的springBootUtility,Dockerfile就可以把Spring Boot应用拆分。

我们把基础镜像openjdk8-openj9命名为stagging镜像。

首先,我们安装unzip。

接下来,我们下载Open Liberty并进行一些配置。所有这些准备工作都需要使用Open Liberty工具。虽然它现在很简陋,但是我们将在Docker镜像18.0.0.2发布 时改进完善。

镜像具有所需的所有工具后,将程序JAR文件复制到stagging镜像中并进行拆分。

在/staging/myFatApp.jar基础上我们会创建一个精简后的应用,将其解压后可以进行进一步的优化。解压使应用程序直接从类文件中托管。如果类文件没有更改,后续重建可以重新使用应用层。

现在准备工作已经完成,然后我们就可以使用COPY命令,拷贝整个Liberty安装,依赖库和精简后的应用。在Dockerfile中,单独的COPY命令会生成单独的层。较大的库依赖层(34.2MB)和较小的应用层(1.01MB)将会是“双重层”的含义。

$ docker history openlibertyio/spring-petclinic
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
883ee6374f66        7 minutes ago       /bin/sh -c #(nop)  CMD ["/opt/ol/wlp/bin/ser…   0B
e3ba1351fc05        7 minutes ago       /bin/sh -c #(nop)  EXPOSE 9080                  0B
86c646de6626        7 minutes ago       /bin/sh -c #(nop) COPY dir:589967d5ae0ade9a5…   1.01MB
8f98ce0a6c10        7 minutes ago       /bin/sh -c #(nop) COPY dir:d764c6a82219ed564…   34.2MB
240306c081cd        7 minutes ago       /bin/sh -c #(nop) COPY dir:0b45938a62d056d88…   200MB
161006b94f8e        22 minutes ago      /bin/sh -c #(nop)  VOLUME [/tmp]                0B
f50ba84462ab        3 weeks ago         /bin/sh -c #(nop)  ENV PATH=/opt/java/openjd…   0B
<missing>           3 weeks ago         /bin/sh -c set -eux;     ARCH="$(dpkg --prin…   193MB
<missing>           3 weeks ago         /bin/sh -c #(nop)  ENV JAVA_VERSION=jdk8u162…   0B
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/* && ap…   16MB
<missing>           3 weeks ago         /bin/sh -c #(nop)  MAINTAINER Dinakar Gunigu…   0B
<missing>           2 months ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>           2 months ago        /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>           2 months ago        /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$…   2.76kB
<missing>           2 months ago        /bin/sh -c rm -rf /var/lib/apt/lists/*          0B
<missing>           2 months ago        /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   745B
<missing>           2 months ago        /bin/sh -c #(nop) ADD file:592c2540de1c70763…   113MB

现在,当进行应用更改时,仅需要更改应用层。

  • Dockerfile运行效果
    你可以复制此Dockerfile,并运行它。
$ docker build --build-arg JAR_FILE=target/spring-petclinic-2.0.0.BUILD-SNAPSHOT.jar -t openlibertyio/spring-petclinic .

生成的Docker镜像如下所示:
在这里插入图片描述
注意:整个Docker镜像并不像单层方法那么小。因为基本镜像不是基于Alpine Linux的,Liberty的安装也不是最简化。我们也正在努力改善这一点。

12. 未来的方向

到目前为止,我们对所构建的内容感到满意,但是,构建这些镜像的用户体验不是很好。我们将在接下来的几个月中继续努力。还将发布包含预配置的Open Liberty实例的Docker镜像。这将大大降低Dockerfile的复杂性。

我们还认识到,将这些双层构建集成到持续交付中,还有改进的空间。我们也有兴趣去解决在Docker中的Spring Boot应用体验。

最后,这种从应用中分离静态依赖的方法并不是Spring Boot应用独有的!使用Java EE或MicroProfile应用也可以获得类似的效率。这也是我们正在探索的另一个领域。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值