摘要:本文整理自杭州有赞科技有限公司中间件技术专家、Apache Dubbo PMC华钟明在 Community Over Code 2023 大会上的分享。本篇内容主要分为五个部分:
云计算时代比较显著的特点包括:
那么在云计算时代,Java应用存在哪些问题呢?
在Serverless场景上,Java的问题会尤为突出,因为Serverless不仅能简化开发场景和开发体验,还能做到极致的弹性,甚至是秒级的弹性。
上图是Datalog统计的Fast和AWS两个产品。Java语言虽然更流行,但相较于Python和Node.JS,它的占比还是比较低的。Java本身在Serverless层面,比如在做容器的调度、镜像的下载的时候,启动时间、冷启动的时间、预热时间等等,都会影响Serverless场景下它弹性扩容的时间。
下面介绍一下GraalVM,它是可以把Java应用提前编译到独立的二进制包内,这些二进制包相对于跑在JVM上它可以更小,更快的启动,不需要预热就能够达到极限的峰值,还可以减少内存和CPU的占比。
可以看到它的介绍和Java语言的应用所涉及到的问题都一一对应。GraalVM应该算是JDK的"超集",除了包含完整的JDK发行版本外,还有GraalVM Compiler、Native image、Truffle等,甚至还涉及到多语言汇编的能力。
总结一下,GraalVM本身涉及两部分,JIT和AOT。
上图左侧是一张Java生命周期的全景图。可以看到,它从JVM的启动,再到Java的main函数的启动,再到Java的应用预热,再到它的稳定期,最后到达效果,这是Java完整的生命周期的呈现。
而AOT的区别在于,它没有红色的VM。另外,JIT相对于AOT而言是没有的,也没有浅绿色的解释器。所以AOT对于JIT来说,只有内加载,GC以及它能够瞬间达到应用的稳定期。
根据右侧的图可以看出:
我们在编码之后,Soft Code的产物形态新增了。
第一种是我们传统认知上的Jar包形态,比如mvn、clean、package。第二种是Docker Image,它能轻松的帮我们直接打到镜像里面去,不用写dockerfile等文件。第三种是我们集成GraalVM后新产生的一种Native可执行文件的形态。这种形态无需JDK的环境就能启动,它能像GO一样把二进制文件直接启动。
上图的跑分都是基于4c16g的micro24的系统上跑出来的。左边和右边的区别是,左边的Provider端提供了一个Dubbo服务跑出来的,右边是提供了一个调用者的身份跑分出来的。从左边这张图可以看到,Native的可视性文件比Jar包方式的启动耗时降低了12倍+,在客户端应用,它的启动耗时降低了11倍+。所以在刚刚提到的Serverless场景上,它能提供一个非常好的启动速度。在扩容的时候能够达到秒级,甚至达到毫秒级。
上图也是跑分跑出来的数据,可以看到Consumer和Provider端通过静态化执行文件执行后,比都为Jar包的情况,第一次调用的耗时降低6倍。这第一次调用代表的是预热的时长,以及第一次需要解析的类,包括资源的情况等等。这让我们在Serverless场景下能够瞬间达到性能峰值。
上面有左右两张图,在Dubbo应用的基础上,它的内存损耗也降低大概3.5倍。Native静态化执行文件可以做到60M的内存占比,在客户端它的内存损耗也大概降低了4倍。
这是Dubbo在Native Image技术场景中做的努力:
首先需要安装Dubbo Native Image,这里就不过多介绍了,大家可以根据官方的文档进行下载。
然后安装插件,可以看到上图中有三个插件需要安装,但和Dubbo相关的只有一个,是Dubbo Maven Plugin。除此之外,还有Spring Boot 的Maven Plugin,它提供的是Spring的AOT的能力。如果是API的接入方式我们无需加这个插件,如果是XML和注解就还需要加这个插件。第三个是GraalVM提供的插件,可以看到这里加了一个reachabilitu-metadata的执行,这个后面会介绍到。
然后配置依赖,现在Dubbo关于可达性元数据的扫描、执行,以及配置文件和Source code的生成都在Dubbo Native的依赖下面,我们需要引入这个依赖。
另外,注解和XML的还是需要接入一个Spring6的兼容的包。因为Dubbo现在还兼容JDK 8的版本,而Spring6发布后支持的最低版本是JDK 17,所以我们还是需要有这么一个模块做一下兼容。
最后还需要加一个插件和依赖,然后就能轻松的把应用转化成Native Image的形式。
最下面是完整的代码示例,大家感兴趣的话,可以尝试一下编译打包,看一下执行的效果。
首先看一下Dubbo集成GraalVM Native Image技术的发展历程。2021年6月我们发布了Dubbo 3.0版本,初步探索了Native Image技术,提供了一个实验性的版本和Demo,涉及到的问题包括以下三个。
2022年11月Spring6+Spring Boot3正式发布了,它把Native Image技术作为发布的亮点展现给大家。但Dubbo社区认为我们应该对Native Image技术有一个新的跨越,所以在2023年4月发布了Dubbo 3.2版本,将Native Image技术进行了思考和重构。支持以下四个方面:
2023年12月将会发布Dubbo 3.3x版本。它基于Spring6,完成对XML和注解方式的支持,还移除了dubbo-native-plugin。
刚刚提到了非常多次Reachability Metadata可达性的元数据,到底是什么呢?
我们先来聊一聊AOT的局限性,它本身就有局限,它遵循封闭世界的一个假设。比如它只关注能看到的所有字节码的信息,这就会带来一个问题,Java动态语言的功能就不再支持了。也及时说JNI、Java反射、动态代理、ClassPath资源的获取等都不再支持。
我们知道,在组件/业务开发/使用产品上涉及到的这些功能是非常多的,既然GraalVM出了这个功能,当然也考虑到了这个问题,所以它利用Reachability Metadata的能力解决了这些问题。
右边的这张图分为了五类,它涉及到了JNI、反射、序列化、resource、proxy相关的元数据的配置信息。它可以让开发者在编译前就提供好这些元数据信息,提前打包到可执行的二进制文件中。还有第六种分类,是预定义的类型,他需要提供完整的字节码Hash值,所以就没列出来,它可能需要和Tracing的agent联合起来使用。
下面我们主要介绍一下这五类。
第一,GraalVM 提供了Tracing Agent来辅助应用采集Reachability Metadata,也就是说在运行期间它会去采集你的行为,比如你要用到反射,他会把反射的元素自动生成出来,然后生成出一个配置文件提供给你。但是并无法确保把所有都采集完整。
第二,GraalVM提供了Reachability Metadata Repository,用于管理三方组件的Reachability Metadata。我们在Java反射、动态代理这种纯业务的场景,用的相对较少,在运用到组件中相对多一点。举个例子,比如Native里用到反射,我们可以在仓库中直接找到Native反射的metadata。然后通过刚刚提到三个插件中的Native自身提供的插件,它会把仓库里的元数据直接打在二进制包里,我们也就不需要关心这些公共组件的元数据信息了。
和Dubbo相关的元数据信息包括以下五类:
第一,反射的元数据。
第二,resource的元数据。如果业务上要做扩展,配置文件,resource的元数据主要涉及的就是META-INF下的三个路径。此外,还有一个是在Dubbo 3里支持对安全性的增强,序列化的黑白名单的resources的配置。
第三,序列化的元数据。作为rpc调用的框架,它的接口定义、方法定义、内外部分的服务、parameter、请求参数、返回类型等都需要用到序列化的接口。
第四,动态代理的元数据。在传统的rpc框架里动态代理是用的比较多的产品。内外部分服务的引用就是代理的元数据。
第五,JNI的元数据,暂时没有用到。
这是Dubbo 相关的 Reachability Metadata 总结和处理策略,我们把它分为了四类。
这是AOT的执行流程。可以看到它的源码在编译之后,它的区别是它会从main函数开始启动Java应用进程,然后去找所有的source code和Bean的元数据信息。
下面是Spring Server自动生成的一个source code,它会把它生成一个Bean定义的获取的类。
而Dubbo并不是从main函数启动的,它启动了一个扫描的进程,把确定性的、不确定性的元素以及规律性的内容扫描进来,自动帮大家生成元数据信息。
下面是用Dubbo service生成后的一些信息。
上图是Spring本身提供的一个产物的内容以及Dubbo的AOT产物的内容。可以看到Dubbo下面是Adaptive的一些source code。最后Native在执行的时候会读取到这里所有的配置。
这是Dubbo和AOT之间的边界,可以看到API的接入方式和注解、XML的接入方式是有所区别的。注解和XML借用了Spring AOT的能力,包括ServiceBean、ReferenceBean等等,而Dubbo AOT的能力主要是自身的元数据的生产。
Dubbo在3.0之后提供了CTL、脚手架、IDEA插件,Dubbo Native Image目前还在建设中,之后也会加进去。此外,还有一些文档的建设。
刚刚已经分享了很多内容,但还是有很多可以做的事情。在GraalVM提供的能力上,我们还可以把一些类相关的可达性的配置加上去,产生作用之后能让最后打出来的二进制包更小,编译时间更短。
因为目前很多组件都还不支持,所以我们现在的主要思路是把Dubbo主仓库的扩展性支持完成,然后再往wpi的扩展上做相应的支持。
另外,内核所需要的可达性的元数据,我们会把它推到program的可达性的元数据的仓库上面去,让业务开发能够正常使用大陆内核里的元数据信息。
最后我们的思路还是优先考虑GraalVM官方的支持。