【超全教程】SpringBoot 2.3.x 分层构建 Docker 镜像实践

纯洁的微笑 2021-01-28 11:25:01 ⋅ 75 阅读

作者:超级小豆丁

http://www.mydlq.club/article/98/

目录

  • 什么是镜像分层
  • SpringBoot 2.3.x 新增对分层的支持
  • 创建测试的 SpringBoot 应用
    • Maven 中引入相关依赖和插件
    • 创建测试的 Controller 类
    • 创建 SpringBoot 启动类
  • 创建两种构建镜像的 Dockerfile 脚本
    • 普通镜像构建脚本文件 dockerfile-number
    • 分层镜像构建脚本文件 dockerfile-layer
  • 使用两种 Dockerfile 构建项目镜像
    • 在服务器一构建普通 Docker 镜像
    • 在服务器二构建分层 Docker 镜像
  • 镜像推送到镜像仓库测试
    • 推送镜像到镜像仓库测试
    • 镜像仓库拉取镜像测试
  • 镜像构建、推送、拉取时间汇总
    • 不使用分层构建镜像
    • 使用分层构建镜像
    • 总结

系统环境:

  • Docker 版本:19.03.13
  • Open JDK 基础镜像版本:openjdk:8u275
  • 私有的 Harbor 镜像仓库:自建 Harbor 私库
  • 项目 Github:

https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-layer

参考地址:

https://docs.docker.com/storage/storagedriver/

一、什么是镜像分层

镜像的构成

现在一谈起镜像大部分都是指 Docker 引擎构建的镜像,一般 Docker 镜像是由很多层组成,底层是操作系统,然后在其之上是基础镜像或者用户自定义 Dockerfile 脚本中定义的中间层。

其中镜像在构建完成后,用户只能对镜像进行读操作,而不能进行写操作,只有镜像启动后变为容器,才能进行读写操作。镜像整体结构,可以观看下图:

 

该图中展示了镜像的基本组成,但是图中这一个个中间层是什么呢?要想了解这些层具体是什么,那得知道如何构建 Docker 镜像了。平时我们构建 Docker 镜像时候,都是编写 Dockerfile 脚本,然后使用 Docker 镜像构建命令,按照脚本一行行执行构建。

如下就是一个 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 . 

## 命令执行的过程
Step 1/7 : FROM openjdk:8u275
 ---> 82f24ce79de6
Step 2/7 : VOLUME /tmp
 ---> Running in a6361fdfc193
Removing intermediate container a6361fdfc193
 ---> a43948bf1b98
Step 3/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 .

Step 1/6 : FROM openjdk:8u275
 ---> 82f24ce79de6
Step 2/6 : VOLUME /tmp
 ---> Using cache
 ---> a43948bf1b98
Step 3/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 新增对分层的支持

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 两个文件:

  • classpath.idx: 文件列出了依赖的 jar 包列表,到时候会按照这个顺序载入。
  • layers.idx: 文件清单,记录了所有要被复制到 Dokcer 镜像中的文件信息。

根据官方介绍,在构建 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 的内容后生成的文件夹,其中各个文件夹作用是:

  • dependencies: 存储项目正常依赖 Jar 的文件夹。
  • snapshot-dependencies: 存储项目快照依赖 Jar 的文件夹。
  • resources: 用于存储静态资源的文件夹。
  • application: 用于存储应用程序类相关文件的文件夹。

三、创建测试的 SpringBoot 应用

创建测试的 SpringBoot 项目,并且在 pom.xml 中开启镜像分层。

1、Maven 中引入相关依赖和插件

<?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>

2、创建测试的 Controller 类

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello world!";
    }

}

3、创建 SpringBoot 启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.classargs);
    }

}

四、创建两种构建镜像的 Dockerfile 脚本

为了方便体现出 SpringBoot 2.3.x 支持的分层构建 Dockerfile 的优点,这里在 Java 源码文件夹下,创建普通与分层两种构建镜像的 Dockerfile 脚本,后续会使用这两种脚本构建 Docker 镜像进行构建速度、推送速度、拉取速度的对比。

1、普通镜像构建脚本文件 dockerfile-number

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" ]

说明:

  • TZ: 时区设置,而 Asia/Shanghai 表示使用中国上海时区。
  • JVM_OPTS: 指定 JVM 启动时候的参数,-XX:MaxRAMPercentage 参数和 -Xmx 类似,都是限制堆内存大小,只不过 -Xmx 需要手动指定限制大小,而 -XX:MaxRAMPercentage 则是根据虚拟机可用内存百分比限制。
  • JAVA_OPTS: 在镜像启动时指定的自定义 Java 参数,例如 -Dspring.application.name=xxx。

2、分层镜像构建脚本文件 dockerfile-layer

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"
ENV JVM_OPTS="-XX:MaxRAMPercentage=80.0"
ENV JAVA_OPTS=""
ENTRYPOINT ["sh","-c","java $JVM_OPTS $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]

说明:

  • TZ: 时区设置,而 Asia/Shanghai 表示使用中国上海时区。
  • -Djarmode=layertools: 指定构建 Jar 的模式。
  • extract: 从 Jar 包中提取构建镜像所需的内容。
  • -from=builder 多级镜像构建中,从上一级镜像复制文件到当前镜像中。

五、使用两种 Dockerfile 构建项目镜像

1、在服务器一构建普通 Docker 镜像

(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 时间。

2、在服务器二构建分层 Docker 镜像

(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 .

Docker 镜像构建总共花费 3.44s 时间。

(3)、第三次构建(修改代码内容)

## 修改源代码任意内容后,然后再次将源代码构建 Jar 包
$ mvn clean install

## 构建 SpringBoot 应用的 Docker 镜像
$ time docker build -t hub.mydlq.club/library/springboot-layer:0.0.3 .

Docker 镜像构建总共花费 2.82s 时间。

六、镜像推送到镜像仓库测试

1、推送镜像到镜像仓库测试

服务器一推送普通镜像到镜像仓库1:

## 第一次推送镜像time docker push hub.mydlq.club/library/springboot-normal:0.0.1

real    0m35.215s

## 第二次推送镜像time docker push hub.mydlq.club/library/springboot-normal:0.0.2

real    0m14.051s

## 第三次推送镜像time docker push hub.mydlq.club/library/springboot-normal:0.0.3

real    0m14.183s

服务器二推送分层镜像到镜像仓库2:

## 第一次推送镜像time docker push hub.mydlq.club/library/springboot-layer:0.0.1

real    0m34.121s

## 第二次推送镜像time docker push hub.mydlq.club/library/springboot-layer:0.0.2

real    0m13.605s

## 第三次推送镜像time docker push hub.mydlq.club/library/springboot-layer:0.0.3

real    0m4.805s

2、镜像仓库拉取镜像测试

服务器一推送从镜像仓库1拉取镜像:

## 清理全部镜像
$ docker rm --force $(docker images -qa)

## 拉取镜像 springboot-normal:0.0.1
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.1

real    0m35.395s

## 拉取镜像 springboot-normal:0.0.2
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.2

real    0m6.501s

## 拉取镜像 springboot-normal:0.0.3
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.3

real    0m6.993s

服务器二推送从镜像仓库2拉取镜像:

## 清理全部镜像
$ docker rm --force $(docker images -qa)

## 拉取镜像 springboot-layer:0.0.1
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.1

real    0m30.615s

## 拉取镜像 springboot-layer:0.0.2
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.2

real    0m4.811s

## 拉取镜像 springboot-layer:0.0.3
$ time docker push hub.mydlq.club/library/springboot-normal:0.0.3

real    0m1.293s

七、镜像构建、推送、拉取时间汇总

1、不使用分层构建镜像

 

如下图:

 

2、使用分层构建镜像

 

如下图:

 

3、总结

上面进行了使用 SpringBoot2.3.x 分层的方式构建镜像与普通的方式构建镜像,在镜像的构建、推送、拉取方面进行了执行速度对比,总结出如下结论:

  • 镜像构建: 在构建上,使用分层 Jar 构建镜像可能比普通方式构建镜像更繁琐,所以也更耗时,故而在构建上分层 Jar 构建镜像没有太多优势。
  • 镜像推送: 在推送上,如果每次构建镜像都只是修改构建镜像项目的源码,使用分层 Jar 构建镜像,可以大大加快镜像推送速度。如果是修改构建镜像项目中的依赖包,则和普通构建一样速度很慢。
  • 镜像拉取: 拉取和推送类似,如果只修改构建镜像项目的源码,只会拉取源码相关的中间层镜像,该层非常小(一般几百KB),拉取速度自然非常快。而对构建镜像项目的依赖包进行变动(增加依赖、删除依赖、修改依赖版本等),则会和普通方式构建镜像一样,拉取速度很慢,这是因为依赖包层是中间层镜像最大的一层(一般在10MB~200MB之间),如果该层发生变动则整个层会进行重新拉取,这样速度自然会很慢。

全部评论: 0

    我有话说:

    「尝鲜」SpringBoot 快速整合Swagger 3.0

    第一步:Maven引入Swagger3.0 starter依赖 Maven项目中引入springfox-boot-starter依赖: <dependency> <

    项目中为什么用Docker

      项目为什么要用 docker,需要了解 docker 的优势,结合项目的实际情况来决定是否需要使用 docker,千万不能“为了使用而使用”或者“跟风使用 docker”。 使用

    WebMIS 1.0.0 beta.3 发布,栈开发基础框架

    栈开发基础框架,包括 PHP / Python / SpringBoot / Phalcon / Flutter / NodeJS / Vue / Swoole / Redis / API 等技术

    【开源资讯】Spring Boot 2.4.0.M4 发布

    Spring Boot 2.4.0 的第四个里程碑版本发布了,可以从里程碑仓库获取。此版本包含 145 项更新内容,亮点如下:1、改进故障分析器(Failure Analyzer

    项目中为什么用docker

    前几天,公司一批服务器就要到期了,由于服务器是15年购买的,硬件的性能远比现在新出的云主机低,因此决定把所有服务器都换成新一代服务器,但是小编整准备动手迁移服务器时,内心一阵阵崩溃感涌上心头,仔细一算,每台服务器都要做同样的事情,然后他们说可以用...

    Docker Desktop 3.0.0 发布,Docker Hub 限制免费用户

    Docker Desktop 3.0.0 版本发布了。Docker Desktop 是一个支持 Windows 和 MAC 系统的完整桌面开发环境,包括 Docker App,开发人员工具

    SpringBoot+zk+dubbo架构实践(一):本地部署zookeeper

    SpringBoot+zk+dubbo架构实践系列实现目标:自己动手搭建微服务架构

    微服务架构实战篇:快速入手SpringBoot 2.0,欢迎入坑哦~~~

    SpringBoot 2.0 基本要求Java最低要求8以上,不再支持Java 6 和 7等低版本。

    Micronaut 2.3.3,基于 JVM 的微服务应用框架

    Micronaut 是 Grails 框架作者打造的开源项目,也是新一代基于 JVM 的栈微服务框架,用于构建模块化的、易于测试的微服务应用。有关 Micronaut 的特性介绍点此查看。 近日

    Micronaut 2.2.3 发布,基于 JVM 的微服务应用框架

    Micronaut 2.2.3 发布了,本次更新内容主要为项目组件升级。 Micronaut 是 Grails 框架作者打造的开源项目,也是新一代基于 JVM 的栈微服务框架,用于构建模块化的

    SpringBoot+zk+dubbo架构实践(二):SpringBoot 集成 zookeeper

    不啰嗦,本篇完成两件事:1、搭建SpringBoot 框架;2、基于spring boot框架访问zookeeper。

    微服务架构实战篇(六):Spring boot2.x 集成阿里大鱼短信接口详解与Demo

    Spring boot2.x 集成阿里大鱼短信接口,发送短信验证码及短信接口详解。

    IntelliJ IDEA 优化设置,效率飞起来!

    作者:请叫我小思http://blog.csdn.net/zeal9s/article/details/83544074 显示工具条 (1)效果图(2)设置方法 标注1:View–>

    【简单】Docker - 实战TLS加密通讯

    快速配置一个最简单的docker TLS加密通讯

    Serverless Framework 2.3.0 发布

    Serverless 架构开发框架 Serverless Framework 发布了 2.3.0 版本,该框架使用 AWS Lambda、Azure Functions、Google

    高并发案例 - 库存发问题

    1. 库存发的原因是什么? 在执行商品购买操作时,有一个基本流程: 例如初始库存有3个。 第一个购买请求来了,想买2个,从数据库中读取到库存有3个,数量够,可以买,减库存后,更新库存为1个

    Spring Boot 2.4.0-RC1, 2.1.18, 2.2.11 和 2.3.5 发布

    Spring Boot 多个分支发布了新版本,分别2.4.0-RC1, 2.1.18, 2.2.11 和 2.3.5。 Spring Boot 2.4.0-RC1 此版本是 

    SpringBoot2.0填坑(一):使用CROS解决跨域并解决swagger 访问不了问题

    公司后台是采用SpringBoot2.0 搭建的微服务架构,前端框架用的是vue 使用前后端分离的开发方式,在开发联调的时候需要进行跨域访问,那么使用CROS解决了跨域问题,但是swagger 却用