ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

离线环境Docker镜像传输太慢?一个脚本只导出增量部分(省90%时间)

离线环境Docker镜像传输太慢?一个脚本只导出增量部分(省90%时间) 上周公司新项目上线离线环境需要部署3个Spring Boot服务。每个镜像2GB3个就是6GB。在政府客户现场连U盘都不让用只能刻录光盘。刻了2小时结果只有几十MB的代码改动。这种痛做政企离线部署的同行都懂。其实Docker镜像本身就是分层设计的理论上只需要传输变动的layer。但docker save 默认导出完整镜像导致我们每次都要传输几个GB的无用数据。今天分享一个Shell脚本自动对比新旧镜像只导出差异部分。实测能把6GB的传输量压缩到几十MB部署时间从2小时缩短到2分钟。问题离线生产环境下需要 docker save 镜像然后 dock load 导入。问题是 docker save 导出的是个完整的镜像当有变动时每次都传输完整镜像特别浪费时间在现场环境执行 load 时可以看出来 docker 只会导入变动的 layerdocker镜像构建原理背景​ 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此时开发者将镜像推送到公共仓库时一共要推送三个layer2.接下来使用者要下载镜像就从镜像仓库下载三个layer3.此时三个角色拥有的内容都是一样都是三个layer4.这时候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/classesmyapp.jar0063B.classmyapp.jar3452C.class3980有了这些信息我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目。我们不需要解压存档也不需要将所有入口数据读入内存。jar zip打包执行某些 PaaS 实现可能会选择在运行之前解压缩存档。例如Cloud Foundry 就是这样运作的。您可以通过启动适当的启动程序来运行解压的存档如下所示$ unzip -q myapp.jar $ java org.springframework.boot.loader.JarLauncherdemo操作过程pomparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.0.8.RELEASE/version relativePath/ !-- lookup parent from repository -- /parent build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration layoutZIP/layout /configuration /plugin /plugins /build打包后windows本地运行解压jarjava org.springframework.boot.loader.JarLauncherdockerDockerfile 文件官网介绍FROM openjdk:8-jdk-alpine VOLUME /tmp ARG DEPENDENCYtarget 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_FILEtarget/*.jar # 将编译构建得到的jar文件复制到镜像空间中 COPY ${JAR_FILE} application.jar RUN unzip application.jar FROM openjdk:8-jdk-alpine VOLUME /tmp ARG DEPENDENCYtarget COPY --frombuilder ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY --frombuilder ${DEPENDENCY}/META-INF /app/META-INF COPY --frombuilder ${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:1docker load - i apollo.tarspringboot 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/classesmyapp.jar0063B.classmyapp.jar3452C.class3980有了这些信息我们就可以通过寻找外部 jar 的适当部分来加载特定的嵌套条目。我们不需要解压存档也不需要将所有入口数据读入内存。jar zip打包执行某些 PaaS 实现可能会选择在运行之前解压缩存档。例如Cloud Foundry 就是这样运作的。您可以通过启动适当的启动程序来运行解压的存档如下所示$ unzip -q myapp.jar $ java org.springframework.boot.loader.JarLauncherdemo操作过程pomparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.0.RELEASE/version relativePath/ !-- lookup parent from repository -- /parent build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version2.3.0.RELEASE/version configuration layers enabledtrue/enabled /layers /configuration /plugin /plugins /buildpom.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.jarspring-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-Djarmodelayertools-jarspringboot2_3_0-1.jar list4.得到结果如下图所示是layers.idx文件的内容5.来看看官方对这个layertools的解释list参数的作用上面我们已经体验过了重点是红框中的extract参数它的作用是从jar中提取构建镜像所需的内容6.看到这里jar构建生成清单layers.idxDockerfile中根据清单从jar提取文件放入镜像打包后dockerDockerfile# 指定基础镜像这是分阶段构建的前期阶段 FROM openjdk:8-jdk-alpine as builder # 执行工作目录 WORKDIR application # 配置参数 ARG JAR_FILEtarget/*.jar # 将编译构建得到的jar文件复制到镜像空间中 COPY ${JAR_FILE} application.jar # 通过工具spring-boot-jarmode-layertools从application.jar中提取拆分后的构建结果 RUN java -Djarmodelayertools -jar application.jar extract # 正式构建镜像 FROM openjdk:8-jdk-alpine WORKDIR application # 前一阶段从jar中提取除了多个文件这里分别执行COPY命令复制到镜像空间中每次COPY都是一个layer COPY --frombuilder application/dependencies/ ./ COPY --frombuilder application/spring-boot-loader/ ./ COPY --frombuilder application/snapshot-dependencies/ ./ COPY --frombuilder 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:1SpringBoot-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.jar4.旧版的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 -Djarmodelayertools -jar application.jar extract这个操作啥意思java -Djarmodelayertools -jar application.jar extract的作用是从jar中提取文件这些文件是docker镜像的一部分上述操作的参数是extract另外还有两个参数官方解释它们的作用如下至此问题已全部澄清大致流程图帮助您快速理解整个构建流程重点shell脚本获取增量的docker镜像观察不同两个不同版本的镜像发现有4个相同的包其他的就是不同的所以去除相同的打包不同即可完成效果shell实现过程1、定义需要区分的两个版本的所有镜像名字2、拉取两个版本的所有镜像3、分别打包两个版本的镜像并且解压到对应的文件夹下4、读取里面的文件找出不同的删除相同的5、打包删除后的文件6、增量包很小导入测试成功#!/bin/shset-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)# 导出包的名字outPutNametest2.tar.gz# 是否拉取镜像isPullImagesfalse# 拉取镜像pullImages(){if[[$isPullImagestrue]];thenecho拉取旧版本镜像foroldImagein${oldImages[*]}dodockerpull${oldImage}doneecho拉取新版本镜像fornewImagein${newImages[*]}dodockerpull${newImage}doneecho拉取镜像结束fi}packageImage(){path$1flag$2images()mkdir-p$pathcd$pathif[[$flagold]];thenimages${oldImages[*]}elseimages${newImages[*]}fiecho打包 镜像${images[*]}dockersave-o$path/images.tar${images[*]}tar-xfimages.tar-C.ls-l|grep^d/dev/nullrm-rfimages.tar}checkFile(){oldPath$1newPath$2oldFileNams$(ls$oldPath)newFileNams$(ls$newPath)fornewImagein$newFileNamsdoif[[${oldFileNams[]}~${newImage}]][[$newImage!repositories]][[$newImage!manifest.json]][[$newImage!*.json]]thenrm-rf$newPath/$newImageecho相同$newPath/$newImageelseecho不相同$newImagefidonecd$newPathtar-zvcf$CURRENT_DIR/$outPutName*rm-rf$newPathrm-rf$oldPath}main(){# 拉取镜像pullImagesoldPath$CURRENT_DIR/oldImagesnewPath$CURRENT_DIR/newImages# 打包旧的packageImage$oldPathold# 打包新的packageImage$newPathnew# 检查不同的删除相同的打包新的checkFile$oldPath$newPath}main注意事项1、Dockerfile中的层变换位置后就不会使用缓存会变成新的构建了2、当发现有三层怎么操作都没有用缓存如图说明该lib一直在变每打一次jarjar就会变所以将META-INF移到上面一层将lib变化的jar移到另一个文件夹Dockerfile如下# 指定基础镜像这是分阶段构建的前期阶段 FROM openjdk:8-jdk-alpine as builder # 执行工作目录 WORKDIR target # 配置参数 ARG JAR_FILEtarget/*.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 DEPENDENCYtarget COPY --frombuilder ${DEPENDENCY}/META-INF /app/META-INF COPY --frombuilder ${DEPENDENCY}/BOOT-INF/lib /app/lib #每次都不一样的jar COPY --frombuilder ${DEPENDENCY}/BOOT-INF/lib2 /app/lib COPY --frombuilder ${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.egdfile:/dev/./urandom -Denvdev -Duser.timezoneGMT08 -cp $AGENT_AFTER_JAR app:app/lib/* com.liuhm.SpringbootdemoApplicationdockerfile-maven支持cache-fromdockerfile-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接口参数是cachefromhttps://docs.docker.com/reference/api/engine/version/v1.40/#tag/Image/operation/ImageBuild注释plugin里面的pom!--plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-invoker-plugin/artifactId version1.9/version dependencies dependency groupIdcom.spotify/groupId artifactIddocker-client/artifactId version${docker-client.version}/version /dependency /dependencies configuration cloneProjectsTo${project.build.directory}/it/cloneProjectsTo pomIncludes pomInclude*/pom.xml/pomInclude /pomIncludes postBuildHookScriptverify/postBuildHookScript localRepositoryPath${project.build.directory}/local-repo/localRepositoryPath settingsFilesrc/it/settings.xml/settingsFile streamLogstrue/streamLogs goals goalclean/goal goalverify/goal /goals /configuration executions execution idintegration-test/id goals goalinstall/goal goalintegration-test/goal goalverify/goal /goals /execution /executions /plugin--打包上传私库测试说明成功借鉴博客详解SpringBoot(2.3)应用制作Docker镜像(官方方案)如何从Docker Registry中导出镜像docker 如何导出某个镜像增量部分
返回列表