docker 如何导出某个镜像增量部分

本文介绍如何利用Docker镜像构建原理实现增量导出,并深入探讨SpringBoot 2.3版本中引入的分层打包技术,旨在提高镜像构建效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

docker如何导出某个镜像增量部分

问题

离线生产环境下需要 docker save 镜像,然后 dock load 导入。

问题是 docker save 导出的是个完整的镜像,当有变动时,每次都传输完整镜像特别浪费时间,在现场环境执行 load 时可以看出来 docker 只会导入变动的 layer

docker镜像构建原理

背景

​ 1.体验了官方推荐的镜像制作方案,执行docker history命令观察镜像内部,发现是由多个layer组成的,如下图:=
在这里插入图片描述

​ 2.问题来了:搞这么多layer干啥?接下来以图文方式,您一起理解docker镜像layer对java开发者的的作用;

声明

​ 本文的目标是通过图文帮助java开发者理解docker镜像的layer作用,内容和实际情况并未完全保持一致,例如基础镜像的layer没有提到,而且java镜像的layer可能不止业务镜像、配置文件、依赖库这三层;

常见角色

使用docker时,有三个常见角色:

​ 1.镜像制作者:本文中就是SpringBoot应用开发者,写完代码把应用做成docker镜像;
​ 2.docker公共镜像仓库:镜像制作者将镜像推送到仓库给大家使用;
​ 3.镜像使用者:从镜像仓库将镜像下载到本地使用;

接下来的故事围绕上述三个角色展开;

从制作到使用的过程

1.如下图,SpringBoot应用开发者,写完代码把应用做成docker镜像,该镜像的TAG是1.0,此时开发者将镜像推送到公共仓库时,一共要推送三个layer:

在这里插入图片描述

2.接下来,使用者要下载镜像,就从镜像仓库下载三个layer:
在这里插入图片描述

3.此时,三个角色拥有的内容都是一样,都是三个layer:

在这里插入图片描述

4.这时候SpringBoot开发者修改了业务代码,于是做了个新的镜像(TAG是2.0),然后推送到镜像仓库;
5.重点来了:因为只改了业务代码,因此只有业务class的layer是新的,只有这个layer会被推送到仓库,如下图:

在这里插入图片描述

6.对镜像使用者来说,如果之前下载过1.0的镜像,此时要用2.0镜像的话,只要从仓库下载最新的业务class的layer即可:

在这里插入图片描述

7.最终结果如下,公共仓库和镜像使用者都已最小的代价得到了2.0镜像:
在这里插入图片描述

可见,使用多个layer的镜像,在镜像的分发过程中,相比单一layer的镜像会更加高效,尤其是使用

springboot 2.0.8 分层打包

2.0.8 官网介绍
demo地址

可执行 Jar 文件结构

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar

应用程序类应放置在嵌套的“BOOT-INF/classes”目录中。依赖项应该放在嵌套的“BOOT-INF/lib”目录中。

Spring Boot 的“JarFile”类

用于支持加载嵌套 jar 的核心类是 org.springframework.boot.loader.jar.JarFile. 它允许您从标准 jar 文件或嵌套的子 jar 数据加载 jar 内容。首次加载时,每个位置都JarEntry映射到外部 jar 的物理文件偏移量,如下例所示:

myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
||     A.class      |||  B.class  |  C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
 ^                    ^           ^
 0063                 3452        3980

前面的示例显示了如何在 at 位置A.class找到。实际上可以从嵌套的 jar 中找到 at position和is at position 。/BOOT-INF/classes``myapp.jar``0063``B.class``myapp.jar``3452``C.class``3980

有了这些信息,我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目。我们不需要解压存档,也不需要将所有入口数据读入内存。

jar zip打包执行

某些 PaaS 实现可能会选择在运行之前解压缩存档。例如,Cloud Foundry 就是这样运作的。您可以通过启动适当的启动程序来运行解压的存档,如下所示:

$ unzip -q myapp.jar 
$ java org.springframework.boot.loader.JarLauncher

demo操作过程

pom
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>ZIP</layout>
                </configuration>
            </plugin>
        </plugins>
    </build>

打包后

windows本地运行解压jar

 java org.springframework.boot.loader.JarLauncher
docker

Dockerfile 文件官网介绍

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.liuhm.SpringbootdemoApplication"]

文件目录:
在这里插入图片描述

优化后的Dockerfile

# 指定基础镜像,这是分阶段构建的前期阶段
FROM openjdk:8-jdk-alpine as builder
# 执行工作目录
WORKDIR target
# 配置参数
ARG JAR_FILE=target/*.jar
# 将编译构建得到的jar文件复制到镜像空间中
COPY ${JAR_FILE} application.jar 
RUN unzip application.jar


FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=builder ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=builder ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.liuhm.SpringbootdemoApplication"]

报错
在这里插入图片描述

改成

ENTRYPOINT java ${JAVA_OPTS} -cp app:app/lib/* com.liuhm.SpringbootdemoApplication 
打包使用 --no-cache

在服务器上

unzip -q app.jar
docker build --no-cache -t 192.168.0.88/magic/test:1 .
docker build -t 192.168.0.88/magic/test:1 .
docker login 192.168.0.88  -u admin -p hcloud1234!
docker push 192.168.0.88/magic/test:1
下载日志

在这里插入图片描述

历史记录
在这里插入图片描述

打包不用 --no-cache

在这里插入图片描述

拉取对比

docker pull 192.168.0.88/magic/test:1

有三层layer存在,是基础镜像

docker pull 192.168.0.88/magic/test:2

有四层layer存在,是基础镜像和test:1的lib是一样的

在这里插入图片描述

导入导出对比

docker save -o ./apollo.tar 192.168.0.88/magic/test:2 192.168.0.88/magic/test:1

docker load - i apollo.tar
在这里插入图片描述

springboot 2.3 分层打包

springboot 2.3以前的可以按照springboot 2.0.8的方式进行分层

demo地址

2.3.0官网介绍

可执行 Jar 文件结构

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar

应用程序类应放置在嵌套的“BOOT-INF/classes”目录中。依赖项应该放在嵌套的“BOOT-INF/lib”目录中。

Spring Boot 的“JarFile”类

用于支持加载嵌套 jar 的核心类是 org.springframework.boot.loader.jar.JarFile. 它允许您从标准 jar 文件或嵌套的子 jar 数据加载 jar 内容。首次加载时,每个位置都JarEntry映射到外部 jar 的物理文件偏移量,如下例所示:

myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
||     A.class      |||  B.class  |  C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
 ^                    ^           ^
 0063                 3452        3980

前面的示例显示了如何在 at 位置A.class找到。实际上可以从嵌套的 jar 中找到 at position和is at position 。/BOOT-INF/classes``myapp.jar``0063``B.class``myapp.jar``3452``C.class``3980

有了这些信息,我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目。我们不需要解压存档,也不需要将所有入口数据读入内存。

jar zip打包执行

某些 PaaS 实现可能会选择在运行之前解压缩存档。例如,Cloud Foundry 就是这样运作的。您可以通过启动适当的启动程序来运行解压的存档,如下所示:

$ unzip -q myapp.jar 
$ java org.springframework.boot.loader.JarLauncher

demo操作过程

pom
 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.0.RELEASE</version>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
            </plugin>
        </plugins>
    </build>
pom.xml中spring-boot-maven-plugin插件新增的参数
  1. pring-boot-maven-plugin插件新增参数如下图所示:
    在这里插入图片描述

2.上述参数有啥用?我这边编译构建了两次jar,第一次有上述参数,第二次没有,将两次生成的jar解压后对比,发现用了上述参数后,生成的jar会多出下图红框中的两个文件:
在这里插入图片描述

3.看看layers.idx文件的内容,如下图:
在这里插入图片描述

4.上图中的内容分别是什么意思呢?官方已给出了详细解释,如下图红框:
在这里插入图片描述

5.综上所述,layers.idx文件是个清单,里面记录了所有要被复制到镜像中的信息,接下来看看如何使用layers.idx文件,这就涉及到jar包中新增的另一个文件:spring-boot-jarmode-layertools-2.3.0.RELEASE.jar

spring-boot-jarmode-layertools工具

1.前面已经介绍过jar中除了layers.idx,还多了个文件:spring-boot-jarmode-layertools-2.3.0.RELEASE.jar ,来看看这个文件的用处;
2.进入工程的target目录,这里面是编译后的jar文件(我这里文件名为dockerlayerdemo-0.0.1-SNAPSHOT.jar),注意此时的spring-boot-maven-plugin插件是带上了下图红框中的参数的:
在这里插入图片描述

3.执行以下命令:

java -Djarmode=layertools -jar springboot2_3_0-1.jar list

4.得到结果如下图所示,是layers.idx文件的内容:
在这里插入图片描述

5.来看看官方对这个layertools的解释,list参数的作用上面我们已经体验过了,重点是红框中的extract参数,它的作用是从jar中提取构建镜像所需的内容:
在这里插入图片描述

6.看到这里,jar构建生成清单layers.idx,Dockerfile中根据清单从jar提取文件放入镜像:

打包后
docker

Dockerfile

# 指定基础镜像,这是分阶段构建的前期阶段
FROM openjdk:8-jdk-alpine as builder
# 执行工作目录
WORKDIR application
# 配置参数
ARG JAR_FILE=target/*.jar
# 将编译构建得到的jar文件复制到镜像空间中
COPY ${JAR_FILE} application.jar
# 通过工具spring-boot-jarmode-layertools从application.jar中提取拆分后的构建结果
RUN java -Djarmode=layertools -jar application.jar extract

# 正式构建镜像
FROM openjdk:8-jdk-alpine
WORKDIR application
# 前一阶段从jar中提取除了多个文件,这里分别执行COPY命令复制到镜像空间中,每次COPY都是一个layer
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"]
打包使用 --no-cache

在服务器上

unzip -q app.jar
docker build -t 192.168.0.88/magic/test:1 .
docker login 192.168.0.88  -u admin -p hcloud1234!
docker push 192.168.0.88/magic/test:1

SpringBoot-2.3.0.RELEASE推荐的镜像构建方案和旧版本相比有什么不同

1.pom.xml中的spring-boot-maven-plugin插件增加一个配置项;
2.构建好jar后,旧版本要自己解压jar,新版不需要;
3.新版本的jar中,多了个文件清单layers.idx和镜像文件处理工具spring-boot-jarmode-layertools-2.3.0.RELEASE.jar;
4.旧版的Dockefile内容:因为前面解压好了,所有在Dockerfile里直接复制前面解压的内容,这里就有个风险:前一步解压和当前复制的文件位置要保证一致;
5.新版的Dockerfile内容:使用工具spring-boot-jarmode-layertools-2.3.0.RELEASE.jar,根据的layers.idx内容从jar中提取文件,复制到镜像中;
6.新版的Dockerfile中,由于使用了分阶段构建,因此从jar提取文件的操作不会保存到镜像的layer中;

pom.xml中spring-boot-maven-plugin插件新增的参数,到底做了什么

spring-boot-maven-plugin插件新增的参数,使得编译构建得到jar中多了两个文件,如下图所示:
在这里插入图片描述

Dockerfile中,java -Djarmode=layertools -jar application.jar extract这个操作啥意思

  1. java -Djarmode=layertools -jar application.jar extract的作用是从jar中提取文件,这些文件是docker镜像的一部分;
  2. 上述操作的参数是extract,另外还有两个参数,官方解释它们的作用如下:
    在这里插入图片描述

至此,问题已全部澄清,大致流程图,帮助您快速理解整个构建流程:
在这里插入图片描述

(重点)shell脚本获取增量的docker镜像

观察不同

两个不同版本的镜像,发现有4个相同的包,其他的就是不同的,所以去除相同的,打包不同即可完成效果
在这里插入图片描述

shell

实现过程

1、定义需要区分的两个版本的所有镜像名字

2、拉取两个版本的所有镜像

3、分别打包两个版本的镜像,并且解压到对应的文件夹下

4、读取里面的文件找出不同的,删除相同的

5、打包删除后的文件

6、增量包很小,导入测试成功!

#!/bin/sh
set -e

# 当前目录
CURRENT_DIR=$(
   cd "$(dirname "$0")"
   pwd
)
nowDate=$(date +'%Y%m%d%H%M')




# 旧版本镜像 中间空格分割
oldImages=("192.168.0.88/magic/test:1")
# 新版本镜像 中间空格分割
newImages=("192.168.0.88/magic/test:2")
# 导出包的名字
outPutName="test2.tar.gz"

# 是否拉取镜像
isPullImages=false

# 拉取镜像
pullImages(){
	if [[ $isPullImages == "true" ]]; then
		echo "拉取旧版本镜像"
		for oldImage in ${oldImages[*]}
		do
			docker pull ${oldImage}
		done
		
		echo "拉取新版本镜像"
		for newImage in ${newImages[*]}
		do
			docker pull ${newImage}
		done
		
		echo "拉取镜像结束"
	fi
}

packageImage(){
	path=$1
	flag=$2

	images=()
	mkdir -p $path && cd $path
	if [[ $flag == "old" ]];then
		images=${oldImages[*]}
	else
		images=${newImages[*]}
	fi
	echo "打包 镜像${images[*]}" 
	docker save -o  $path/images.tar ${images[*]} 
	tar -xf images.tar  -C . && ls -l |grep ^d  > /dev/null && rm -rf images.tar
}

checkFile(){
	oldPath=$1
	newPath=$2
	oldFileNams=$(ls $oldPath)
	newFileNams=$(ls $newPath)
	
	for newImage in $newFileNams
	do
		if [[ "${oldFileNams[@]}"  =~ "${newImage}" ]] && [[ "$newImage" != "repositories" ]]&& [[ "$newImage" != "manifest.json" ]] && [[ "$newImage" !=  *.json ]]
		then
			rm -rf $newPath/$newImage
			echo "相同 $newPath/$newImage"
		else
			echo "不相同 $newImage"
		fi
	done
	cd $newPath && tar -zvcf $CURRENT_DIR/$outPutName *
	rm -rf $newPath
	rm -rf $oldPath
}

main(){
	# 拉取镜像
	pullImages
	
	oldPath=$CURRENT_DIR/oldImages
	newPath=$CURRENT_DIR/newImages
	
	# 打包旧的
	packageImage $oldPath old
	# 打包新的
	packageImage $newPath new
	# 检查不同的,删除相同的,打包新的
	checkFile $oldPath $newPath
	
}

main


注意事项

1、Dockerfile中的层变换位置后,就不会使用缓存,会变成新的构建了

2、当发现有三层怎么操作都没有用缓存,如图

在这里插入图片描述
说明该lib一直在变,每打一次jar,jar就会变,所以将META-INF移到上面一层,

将lib变化的jar移到另一个文件夹

Dockerfile如下

# 指定基础镜像,这是分阶段构建的前期阶段
FROM openjdk:8-jdk-alpine as builder
# 执行工作目录
WORKDIR target
# 配置参数
ARG JAR_FILE=target/*.jar
# 将编译构建得到的jar文件复制到镜像空间中
COPY ${JAR_FILE} application.jar
# 将企业jar,多模块的其他依赖jar 每次重新打包的jar移动到另外一个目录
RUN unzip application.jar && mkdir -p BOOT-INF/lib2 && mv BOOT-INF/lib/scs-*.jar BOOT-INF/lib2


FROM openjdk:8-jdk-alpine
VOLUME /tmp
ARG DEPENDENCY=target
COPY --from=builder ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib /app/lib
#每次都不一样的jar
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib2 /app/lib
COPY --from=builder ${DEPENDENCY}/BOOT-INF/classes /app
COPY java_agent-1.jar /app/java_agent-1.jar

ENTRYPOINT java $JAVA_OPTS -verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/opt/logs/jvm/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/logs/jvm/dump.hprof -Djava.security.egd=file:/dev/./urandom -Denv=dev -Duser.timezone=GMT+08 -cp $AGENT_AFTER_JAR app:app/lib/* com.liuhm.SpringbootdemoApplication

dockerfile-maven支持cache-from

dockerfile-maven 目前的官方版本说是1.4.5以后都支持cacheFrom,实际操作不支持

在这里插入图片描述

下载源码进行修改

更新 BuildMojo.java 文件

if (!cacheFromExistLocally.isEmpty()) { 
	buildParameters.add(new DockerClient.BuildParam("cache-from", encodeBuildParam(cacheFromExistLocally))); 
}

修改为

if (!cacheFromExistLocally.isEmpty()) { 
	buildParameters.add(new DockerClient.BuildParam("cachefrom", new Gson().toJson(cacheFromExistLocally).toString())); 
}

修改原因如下:api接口参数是cachefrom
https://docs.docker.com/reference/api/engine/version/v1.40/#tag/Image/operation/ImageBuild
在这里插入图片描述

注释plugin里面的pom

<!--<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-invoker-plugin</artifactId>
        <version>1.9</version>
        <dependencies>
          <dependency>
            <groupId>com.spotify</groupId>
            <artifactId>docker-client</artifactId>
            <version>${docker-client.version}</version>
          </dependency>
        </dependencies>
        <configuration>
          <cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
          <pomIncludes>
            <pomInclude>*/pom.xml</pomInclude>
          </pomIncludes>
          <postBuildHookScript>verify</postBuildHookScript>
          <localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
          <settingsFile>src/it/settings.xml</settingsFile>
          <streamLogs>true</streamLogs>
          <goals>
            <goal>clean</goal>
            <goal>verify</goal>
          </goals>
        </configuration>
        <executions>
          <execution>
            <id>integration-test</id>
            <goals>
              <goal>install</goal>
              <goal>integration-test</goal>
              <goal>verify</goal>
            </goals>
          </execution>
        </executions>
      </plugin>-->

打包上传私库

测试

说明成功
在这里插入图片描述

借鉴博客

详解SpringBoot(2.3)应用制作Docker镜像(官方方案)
如何从Docker Registry中导出镜像
docker 如何导出某个镜像增量部分?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liuhm~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值