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 分层打包
可执行 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的方式进行分层
可执行 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插件新增的参数
- 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
# 指定基础镜像,这是分阶段构建的前期阶段
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这个操作啥意思
- java -Djarmode=layertools -jar application.jar extract的作用是从jar中提取文件,这些文件是docker镜像的一部分;
- 上述操作的参数是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 如何导出某个镜像增量部分?