一切福田,不離方寸,從心而覓,感無不通。

spring boot服务docker镜像优化之旅

缘由

我们在使用spring boot开发的服务中,一般会选择打包成单体的fatjar来发布服务,这在传统的部署方式下是非常方便的,但是当我们选择使用docker这种容器化的方式来部署应用的时候,却有一点点的不便之处,因为这个单体的jar一般都比较大,每次镜像push到仓库和从仓库拉取都需要比较长的时间。

原因是什么了?

docker的一大特色就是镜像的存储是分层的,参考下面这张官图


我们在Dockerfile中的每一个指令会对应到镜像的每一层,docker在更新镜像时,只会推送变更过的层,当它计算出来这一层的摘要和之前的版本一致时,会复用上一次打包镜像时的缓存,会极大的提高打包镜像以及镜像push/pull操作的速度。

那么问题来了,当我们springboot打包出来的单体jar的时候,每次编译这个jar都会发生变化,对应的存储层也会发生变化,push和pull操作时都需要重新推送,而且这个jar一般都不小,一个典型的应用会在100M左右,对应用部署和发布的速度会有比较大的影响。

稍作思考,很容易就能发现这个肥大的jar文件里面,大部分其实都是固定不变的各种依赖库,我们真正每次编译会变化的业务代码部分其实很小很小,可能也就只有几百KB,只要能将这两部分分离,变成docker镜像中的两层,一定能极大的提升镜像发布的速度

牛刀小试

首先拿来动手尝试的是一个springboot admin的项目,项目的结构是这样的:


使用最常见的打包方式:

会生成一个32M的jar文件,优化之前的Dockerfile非常简单:

 


可以看到这种方式在构建v2版本的镜像时,会重新copy整个完整的jar

如果要拆开这个单体的jar,有两种方式,一是修改mvn打包的配置,将lib包放在独立的文件夹下,在这里我们考虑到项目众多,尽量减少修改,选择了在Docker打包镜像时,解压打包出来的jar包,将其中的内容分开来copy,修改后的Dockerfile如下:

 

看看修改后的效果:


在copy lib目录时,是直接using cache的。来看看push的时候效果对比 首先是优化前的push:


可以看到在push v2的时候还是会push一个33MB的层,虽然其实我们一行代码没有修改。

然后是优化后的:


可以看到这一次仅仅只推送了一个13KB的层,推送的速度快了非常多,同理也可以想象的到,我们在拉取镜像更新版本时速度会快很多。

路遇荆棘

在针对springboot-admin这个最简单的项目的优化取得很好的效果之后,就开始准备照搬到其他的项目中,没想到同样的方式怎么折腾都无效,分离之后的lib目录依然会每次需要全量重新push。出问题的项目结构大概是这样的:


一个常见的多模块mvn项目,有common,domain,rest-client,rest-server 这4个子模块,其中rest-server会依赖common和domain这两个子模块,打包出来的jar是在rest-server这个模块中。

究竟是什么鬼了?

终得正果

苦苦思索一番之后,lib目录既然不能复用上一次的cache,那一定是因为里面的内容有变化,遂将jar包解压,进到lib目录,真凶果然在此:


项目自身的3个子模块在每次编译的时候也会做为jar包放到lib目录下,这3个jar包每次编译都会有变化,所以导致这一层的cache失效。

找到问题之后,解决的思路就很简单了,将这种jar单独copy到一个目录下即可,修改后的Dockerfile如下:

 

这样修改之后效果就和上面单模块的项目一样了,至此,基本完成了springboot项目的docker镜像优化,在jenkins的流水线上可以将原来镜像push的时间从1分钟以上优化到10s左右

未来之路

在整个优化的过程中,发现springboot2.3 M1版本已经有针对性的优化方案,增加了LAYERED_JAR的打包格式,未来可期。

具体可参考下文: www.jdon.com/53738

注: 本文中举例的两个项目案例,可在github上找到:github.com/yishh/sprin…

作者:thor_lee
链接:https://juejin.cn/post/6844904119338008583
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。