我司有个 Spring Boot 应用的部署方式是这样的:
java -jar app.jar
启动应用因为应用依赖很多,打出来的 jar 包体积巨大,非常的 fat,有 243MB 之多。 公司的网又比较慢,scp 这样的庞然大雾,非常耗时,每次上线光上传 jar 包就要 2 分半,非常折磨人。
大体看了下,应用本身比较简单,加上依赖之后能有这么大体积真是匪夷所思。
首先,我把打出来的 jar 包 unzip 一下,在 BOOT-INF/lib
目录下有 237 个 jar 包:
% ls BOOT-INF/lib | wc -l
237
然后看看依赖的最大的 jar 包有哪些:
% ls -lhS BOOT-INF/lib | head -n 5
total 493192
-rw-r--r--@ 1 xyzwps staff 102M Dec 1 2023 opencv-4.8.1-0.jar
-rw-r--r--@ 1 xyzwps staff 13M May 16 2020 Happy-Captcha-1.0.1.jar
-rw-r--r--@ 1 xyzwps staff 8.6M Jun 16 12:10 byte-buddy-1.17.6.jar
-rw-r--r--@ 1 xyzwps staff 8.5M Jun 3 17:31 bcprov-jdk18on-1.81.jar
可以看到 opencv 这个 jar 包占用了最大的体积,占了一小半。要是能把它去掉,应用的体积可以减小一小半左右。
处理这个问题的思路是这样的:
应用的依赖不会经常升级,所以可以考虑提前把这些巨型一来提前放到应用服务器的
CLASSPATH
下,日后升级只要升级变动的部分即可。
最开始我的想法是在 pom.xml 中直接把依赖排除掉。操作了一下,发现有点复杂。于是,我去 Spring Boot 官网看看有关打包的文档,看看有没有什么好的做法。
很幸运,在 Efficient Deployments 这一章,第一节就是关于如何解包 fat jar 包的,执行以下命令就可以解包了:
% java -Djarmode=tools -jar my-app.jar extract
执行完以上命令后,可以得到这样的目录结构:
my-app
├── lib
│ ├── Happy-Captcha-1.0.1.jar
│ ├── ...
│ └── xxl-job-core-3.1.0.jar
└── my-app.jar
即把 my-app.jar
这个 fat jar 解包成 my-app/my-app.jar
这个 thin jar 包 + my-app/lib
这个目录下的所有的依赖 jar 包。
现在做法就很明了了
部署的时候只要这样就可以了:
rsync
把 my-app/lib
目录下所有的依赖 jar 包同步到应用服务器上 $WORKDIR/app/lib
目录下
只有第一次的时候需要全量同步,以后只需要同步变动的依赖 jar 包即可。多数情况下,依赖的 jar 包保持稳定,完全没有数据传输。
下面是我使用的
rsync
命令:# 按文件大小和 checksum 是否改变来判断是否需要更新,而非按时间戳 rsync -r --ignore-times --checksum --itemize-changes --progress --delete my-app/lib/ $USER@$HOST:$WORKDIR/app/lib
scp
把 my-app/my-app.jar
这个 thin jar 包同步到应用服务器上 $WORKDIR/app
目录下java -jar app.jar
启动应用解包后的 my-app/my-app.jar
体积为 2.4MB,体积减小到了原来的 1%!!! 现在部署体验就欻欻快了。
解包之后后的目录结构非常适合打 docker 镜像。合理地利用 docker 镜像 layer 缓存,可以极大地打 docker 镜像的时间。
Quarkus 这样的框架打包一早就不是 fat jar……