更多精彩内容,欢迎观看:
《Java应用提速(速度与激情)》——一、maven构建提速(上):https://developer.aliyun.com/article/1223856?spm=a2c6h.13148508.setting.14.68ac4f0elVtcZX
为保证依赖包的准确性,需要将.m2隔离.即每个pod都有一个独立的.m2来volume。
(架构2.0:按pod隔离.m2)
虽然这样会浪费一些磁盘,但准确性就能得到保障。但产生了同一个仓库的同一个依赖会在同一个node上重复下载的问题,即使2.0图示的二个pod是同一个应用的并发构建,但在同一个node上面下载maven-compiler-plugin:3.8.1时,要下载二次。
所以我们继续架构演进,来按app来隔离。
(架构3.0:按app隔离.m2)
但还是会存在同一个node上重复下载同一个仓库的同一个依赖文件,因为appA与appB,它们用的是同一个仓库。因为在一个node上,是不会知道在其中运行的mvn build的,是同一个应用?或不同应用但相同仓库?或不同应用不同仓库?
所以我们得继续演进,不按应用而按仓库来隔离.m2。
(架构4.0:按repo隔离.m2)
这架构看上去感觉还可以,但解决不了一个应用依赖多个仓库的问题。
有些应用有些复杂,它会在maven构建的仓库配置文件settings.xml(或pom文件)中指定下载多个仓库。因为这应用的要下载的依赖的确来自多个仓库。当指定多个仓库时,下载一个依赖包,会依次从这多个仓库查找并下载。
虽然maven的settings.xml语法支持多个仓库,但localRepository却只能指定一个。所以要看下docker是否支持将多个目录volume到同一个容器中的目录,即上图中的红线,但初步看了docker官网文档,并不支持。
那是否可以用overlay的方式?即使可行,但overlay时谁在lower,谁在upper,这个得根据settings.xml中指定的多个仓库的顺序来才行。于是得在启pod构建前要解析下这应用的settings.xml,稍嫌复杂。
为解决按仓库隔离.m2,且应用依赖多个仓库时的问题,我们现在通过对amaven的优化来解决。
(架构5.0:repo_mirror)
当amaven执行mvn build时,当一个依赖包不在本地.m2目录,而要下载时,会先到repo_mirror中对应的仓库中找,如找到,则从repo_mirror中对应的仓库中将包直接复制到.m2,否则就只能到远程仓库下载,下载到.m2后,会同时将包复制到repo_mirror中对应的仓库中。
通过repo_mirror可以实现同一个node上只会下载一次同一个仓库的同一个文件。
当然如果有合适的共享存储,多个node共享一个存储服务,那就能解决多个node只下载一次同一个仓库的同一个文件了。
a) SNAPSHOT版本号缓存
其实在amavenServer的缓存中,除了依赖树,还缓存了SNAPSHOT的版本号。
我们的应用会依赖一些snapshot包,同时当我们在mvn构建时加上-U就会去检测这些SNAPSHOT的更新,而在apache-maven中检测SNAPSHOT需要多次请求maven仓库,会有一些网络开销。
现在我们结合maven仓库作了优化,从而让多次请求maven仓库,换成了一次cache服务直接拿到SNAPSHOT的最新版本。
1) 增量
增量是与缓存息息相关的,增量的实现就是用缓存。maven的开放性是通过插件机制实现的,每个插件实现具体的功能,是一个函数。当输入不变,则输出不变,即复用输出,而将每次每个函数执行后的输出缓存起来。
上面讲的依赖树缓存,也是maven本身(非插件)的一种增量方式。
要实现增量的关键是定义好一个函数的输入与输出,即要保证定义好的输入不变时,定义好的输出肯定不变。每个插件自己是清楚输入与输出是什么的,所以插件的增量不是由amaven统一实现,而是amaven提供了一个机制。如一个插件按约定定义好了输入与输出,则amaven在执行前会检测输入是否变化,如没变化,则直接跳过插件的执行,而从缓存中取到输出结果。
但其实一个插件的输入与输出要定义清楚并没那么简单,我们拿maven-compiler-plugin来说。maven-compiler-plugin简单地说,它的输入是src/java目录,输出是classes目录。但有些场景即使src/java没变,classes也不能复用。如一个project有二个module:A与B。B中有一个Java文件引用了A的一个Java文件的常量。A修改了一个Java中的一个常量,A会重新compile。但B没修改Java文件,但B也要重新compile。
除了常量外,还有一些ABI兼容性的问题也要考虑。module B依赖A,B调用了A中的一个方法foo(String[] args),当A将方法签名修改成foo(String... args),B不用修改对应的调用foo方法的类,而需要重新编译,否则运行时会报找不到方法。
一个module执行一个插件,是否能用增量,不只是考虑自己module的变化情况,还要考虑其它module及直接依赖或间接依赖的变化情况,这会让增量的实现有一定的挑战性。需要不断的校正输入。
但增量的效果是明显的,如依赖树缓存与算法的优化能让maven构建从10分钟降到2分钟,那增量则可以将构建耗时从分钟级降到秒级。
而gradle与bazel能达到修改一个Java文件,几秒内完成编译,就是增量效果的体现。当然它们也有定义清晰与准确输入的挑战性。
2) daemon与分布式
daemon是为了进一步达到10秒内构建的实现途径。
maven也是Java程序,运行时要将字节码转成机器码,而这转化有时间开销。虽这开销只有几秒时间,但对一个mvn构建只要15秒的应用来说,所占比例也有10%多。为降低这时间开销,可以用JIT直接将maven程序编译成机器码,同时mvn在构建完成后,不退出,常驻进程,当有新构建任务来时,直接调用mvn进程。
一般,一个maven应用编译不会超过10分钟,所以,看上去没必要将构建任务拆成子任务,再调度到不同的上执行分布式构建。因为分布式调度有时间开销,这开销可能比直接在本机上编译耗时更大,即得不偿失。
所以分布式构建的使用场景是大库。为了简化版本管理,将二进制依赖转成源码依赖,将依赖较密切的源码放在一个代码仓库中,就是大库。当一个大库有成千上万个module时,则非用分布式构建不可了。使用分布式构建,可以将大库几个小时的构建降到几分钟级别。