Java项目镜像化之分层构建

上篇日志介绍了关于Docker的一些粗浅知识,本期将简单的说明下Docker镜像和容器的存储结构,以及优化构建的几种方式。

镜像及容器存储结构

为了高效的构建镜像我们应当去了解学习下镜像和容器的存储结构。在官方文档中有较为详细的介绍,这里做一些简单的说明,如下图所示

Docker 镜像由众多只读层组成,层即图层,常见于命令行中拉取镜像时的layers。每个图层代表一个 Dockerfile 指令,并不是所有命令会增加图层,只有命令 RUN, COPY, ADD会创建真实的图层。其他指令会创建临时中间镜像,并不会增加构建大小。镜像的这些层是堆叠的,每一层都是前一层增量变化,当我们运行镜像产生容器时,会在镜像顶层追加一层可写层(容器层)。运行时容器做的文件更改,例如写入新文件、修改现有文件和删除文件,都将写入容器层。对镜像层文件操作时,采用写时覆盖的原则,将由上向下查找镜像层文件并复制至容器层进行修改。多个运行时的容器存储结构看起来就像下面一样

每个容器拥有自己的读写层但共用同一个镜像的只读层。分层的镜像设计,使得镜像之间可以复用图层,当我们pull一个image的时,本地其他image已经拉取的图层就可以跳过,加快镜像使用的同时也节约磁盘的占用。在知道这些知识之后我们可以对上篇日志中提到的Dockerfile构建进行优化和改进,对其他分层构建的插件这里也会介绍一二。

优化构建

Dockerfile

先来回顾之前的Dockerfile

1
2
3
4
5
6
FROM openjdk:8-jdk-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

可以看到应用app.jar只有一层,按照官方的最佳实践,我们可以进行如下方式的优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM openjdk:8-jre-alpine as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:8-jre-alpine
WORKDIR application

ENV TZ Asia/Shanghai
# 调整时区
RUN apk add tzdata && cp /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone \
&& apk del tzdata

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

上述包含了官方的两种最佳实践

  • 多阶段构建,第一阶段我们构建了中间镜像builder,为第二阶段最终生产镜像做准备
  • 镜像分层,从第一阶段里jar拆分的文件COPY产生多个图层组成镜像

镜像分层这里借助了spring boot在spring-boot-maven-plugin文档的解决方案,只此一个jar图层分解为

  • dependencies依赖层
  • spring-boot-loader启动加载层
  • snapshot-dependencies快照依赖层
  • application本地模块,类,资源层

我们也可以按文档中那样自己定义图层,比如把类和资源再单独分层,这里就不在做演示了。需要注意的是,由于图层是堆叠的,我们在编写Dockerfile时需要注意图层构建的顺序,经常变动的图层应该写在下方,这样构建的镜像图层的复用程度会比较高。

Jib

使用Dockerfile是一种方式,而Jib为Java 应用程序构建提供了另外一种方式,Jib的构建无需安装Docker,也无需深入掌握 Docker 技巧。其目标有如下三点:

  • 快速部署修改部分。Jib 将Java应用分为多层,从类中分离依赖项, 只需部署更改的层
  • 可重现- 相同内容创建的镜像始终相同
  • Daemonless - 减少对容器服务端依赖

其使用方式如下

1
2
3
4
5
6
7
8
9
10
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib-maven-plugin.version}</version>
<configuration>
<from>
<image>openjdk:8-jre-alpine</image>
</from>
</configuration>
</plugin>

Jib也进行了较为详细的图层拆分,它的拆分由低到高可划分为

  • dependencies
  • resources
  • classes
  • jvm arg files

Jib使用比简单,如果没有复杂的镜像化需求用这个我认为是最简单快速的选择,更详细的了解可以参考Jib的官方文档。

CNB

最后介绍下spring boot插件所使用的镜像构建技术CNB,CNB全称Cloud Native Buildpacks是云原生基金会旗下的镜像构建工具。其使用方式如下

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>

CNB使用了一种名为builder的概念,并具象化为builder镜像,包含执行构建所需的所有组件:

  • Buildpacks,一组可执行文件,用于检查应用程序源代码并创建构建和运行应用程序的计划
  • 生命周期,生命周期协调 buildpack 的执行,然后将生成的工件组装
  • Stack’s build image,堆叠运行时镜像与应用镜像

作为spring boot插件原生支持的镜像构建方式,也是分层构建,可以通过修改layer文件来更改自定义图层。CNB集成使用虽然简单,但遇到的外部依赖问题却让人望而却步。CNB的buildpacks在构建中会下载外部资源如github的JRE,但由于众所周知的原因,这个下载的过程相当不顺利,解决方案可以参考stackoverflow路径映射来解决,但同样的当资源更新时路径映射也要做相关的修改,使用起来颇为繁琐。不过可以关注其后续的发展,说不定以后问题就得到解决。

写在最后

一项技术的使用从来都不像其表面那样看起来容易,简单的镜像构建也会有不简单的一面,介绍的这几种镜像构建的方式大家可以根据自己的需要进行选择,希望这篇日志可以帮到大家。