扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
本篇内容介绍了“Java SpringBoot项目怎么构建Docker镜像调优”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
创新互联公司服务热线:13518219792,为您提供成都网站建设网页设计及定制高端网站建设服务,创新互联公司网页制作领域10多年,包括PE包装袋等多个方面拥有丰富的网站营销经验,选择创新互联公司,为网站保驾护航。
前言:
以前的 SpringCloud 微服务时代以 “Jar包" 为服务的基础,每个服务都打成 Jar 供服务间相互关联与调用。而 现在随着 Kubernetes 流行,已经变迁到一个镜像一个服务,依靠 Kubernetes 对镜像的统一编排进行对服务进行统一管理。在对 Kubernetes 微服务实践过程中,接触最多的肯定莫过于 Docker 镜像。由于本人使用的编程语言是 Java,所以对 Java SpringBoot 项目接触比较多,所以比较关心如何更好的通过 Dockerfile 编译 Docker 的镜像。
Kubernetes 微服务简单说就是一群镜像间的排列组合与相互间调的关系,故而如何编译镜像会使服务性能更优,使镜像构建、推送、拉取速度更快,使其占用网络资源更少这里优化,更易使用成为了一个重中之重的事情,也是一个非常值得琢磨的问题。这里我将对 SpringBoot 项目打包 Docker 镜像如何写 Dockerfile 的探究进行简单叙述。
系统环境:
Docker 版本:18.09.3
Open JDK 基础镜像版本:openjdk:8u212-b04-jre-slim
测试用的镜像仓库:阿里云 Docker Hub
项目 Github:https://github.com/my-dlq/blog-example/tree/master/springboot-dockerfile
这里将用常规 SpringBoot 编译 Docker 镜像的 Dockerfile 写法,感受下这种方式编译的镜像用起来如何。
这里准备一个经过 Maven 编译后的普通的 springboot 项目来进行 Docker 镜像构建,项目内容如下图所示,可以看到要用到的就是里面的应用程序的 Jar 文件,将其存入镜像内完成镜像构建任务。
jar 文件大小:70.86mb
构建 Docker 镜像需要提前准备 Dockerfile 文件,这个 Dockerfile 文件中的内容为构建 Docker 镜像执行的指令。下面是一个常用的 SpringBoot 构建 Docker 镜像的 Dockerfile,将它放入 Java 源码目录(target 的上级目录),确保下面设置的 Dockerfile 脚本中设置的路径和 target 路径对应。
FROM openjdk:8u212-b04-jre-slim VOLUME /tmp ADD target/*.jar app.jar RUN sh -c 'touch /app.jar' ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ENV APP_OPTS="" ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
通过 Docker build 命令构建 Docker 镜像,观察编译的时间。
由于后续需要将镜像推送到 Aliyun Docker 仓库,所以镜像前缀用了 Aliyun。
time:此参数会显示执行过程经过的时间
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 .
构建过程
Sending build context to Docker daemon 148.7MB Step 1/7 : FROM openjdk:8u212-b04-jre-slim 8u212-b04-jre-slim: Pulling from library/openjdk 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456c Status: Downloaded newer image for openjdk:8u212-b04-jre-slim --->; 7c6b62cf60ee Step 2/7 : VOLUME /tmp --->; Running in 13a67ab65d2b Removing intermediate container 13a67ab65d2b --->; 52011f49ddef Step 3/7 : ADD target/*.jar app.jar --->; 26aa41a404fd Step 4/7 : RUN sh -c 'touch /app.jar' --->; Running in 722e7e44e04d Removing intermediate container 722e7e44e04d --->; 7baedb10ec62 Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" --->; Running in 2681d0c5edac Removing intermediate container 2681d0c5edac --->; 5ef4a794b992 Step 6/7 : ENV APP_OPTS="" --->; Running in 5c8924a2a49d Removing intermediate container 5c8924a2a49d --->; fba87c19053a Step 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] --->; Running in c4cf97009b3c Removing intermediate container c4cf97009b3c ---> d5f30cdfeb81 Successfully built d5f30cdfeb81 Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m13.778s user 0m0.078s sys 0m0.153s
看到这次编译在 14s内完成。
将镜像推送到 Aliyun 仓库,然后查看并记录推送时间
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1
执行过程
The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot] cc1a2376d7c0: Pushed 2b940d07e9e7: Pushed 9544e87fb8dc: Pushed feb5d0e1e192: Pushed 8fd22162ddab: Pushed 6270adb5794c: Pushed 0.0.1: digest: sha256:dc60d304383b1441941ca4e9abc08db775d7be57ccb7c534c929b34ff064a62f size: 1583 real 0m24.335s user 0m0.052s sys 0m0.059s
看到这次在 25s内完成。
这里切换到另一台服务器上进行镜像拉取操作,观察镜像拉取时间。
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1
拉取过程
0.0.1: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete 4847672cbfa5: Pull complete b60476972fc4: Pull complete Digest: sha256:dc60d304383b1441941ca4e9abc08db775d7be57ccb7c534c929b34ff064a62f Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m27.528s user 0m0.033s sys 0m0.192s
看到这次拉取总共用时 28s内完成。
这里将源码的 JAVA 文件内容修改,然后重新打 Jar 包,这样再次尝试编译、推送、拉取过程,由于 Docker 在执行构建时会采用分层缓存,所以这是一个执行较快过程。
(1)、编译
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 . Sending build context to Docker daemon 148.7MB Step 1/7 : FROM openjdk:8u212-b04-jre-slim --->; 7c6b62cf60ee Step 2/7 : VOLUME /tmp --->; Using cache --->; 52011f49ddef Step 3/7 : ADD target/*.jar app.jar --->; c67160dd2a23 Step 4/7 : RUN sh -c 'touch /app.jar' --->; Running in 474900d843a2 Removing intermediate container 474900d843a2 --->; 3ce9a8bb2600 Step 5/7 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" --->; Running in f48620b1ad36 Removing intermediate container f48620b1ad36 --->; 0478f8f14e5b Step 6/7 : ENV APP_OPTS="" --->; Running in 98485fb15fc8 Removing intermediate container 98485fb15fc8 --->; 0b567c848027 Step 7/7 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] --->; Running in e32242fc6efe Removing intermediate container e32242fc6efe --->; 7b223b23ebfd Successfully built 7b223b23ebfd Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m3.190s user 0m0.039s sys 0m0.403s
可以看到在编译镜像过程中,前1、2层用的缓存,所以速度非常快。总编译过程耗时 4s内完成。
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot] d66a2fec30b5: Pushed f4da2c7581aa: Pushed 9544e87fb8dc: Layer already exists feb5d0e1e192: Layer already exists 8fd22162ddab: Layer already exists 6270adb5794c: Layer already exists real 0m20.816s user 0m0.024s sys 0m0.081s
可以看到只推送了前两层,其它四次由于远程仓库未变化,所以没有推送。整个推送过程耗时 21s内完成。
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 0.0.2: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists d7e364f0d94a: Pull complete 8d688ada35b1: Pull complete Digest: sha256:7c13c40fa92ec2fdc3a8dfdd3232be1be9c1a1a99bf123743ff2a43907ee03dc Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m23.214s user 0m0.053s sys 0m0.097s
本地以及缓存前四层,只拉取有变化的后两层。这个过程耗时 24s内完成。
通过这种方式对 SpringBoot 项目构建 Docker 镜像来使用,给我的感受就是只要源码中发生一点点变化,那么 SpringBoot 项目就需要将项目经过 Maven 编译后再经过 Docker 镜像构建,每次都会将一个 70M+ 的应用 Jar 文件存入 Docker 中,有时候明明就改了一个字母,可能又得把整个程序 Jar 重新存入 Docker 镜像中,然后在推送和拉取过程中,每次都得推一个大的镜像或者拉取一个大的镜像来进行传输,感觉非常不方便。
Docker 为了节约存储空间,所以采用了分层存储概念。共享数据会对镜像和容器进行分层,不同镜像可以共享相同数据,并且在镜像上为容器分配一个 RW 层来加快容器的启动顺序。
在构建镜像的过程中 Docker 将按照 Dockerfile 中指定的顺序逐步执行 Dockerfile 中的指令。随着每条指令的检查,Docker 将在其缓存中查找可重用的现有镜像,而不是创建一个新的(重复)镜像。
Dockerfile 的每一行命令都创建新的一层,包含了这一行命令执行前后文件系统的变化。为了优化这个过程,Docker 使用了一种缓存机制:只要这一行命令不变,那么结果和上一次是一样的,直接使用上一次的结果即可。
为了充分利用层级缓存,我们必须要理解 Dockerfile 中的命令行是如何工作的,尤其是RUN,ADD和COPY这几个命令。
参考 Docker 文档了解 Docker 镜像缓存:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
SpringBoot 编译成镜像后,底层会是一个系统,如 Ubantu,上一层是依赖的 JDK 层,然后才是 SpringBoot 层,最下面两层我们无法操作,考虑优化只能是 SpringBoot 层琢磨。
从上面实验中了解到之所以每次编译、推送、拉取过程中较为缓慢,原因就是庞大的镜像文件。了解到 Docker 缓存概念后就就产生一种想法,如果不经常改变的文件缓存起来,将常改动的文件不进行缓存。由于 SpringBoot 项目是经常变换的,那么应该怎么利用缓存机制来实现呢?如果强行利用缓存那么每次打的镜像不都是缓存中的旧的程序内容吗。
所以就考虑一下应用 Jar 包里面都包含了什么文件, Java 的哪些文件是经常变动的,哪些不经常变动,对此,下面将针对 SpringBoot 打的应用 Jar 包进行分析。
显示解压后的列表,查看各个文件夹大小
$ tree -L 3 --si --du . ├── [ 74M] BOOT-INF │ ├── [2.1k] classes │ └── [ 74M] lib ├── [ 649] META-INF │ ├── [ 552] MANIFEST.MF │ └── [ 59] maven └── [ 67] org └── [ 38] springframework
可以看到最大的文件就是 lib 这个文件夹,打开这个文件夹,里面是一堆相关依赖 Jar,这其中一个 Jar 不大,但是一堆 Jar 组合起来就非常大了,一般 SpringBoot 的项目依赖 Jar 大小维持在 40MB ~ 160MB。
在看看 org 文件夹,里面代码加起来才几百 KB。故此 SpringBoot 程序 Jar 包就是这些 Classes 文件和依赖的 Jar 组成,这些依赖 Jar 总共 74 MB,几乎占了这个应用 Jar 包的全部大小。
如果一个 Jar 包只包含 class 文件,那么这个 Jar 包的大小可能就几百 KB。现在要探究一下,如果将 lib 依赖的 Jar 和 class 分离,设置应用的 Jar 包只包含 class 文件,将 lib 文件夹下的 Jar 文件放在 SpringBoot Jar 的外面。
当我们写一个程序的时候,常常所依赖的 Jar 不会经常变动,变动多的是源代码程序,依赖的 Jar 包非常大而源代码非常小。仔细思考一下,如果在打包成 Docker 镜像的时候将应用依赖的 Jar 包单独设置一层缓存,而应用 Jar 包只包含 Class 文件,这样在 Docker 执行编译、推送、拉取过程中,除了第一次是全部都要执行外,再往后的执行编译、推送、拉取过程中,只会操作改动的那个只包含 Class 的 Jar 文件,就几百 KB,可以说是能够瞬间完成这个过程。所以思考一下,如何将 lib 文件夹下的依赖 Jar 包和应用 Jar 包分离开来。
经过查找很多相关资料,发现 SpringBoot 的 Maven 插件在执行 Maven 编译打 Jar 包时候做了很多事情,如果改变某些插件的打包逻辑,致使打应用 Jar 时候将 lib 文件夹下所有的 Jar 包都拷贝到应用 Jar 外面,只留下编译好的字节码文件。
将这几个 Maven 工具引入到项目 pom.xml 中
org.apache.maven.plugins maven-jar-plugin true lib/ org.springframework.boot spring-boot-maven-plugin nothing nothing org.apache.maven.plugins maven-dependency-plugin copy-dependencies prepare-package copy-dependencies ${project.build.directory}/lib
执行 Maven 命令打包 Jar
$ mvn clean install
当 Maven 命令执行完成后,查看 target 目录如下图:
然后测试下这个 Jar 文件是否能正常运行
$ java -jar springboot-helloworld-0.0.1.jar
然后看到运行日志,OK!下面将继续进行 Dockerfile 改造工作。
> 项目 Github 地址:https://github.com/my-dlq/blog-example/tree/master/springboot-dockerfile
这里修改上面的 Dockerfile 文件,需要新增一层指令用于将 lib 目录里面的依赖 Jar 复制到镜像中,其它保持和上面 Dockerfile 一致。
FROM openjdk:8u212-b04-jre-slim VOLUME /tmp COPY target/lib/ ./lib/ ADD target/*.jar app.jar RUN sh -c 'touch /app.jar' ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" ENV APP_OPTS="" ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ]
> 这里新增了一层指令,作用为将 lib 文件夹复制到镜像之中,由于 Docker 缓存机制原因,这层一定要在复制应用 Jar 之前,这样改造后每次只要 lib/ 文件夹里面的依赖 Jar 不变,就不会新创建层,而是复用缓存。
在执行编译、推送、拉取之前,先将服务器上次镜像相关的所有资源都清除掉,然后再执行。
(1)、编译
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 . Sending build context to Docker daemon 223.2MB Step 1/8 : FROM openjdk:8u212-b04-jre-slim 8u212-b04-jre-slim: Pulling from library/openjdk 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete Digest: sha256:a5bcd678408a5fe94d13e486d500983ee6fa594940cbbe137670fbb90030456c Status: Downloaded newer image for openjdk:8u212-b04-jre-slim --->; 7c6b62cf60ee Step 2/8 : VOLUME /tmp --->; Running in 529369acab24 Removing intermediate container 529369acab24 --->; ad689d937118 Step 3/8 : COPY target/lib/ ./lib/ --->; 029a64c15853 Step 4/8 : ADD target/*.jar app.jar --->; 6265a83a1b90 Step 5/8 : RUN sh -c 'touch /app.jar' --->; Running in 839032a58e6b Removing intermediate container 839032a58e6b --->; 5d877dc35b2b Step 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" --->; Running in 4043994c5fed Removing intermediate container 4043994c5fed --->; 7cf32beb571f Step 7/8 : ENV APP_OPTS="" --->; Running in b7dcfa10458a Removing intermediate container b7dcfa10458a --->; b6b332bcf0e6 Step 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] --->; Running in 539093461b59 Removing intermediate container 539093461b59 --->; d4c095c4ffec Successfully built d4c095c4ffec Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m22.983s user 0m0.051s sys 0m0.540s
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot] c16749205e05: Pushed 7fef1a146748: Pushed a3bae74bbdf2: Pushed 9544e87fb8dc: Pushed feb5d0e1e192: Pushed 8fd22162ddab: Pushed 6270adb5794c: Pushed 0.0.1: digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157 size: 1789 real 0m30.335s user 0m0.052s sys 0m0.059s
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 0.0.1: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Pull complete 04305660f45e: Pull complete bbe7020b5561: Pull complete de6c4f15d75b: Pull complete 7066947b7d89: Pull complete e0742de67c75: Pull complete Digest: sha256:e2f4db740880dbe5338b823112ba9467fedf8b27cd75572611d0d3837c80f157 Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.1 real 0m36.585s user 0m0.024s sys 0m0.092s
(1)、编译
$ time docker build -t registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 . Sending build context to Docker daemon 223.2MB Step 1/8 : FROM openjdk:8u212-b04-jre-slim --->; 7c6b62cf60ee Step 2/8 : VOLUME /tmp --->; Using cache --->; ad689d937118 Step 3/8 : COPY target/lib/ ./lib/ --->; Using cache --->; 029a64c15853 Step 4/8 : ADD target/*.jar app.jar --->; 563773953844 Step 5/8 : RUN sh -c 'touch /app.jar' --->; Running in 3b9df57802bd Removing intermediate container 3b9df57802bd --->; 706a0d47317f Step 6/8 : ENV JAVA_OPTS="-Duser.timezone=Asia/Shanghai" --->; Running in defda61452bf Removing intermediate container defda61452bf --->; 742c7c926374 Step 7/8 : ENV APP_OPTS="" --->; Running in f09b81d054dd Removing intermediate container f09b81d054dd --->; 929ed5f8b12a Step 8/8 : ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar $APP_OPTS" ] --->; Running in 5dc66a8fc1e6 Removing intermediate container 5dc66a8fc1e6 --->; c4942b10992c Successfully built c4942b10992c Successfully tagged registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m2.524s user 0m0.051s sys 0m0.493s
可以看到,这次在第 3 层直接用的缓存,整个编译过程才花了 2.5秒时间
(2)、推送
$ time docker push registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 The push refers to repository [registry.cn-beijing.aliyuncs.com/mydlq/springboot] d719b9540809: Pushed d45bf4c5fb92: Pushed a3bae74bbdf2: Layer already exists 9544e87fb8dc: Layer already exists feb5d0e1e192: Layer already exists 8fd22162ddab: Layer already exists 6270adb5794c: Layer already exists 0.0.2: digest: sha256:b46d81b153ec64321caaae7ab28da0e362ed7d720a7f0775ea8d1f7bef310d00 size: 1789 real 0m0.168s user 0m0.016s sys 0m0.032s
可以看到在 0.2s内就完成了镜像推送
(3)、拉取
$ time docker pull registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 0.0.2: Pulling from mydlq/springboot 743f2d6c1f65: Already exists b83e581826a6: Already exists 04305660f45e: Already exists bbe7020b5561: Already exists de6c4f15d75b: Already exists 1c77cc70cc41: Pull complete aa5b8cbca568: Pull complete Digest: sha256:b46d81b153ec64321caaae7ab28da0e362ed7d720a7f0775ea8d1f7bef310d00 Status: Downloaded newer image for registry.cn-beijing.aliyuncs.com/mydlq/springboot:0.0.2 real 0m1.947s user 0m0.017s sys 0m0.042s
可以看到在 2s内就完成了镜像拉取
由于网络波动和系统变化,所以时间只能当做参考,不过执行编译、推送、拉取过程的确快了不少,大部分用文件都进行了缓存,只有几百 KB 的流量交互自然速度比几十 MB 甚至几百 MB 速度要快很多。
最后说明一下,这种做法只是提供了一种参考,现在的微服务服务 Docker 镜像化以来,维护的是整个镜像而不是一个服务程序,所以关心的是 Docker 镜像能否能正常运行,怎么构建镜像会使构建的镜像更好用。
在生产环境下由于版本变化较慢,不会动不动就更新,所以在生产环境下暂时最好还是按部就班,应用原来 SpringBoot 镜像编译方式以确保安装(除非已大量实例验证该构建方法)。
“Java SpringBoot项目怎么构建Docker镜像调优”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流