一、什么是镜像分层
二、SpringBoot 2.3.x 新增对分层的支持
三、创建测试的 SpringBoot 应用
四、创建两种构建镜像的 Dockerfile 脚本
五、使用两种 Dockerfile 构建项目镜像
六、镜像推送到镜像仓库测试
七、镜像构建、推送、拉取时间汇总
系统环境:
作者:超级小豆丁,链接:www.mydlq.club/article/98/
镜像的构成
现在一谈起镜像大部分都是指 Docker 引擎构建的镜像,一般 Docker 镜像是由很多层组成,底层是操作系统,然后在其之上是基础镜像或者用户自定义 Dockerfile 脚本中定义的中间层。其中镜像在构建完成后,用户只能对镜像进行读操作,而不能进行写操作,只有镜像启动后变为容器,才能进行读写操作。镜像整体结构,可以观看下图:
该图中展示了镜像的基本组成,但是图中这一个个中间层是什么呢?要想了解这些层具体是什么,那得知道如何构建 Docker 镜像了。平时我们构建 Docker 镜像时候,都是编写 Dockerfile 脚本,然后使用 Docker 镜像构建命令,按照脚本一行行执行构建。
PS:更多 Docker 和 Spring Boot 的文章可以关注微信公众号「Java后端」回复「666」下载技术栈手册。
如下就是一个 Dockerfile 脚本,脚本内容就构建 Java 项目镜像常用的 Dockerfile 命令:
FROM openjdk:8u275 VOLUME/tmp ADD target/*.jar app.jar ENV TZ="Asia/Shanghai" ENV JAVA_OPTS="" ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0" ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar"]
有了 Dockerfile 脚本,我们需要执行 Docker 的构建镜像命令对执行 Dockerfile 脚本构建镜像,其中构建镜像的过程如下:
## 构建镜像的命令 $ docker build-t java-test:latest. ## 命令执行的过程 Step1/7: FROM openjdk:8u275--->82f24ce79de6 Step2/7: VOLUME/tmp---> Running in a6361fdfc193 Removing intermediate container a6361fdfc193---> a43948bf1b98 Step3/7: ADD target/*.jar app.jar ---> 18f4bc60818f Step 4/7 : ENV TZ="Asia/Shanghai" ---> Running in cc738aa5865b Removing intermediate container cc738aa5865b ---> 538adb85609e Step 5/7 : ENV JAVA_OPTS="" ---> Running in f8b635d32b2b Removing intermediate container f8b635d32b2b ---> 34e7a8cd7b6e Step 6/7 : ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0" ---> Running in 9331cb6e443e Removing intermediate container 9331cb6e443e ---> 232b9c6c1d29 Step 7/7 : ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ] ---> Running in c3a24fba3a10 Removing intermediate container c3a24fba3a10 ---> a41974d5f0e3
可以看到总共存在 7 个构建步骤,每步都与 Dockerfile 里面一行指令对应。样子和下图相似:
如果这时候,我们改变原来 Dockerfile 内容,创建一个新的镜像,其 Dockerfile 如下:
FROM openjdk:8u275 VOLUME/tmp ADD target/*.jar app.jar ENV TZ="Asia/Macao" #与原来 Dockerfile 不同 ENV JVM_OPTS="-Xmx512m -Xss256k" #与原来 Dockerfile 不同 ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ]
执行 Docker 命令构建镜像:
$ docker build-t java-test2:latest. Step1/6: FROM openjdk:8u275--->82f24ce79de6 Step2/6: VOLUME/tmp---> Using cache---> a43948bf1b98 Step3/6: ADD target/*.jar app.jar ---> Using cache ---> 18f4bc60818f Step 4/6 : ENV TZ="Asia/Macao" ---> Running in fd98b90a5485 Removing intermediate container fd98b90a5485 ---> afab3fcdab07 Step 5/6 : ENV JVM_OPTS="-Xmx512m -Xss256k" ---> Running in 19a99576fba9 Removing intermediate container 19a99576fba9 ---> 4eeab7d7c720 Step 6/6 : ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS -jar /app.jar" ] ---> Running in 2dba72e1eef4 Removing intermediate container 2dba72e1eef4 ---> 7c706ecf7698
可以观察到执行过程中,从一开始执行的构建步骤中显示,并没有生成新的中间层镜像,而是直接使用了已经存在的缓存镜像。直至 4⁄6这部中,由于新的 Dockerfile 与原来 Dockerfile 发生变动,所以这部中间层镜像直接是新创建的,并没有使用缓存中间层镜像。
然后往下观察,发现之后的全部构建都是新创建的中间层镜像,即是脚本最后的一行和原来相同,也没有使用缓存中间层镜像。
上面现象说明,Docker 镜像在构建过程中按照 Dockerfile 自上往下的执行顺序中,如果从最上层开始,其脚本内容和已有的缓存中间层镜像内容一致,就会引入缓存中的中间层镜像(并不是直接复制缓存镜像,而是引入镜像文件地址,多个镜像共享这些中间层镜像)。但是,如果执行过程中中间任意一行镜像构建的内容发生变化,那么当前行和之后的全部行在执行时就不会使用缓存中的中间层镜像,而是全部创建新的镜像。
这就是 Docker 镜像中缓存中间层镜像的复用,学会使用缓存构建镜像将大大减少存储空间的占用以及镜像的构建的构建速度,镜像的缓存不仅仅体现在镜像的构建上,在执行”镜像推送”、”镜像拉取”操作时都能观察到其的好处。
说了这么多,相信大家已经对镜像缓存的使用有了初步了解,那么再谈及为什么需要镜像分层就很好解释,其原因就是 Docker 想提高资源的复用率,将一个大镜像拆分成很多层小镜像组成,以达到镜像中间层的复用的目的。
SpringBoot 2.3.x 以后支持分层打包应用,需要 Pom.xml 中引入 SpringBoot 2.3.x 后的父依赖和使用 SpringBoot 打包插件spring-boot-maven-plugin
,并且开启layers
功能,然后执行 Maven 编译源码构建 Jar 包,使用该 Jar 包就可以构建基于分层模式的 Docker 镜像:
项目 pom.xml 中引入 SpringBoot 2.3.x 依赖:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.6.RELEASE</version><relativePath/></parent>
项目 pom.xml 中引入 spring-boot-maven-plugin 打包插件,并且开启分层功能:
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><!--开启分层编译支持--><layers><enabled>true</enabled></layers></configuration></plugin></plugins></build>
执行 Maven 命令,构建分层的 JAR 包,命令和平时的 Maven 构建命令相同:
$ mvn install
观察 Jar 结构,可以看到里面多了classpath.idx
与layers.idx
两个文件:
根据官方介绍,在构建 Docker 镜像前需要从 Jar 中提起出对应的分层文件到 Jar 外面,可用使用下面命令列出可以从分层 Jar 中提取出的文件夹信息:
$ java-Djarmode=layertools-jar target/springboot-layer-0.0.1.jar list
可用该看到以下输出,下面的内容就是接下来使用分层构建后,生成的 Jar 提取出对应资源后的结构:
dependencies spring-boot-loader snapshot-dependencies application
上面即是使用分层工具提取 Jar 的内容后生成的文件夹,其中各个文件夹作用是:
创建测试的 SpringBoot 项目,并且在 pom.xml 中开启镜像分层。
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.6.RELEASE</version></parent><artifactId>springboot-dockerfile-layer</artifactId><packaging>jar</packaging><name>springboot-dockerfile-layer</name><description>springboot build layer example</description><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layers><enabled>true</enabled></layers></configuration></plugin></plugins></build></project>
import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController; @RestController public class TestController{ @GetMapping("/hello") public Stringhello(){return"hello world!";}}
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application{ public static voidmain(String[] args){ SpringApplication.run(Application.class, args);}}
为了方便体现出 SpringBoot 2.3.x 支持的分层构建 Dockerfile 的优点,这里在 Java 源码文件夹下,创建普通与分层两种构建镜像的 Dockerfile 脚本,后续会使用这两种脚本构建 Docker 镜像进行构建速度、推送速度、拉取速度的对比。
FROM openjdk:8u275 VOLUME/tmp ADD target/*.jar app.jar RUN sh -c 'touch /app.jar' ENV TZ="Asia/Shanghai" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0" ENV JAVA_OPTS="" ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
说明:
FROM openjdk:8u275 as builder WORKDIR application COPY target/*.jar application.jar RUN java -Djarmode=layertools -jar application.jar extract FROM openjdk:8u275 WORKDIR application COPY --from=builder application/dependencies/ ./ COPY --from=builder application/snapshot-dependencies/ ./ COPY --from=builder application/spring-boot-loader/ ./ COPY --from=builder application/application/ ./ ENV TZ="Asia/Shanghai" RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0" ENV JAVA_OPTS="" ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
说明:
(1) 第一次构建
## 执行 Maven 命令,将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-normal:0.0.1.
Docker 镜像构建总共花费 24.98s 时间。
(2) 第二次构建(修改依赖 pom.xml 文件)
## 修改 pom.xml 里面的依赖,随意添加一个新的依赖包,然后再次将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-normal:0.0.2.
Docker 镜像构建总共花费 1.27s 时间。
(3) 第三次构建(修改代码内容)
## 修改源代码任意内容后,然后再次将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-normal:0.0.3.
Docker 镜像构建总共花费 1.32s 时间。
(1) 第一次构建
## 执行 Maven 命令,将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-layer:0.0.1.
Docker 镜像构建总共花费 26.12s 时间。
(2) 第二次构建(修改依赖 pom.xml 文件)
## 修改 pom.xml 里面的依赖,随意添加一个新的依赖包,然后再次将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-layer:0.0.2.
(3) 第三次构建(修改代码内容)
## 修改源代码任意内容后,然后再次将源代码构建 Jar 包 $ mvn clean install ## 构建 SpringBoot 应用的 Docker 镜像 $ time docker build-t hub.mydlq.club/library/springboot-layer:0.0.3.
Docker 镜像构建总共花费 2.82s 时间。
服务器一推送普通镜像到镜像仓库1:
## 第一次推送镜像 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.1real0m35.215s ## 第二次推送镜像 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.2real0m14.051s ## 第三次推送镜像 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.3real0m14.183s
服务器二推送分层镜像到镜像仓库2:
## 第一次推送镜像 $ time docker push hub.mydlq.club/library/springboot-layer:0.0.1real0m34.121s ## 第二次推送镜像 $ time docker push hub.mydlq.club/library/springboot-layer:0.0.2real0m13.605s ## 第三次推送镜像 $ time docker push hub.mydlq.club/library/springboot-layer:0.0.3real0m4.805s
服务器一推送从镜像仓库1拉取镜像:
## 清理全部镜像 $ docker rm--force $(docker images-qa) ## 拉取镜像 springboot-normal:0.0.1 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.1real0m35.395s ## 拉取镜像 springboot-normal:0.0.2 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.2real0m6.501s ## 拉取镜像 springboot-normal:0.0.3 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.3real0m6.993s
服务器二推送从镜像仓库2拉取镜像:
## 清理全部镜像 $ docker rm--force $(docker images-qa) ## 拉取镜像 springboot-layer:0.0.1 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.1real0m30.615s ## 拉取镜像 springboot-layer:0.0.2 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.2real0m4.811s ## 拉取镜像 springboot-layer:0.0.3 $ time docker push hub.mydlq.club/library/springboot-normal:0.0.3real0m1.293s
如下图:
如下图:
上面进行了使用 SpringBoot2.3.x 分层的方式构建镜像与普通的方式构建镜像,在镜像的构建、推送、拉取方面进行了执行速度对比,总结出如下结论:
而对构建镜像项目的依赖包进行变动(增加依赖、删除依赖、修改依赖版本等),则会和普通方式构建镜像一样,拉取速度很慢,这是因为依赖包层是中间层镜像最大的一层(一般在10MB~200MB之间),如果该层发生变动则整个层会进行重新拉取,这样速度自然会很慢。