likes
comments
collection
share

如何优化SpringBoot应用的Docker镜像?

作者站长头像
站长
· 阅读数 12
  • 前言

容器技术已经成为当前软件开发的技术标准,我们周边的项目也越来越多选择容器化,而Docker已然成为我们不可或缺的软件开发工具。我们通常通过DockerFile将Spring Boot应用打包为Docker镜像,在业务初期,这一切看起来都很美好。然而随着业务的发展与需求的快速响应,任意的代码变动,镜像都需要重新的构建,导致我们的CI/CD耗时很久,效率低下。 今天,将为大家介绍如何使用Spring Boot的新功能来复用Docker Layers,从而提升CI/CD效率。

  • 背景

作为以微服务为核心架构的技术团队,我们几乎每天都需要和Spring Boot、Docker打交道,这种技术架构为我们带来了极大的灵活性和可拓展性。但是随着我们业务的发展,慢慢的我们发现应用的镜越来越大,消耗的带宽、磁盘、构建时间越来越多,我们的CI/CD效率从刚开始的几分钟慢慢的增长到几十分支。即使是一个微小的改动,上线时间也是惊人的“长”。看起来这些问题都是不起眼的,但却真实的影响着每位研发人员的开发效率。一些新的技术正在悄悄的改变这一现状,是的,我们的老朋友Spring Boot为大家带来了以“隔离”为核心思想的解决方案。

  • Docker 架构如何优化SpringBoot应用的Docker镜像?

    • Clients - 使用户可以和Docker交互
    • Daemon - Docker守护进程,管理Docker镜像、容器、网络、存储等;监听DockerAPI请求;
    • Image - 用于构建容器的只读的二进制模板,包含了容器的功能、需要的元数据等
    • Container - 容器是镜像可运行的实例,提供了一种轻量级的且独立于操作系统的可以运行应用程序的方式;
    • Repository - 镜像仓库
  • 分层机制

    如果想要对Docker镜像优化,我们首要的是要了解镜像的分层机制

    • 分层机制

如何优化SpringBoot应用的Docker镜像? * Docker镜像由很多“层”组成,每一“层”代表了DockerFile中的一个命令,每一层都是基于基础层变化的增量,增量的构建自下而上。 * 当我们构建Docker镜像时,这些“层”会缓存在宿主机中,这些“层”是可以复用的,也就给我们优化的空间和机会。

如何优化SpringBoot应用的Docker镜像?

  • 优化原理

    • 根据上面Docker Image构建的原理,如果我们把易变化的“层”放在底部,那么我们每次构建它,都需要把它的顶层去掉,重新构建;相反,如果我们易变化的放在顶层,就可以减少工作量了,这就是我们优化Image的核心思想。
  • Spring Boot Jar优化

  • Spring Boot fat jar

    FROM openjdk:11-jre-slim
    ARG JAR_FILE=target/*.jar
    COPY ${JAR_FILE} application.jar
    EXPOSE 8080
    ENTRYPOINT ["java","-jar","/application.jar"]
  • 优化思路
    • 根据上述的Docker Image的优化原理,如果我们把Spring Boot fat jar,拆分为多个“层”,并重用宿主机缓存中的“复用层”,那么它的构建效率也会大大提升。

    • 同时,根据应用的变化频率,我们可以对Spring Boot应用进行上下分层。

    • 如下所示,应用程序有自己的“层”,我们修改源码时,只需要重新构建独立的“应用层”,其他“层”则使用“可复用层”,从而减少了Docker Image的构建和启动时间。 如何优化SpringBoot应用的Docker镜像?

    • application - 核心业务层及配置,最经常变动 - 最高

    • snapshot-dependencies - 业务层依赖,时常变动 - 高

    • spring-boot-loader - spring-boot jar loader,不经常变动 - 低

    • dependencies - 基础依赖,不经常变动 - 低

    • Spring Boot镜像优化

    • 生成“分层”Fat Jar

    • Maven 配置 Spring-Boot >=2.3.0

    • Spring Boot将生成“分层”的fat jar(将分层工具加入到Jar)。

    • 我们使用Spring Boot “分层”工具将“层”提取到Docker 镜像中。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <layers>
          <enabled>true</enabled>
          <includeLayerTools>true</includeLayerTools>
        </layers>
      </configuration>
    </plugin>
  </plugins>
</build>
  • “分层”工具 - spring-boot-jarmode-layertools
  • 生成fat jar - mvn package
  • 分层展示 - java -Djarmode=layertools -jar app.jar list
  • 分层提取 - java -Djarmode=layertools -jar my-app.jar extract
/dependencies 
/spring-boot-loader 
/snapshot-dependencies 
/application
  • 索引文件
jar tf target/application.jar

BOOT-INF/layers.idx

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"
  • 自定义“层”的划分
// maven configuration
<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                        <configuration>${project.basedir}/src/layers.xml</configuration>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

// custom layer configuration
<layers xmlns="http://www.springframework.org/schema/boot/layers"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
                          https://www.springframework.org/schema/boot/layers/layers-3.0.xsd">
    <application>
        <into layer="spring-boot-loader">
            <include>org/springframework/boot/loader/**</include>
        </into>
        <into layer="application" />
    </application>
    <dependencies>
        <into layer="application">
            <includeModuleDependencies />
        </into>
        <into layer="snapshot-dependencies">
            <include>*:*:*SNAPSHOT</include>
        </into>
        <into layer="dependencies" />
    </dependencies>
    <layerOrder>
        <layer>dependencies</layer>
        <layer>spring-boot-loader</layer>
        <layer>snapshot-dependencies</layer>
        <layer>application</layer>
    </layerOrder>
</layers>
  • Docker 镜像构建
  • Multi-stage Docker build
  • Stage1 - 基础构建
FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
  • Stage2 - 分层构建
FROM openjdk:11-jre-slim
WORKDIR application
COPYfrom=builder application/dependencies/ ./
COPYfrom=builder application/spring-boot-loader/ ./
COPYfrom=builder application/snapshot-dependencies/ ./
COPYfrom=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
  • 生成Docker File
FROM openjdk:11-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:11-jre-slim
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"]
  • 构建日志
Sending build context to Docker daemon  41.87MB
Step 1/12 : FROM openjdk:11-jre-slim as builder
 – -> 973c18dbf567
Step 2/12 : WORKDIR application
 – -> Using cache
 – -> b6b89995bd66
Step 3/12 : ARG JAR_FILE=target/*.jar
 – -> Using cache
 – -> 2065a4ad00d4
Step 4/12 : COPY ${JAR_FILE} application.jar
 – -> c107bce376f9
Step 5/12 : RUN java -Djarmode=layertools -jar application.jar extract
 – -> Running in 7a6dfd889b0e
Removing intermediate container 7a6dfd889b0e
 – -> edb00225ad75
Step 6/12 : FROM openjdk:11-jre-slim
 – -> 973c18dbf567
Step 7/12 : WORKDIR application
 – -> Using cache
 – -> b6b89995bd66
Step 8/12 : COPY – from=builder application/dependencies/ ./
 – -> Using cache
 – -> c9a01ed348a9
Step 9/12 : COPY – from=builder application/spring-boot-loader/ ./
 – -> Using cache
 – -> e3861c690a96
Step 10/12 : COPY – from=builder application/snapshot-dependencies/ ./
 – -> Using cache
 – -> f928837acc47
Step 11/12 : COPY – from=builder application/application/ ./
 – -> 3a5f60a9b204
Step 12/12 : ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
 – -> Running in f1eb4befc4e0
Removing intermediate container f1eb4befc4e0
 – -> 8575cc3ac2e3
Successfully built 8575cc3ac2e3
Successfully tagged svc1:latest
  • 观察“分层镜像”
// really useing cache ?
docker history svc1

IMAGE               CREATED              CREATED BY                                      SIZE             
8575cc3ac2e3        About a minute ago   /bin/sh -c #(nop)  ENTRYPOINT ["java" "org.s…   0B                
3a5f60a9b204        About a minute ago   /bin/sh -c #(nop) COPY dir:0cea19e682012ea7b…   54.1kB           
f928837acc47        4 hours ago          /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba…   0B               
e3861c690a96        4 hours ago          /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8…   224kB             
c9a01ed348a9        4 hours ago          /bin/sh -c #(nop) COPY dir:124320f4334c6319e…   41.5MB           
b6b89995bd66        5 hours ago          /bin/sh -c #(nop) WORKDIR /application          0B

// change source codes
IMAGE               CREATED             CREATED BY                                      SIZE               
b328f4d5f61a        6 seconds ago       /bin/sh -c #(nop)  ENTRYPOINT ["java" "org.s…   0B                 
aca4b7a5f92a        7 seconds ago       /bin/sh -c #(nop) COPY dir:7a586cf8680e2bd04…   55.7kB             
f928837acc47        4 hours ago         /bin/sh -c #(nop) COPY dir:e20e0f7d3984c5fba…   0B                 
e3861c690a96        4 hours ago         /bin/sh -c #(nop) COPY dir:9ef30157c6318a2d8…   224kB             
c9a01ed348a9        4 hours ago         /bin/sh -c #(nop) COPY dir:124320f4334c6319e…   41.5MB             
b6b89995bd66        5 hours ago         /bin/sh -c #(nop) WORKDIR /application          0B
  • 仅有application层重新构建 54.1KB -> 55.7KB
  • 使用了宿主机“可复用层”,即缓存“层”
  • 镜像优化
  • 通过开启“分层”构建Spring Boot 应用,可以明显感到推送镜像速度快了很多
  • 从远程拉去镜像,也只需要拉取变化的“层”,速度明显更快
  • 每次部署,可以节省更多的空间
  • 对比

如何优化SpringBoot应用的Docker镜像?

  • 解决了什么问题?
  • “可复用”的镜像“层”
  • 一次构建,缓存在宿主机,重复使用
  • 没有解决什么?
  • 代码的编译
  • 优化总结
  • 分层镜像构建总览

如何优化SpringBoot应用的Docker镜像?

  • 分层构建的优点
  • 减少了因业务代码变更带来的全量构建所需时间,提高了业务上线的效率
  • 减少了构建时所需的带宽、磁盘空间,提高了宿主机缓存的利用率
  • 提高了镜像仓库推送、拉取的效率
  • 提高了CI/CD的性能和效率
  • 业务试点
  • bill-scheduler 配置Maven,package生成layers.idx如何优化SpringBoot应用的Docker镜像?
  • 编译时间 - Total time: 42.382 s
  • 分层Jar包如何优化SpringBoot应用的Docker镜像?
  • 提取分层Jar包如何优化SpringBoot应用的Docker镜像?
  • 创建Dockerfile
FROM openjdk:8-jre-slim as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} antispam-bill-scheduler-2.20230202.67960-SNAPSHOT.jar
RUN java -Djarmode=layertools -jar antispam-bill-scheduler-2.20230202.67960-SNAPSHOT.jar extract

FROM openjdk:8-jre-slim
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"]
  • 镜像构建

如何优化SpringBoot应用的Docker镜像?

  • 耗时30s左右 - 31.19s
  • 代码变动如何优化SpringBoot应用的Docker镜像?
  • 耗时 - 5s
  • 镜像变更记录如何优化SpringBoot应用的Docker镜像?
  • 节省空间: 300M
  • 传统镜像构建如何优化SpringBoot应用的Docker镜像?
  • 耗时: 15.8s
  • 性能提升
  • 镜像拉取
  • 优化前如何优化SpringBoot应用的Docker镜像?
  • 优化后如何优化SpringBoot应用的Docker镜像?
  • 性能提升20s左右
  • 镜像推送
  • 优化前如何优化SpringBoot应用的Docker镜像?
  • 优化后如何优化SpringBoot应用的Docker镜像?