走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解
本文主要分为以下四个部分展开:
- 首先我们会先看一下在云计算快速发展的当下,云上应用应该具备的特点,Java 应用在云上所面临的挑战有哪些。
- 其次,我会介绍一下 GraalVM,什么是 Native Image,如何通过 GraalVM 对 Java 应用进行静态化打出 Native Image 可执行的二进制程序。
- 第三部分,我们知道 GraalVM 的使用是有一定限制的,比如 Java 的反射等动态特性是不被支持的,因此我们需要提供特殊的 Metadata 配置来绕过这些限制,在这一部分我们会讲解如何加入引入 AOT Processing 来实现自动化的 Metadata 配置,包括 Spring6 框架中 AOT 处理、Dubbo3 框架的 AOT 处理等。
- 最后,我们将通过一个 Spring6+Dubbo3 的应用示例,来演示如何将这么一个 Java 应用进行静态化打包。
Aliware 首先,我们先看一下云计算时代的应用特点,以及 Java 在云时代所面临的挑战。从各个统计机构给出的数据来看,Java 语言仍然是当今最受开发者欢迎的编程语言之一,仅次于一些脚本开发语言。使用 Java 语言可以非常高效的开发业务应用,丰富的生态使得 Java 具有非常高的开发和运行效率,有无数的应用基于 Java 语言开发。
出现这样的原因,主要是 Java 应用不能很好的满足 Serverless 场景的几个关键要求。
- 首先是启动速度问题,Java 冷启动的耗时是比较长的。 这对于 Serverless 需要快速弹起的场景是一个非常大的挑战,因为 Java 应用的拉起时间可能是秒、数十秒级别的;
- 第二点,Java 应用往往都需要一定的预热时间, 才能达到最佳的性能状态,刚刚拉起的应用如果分配比较大的流量是不合适的,往往会出现请求超时、资源占用过高等问题,这就进一步拉长了 Java 应用的有效拉起时间;
- 第三点是 Java 应用对运行环境的要求, 它往往需要较大的内存、计算资源,而这些真正分配给业务自身的并不多,都消耗在一些 JVM 运行时上,这与用云降本提效的目标并补匹配;
- 最后,Java 应用打出来的包或者镜像也是非常大, 从总体上也影响存储、拉取的效率。
接下来,我们具体看一下针对 Java 应用所面临的这些问题, GraalVM 这样一种打包和运行时技术是如何解决的。 GraaIVM 简介
Aliware
GraalVM compiles your Java applications ahead of time into standalone binaries that start instantly, provide peak performance with no warmup, and use fewer resources.引用官方介绍来看,GraalVM 为 Java 应用提供 AOT 编译和二进制打包能力,基于 GraalVM 打出的二进制包可以实现快速启动、具有超高性能、无需预热时间、同时需要非常少的资源消耗。这里所说的 AOT 是发生在编译期间的一个技术简称,即 Ahead-of-time,这一点我们后续会讲到。总的来说 GraalVM 可以分为两部分内容来看
- 首先, GraalVM 是一个完整的 JDK 发行版本, 从这一点它是与 OpenJDK 对等的,可以运行任何面向 jvm 的语言开发的应用;
- 其次, GraalVM提供了 Native Image 打包技术, 这可以将应用打包为可以独立运行的二进制包,这个包是自包含的、可脱离 JVM 运行的应用程序。
- 对于 JIT 而言,我们都知道 Java 类会被编译为 .class 格式的文件,这里编译后就是 jvm 识别的字节码,在 Java 应用运行的过程中,而 JIT 编译器又将一些热点路径上的字节码编译为机器码,已实现更快的执行速度;
- 对于 AOT 模式来说,它直接在编译期间就将字节码转换为机器码,直接省去了运行时对 jvm 的依赖,由于省去了 jvm 加载和字节码运行期预热的时间,AOT 编译和打包的程序具有非常高的运行时效率。
Aliware
我们上面多次提到 GraalVM 中 Native Image 概念,Native Image 是一项将 Java 代码编译打包为可执行二进制程序的技术,打出的包中仅包含运行期所需要的代码,包括应用自身代码、标准依赖包、 语言运行时、JDK 库关联的静态代码。这个包的运行不再需要 jvm 环境,当然它是和具体的机器环境相绑定的,需要为不同的机器环境单独打包。Native Image 有这里列出来的一系列特点:
- 仅包含 JVM 运行所需的一部分资源,运行成本更低
- 毫秒级的启动时间
- 启动后即进入最佳状态,无需预热
- 可打包为更轻量的二进制包,让部署速度更快更高效
- 安全程度更高
Aliware 接下来,我们看一下 GraalVM 的基本使用方式,首先,需要安装 native-image 需要的相关基础依赖,根据不同的操作系统环境会有所差异,接下来可以使用 GraalVM JDK 下载器下载 native-image。都安装好之后,第二步,就可以使用 native-image 命令编译和打包 Java 应用了,输入可以是 class 文件、jar文件、Java 模块等,最终打包为一个可独立运行的可执行文件,比如这里的 HelloWorld。另外,GraalVM 也提供了对应的 Maven和Gradle 构建工具插件,让打包过程更容易。
- GraalVM 在编译构建期间就会以 main 函数为入口,完成对应用代码的静态分析
- 在静态分析期间无法被触达的代码,将会被移除,不会包含在最终的二进制包中
- GraalVM 无法识别代码中的一些动态调用行为,如反射、resource 资源加载、序列化、动态代理等都动态行为都将受限
- Classpath 在构建阶段就固化下来,无法修改
- 不再支持延迟的类加载,所有可用类和代码在程序启动阶段就确定了
- 还有一些其他的 Java 应用能力是受限使用的(比如类初始化提前等)
Aliware Java 应用或框架中的反射等动态特性的使用是影响 GraalVM 使用的障碍,而大量的框架都存在这个限制,如果都要求应用或者开发者提供 Metadata 配置的话将会是一项非常有挑战的任务,因此,Spring 和 Dubbo 等框架都在 AOT Compilation 即 AOT 编译之前引入了 AOT Processing 即 AOT 预处理的过程,AOT Processing 用来完成自动化的 Metadata 采集,并将 Metadata 提供给 AOT 编译器使用。
- Spring - Spring AOT processing
- Dubbo - Dubbo AOT processing
- Third-party libraries - Reachability Metadata
Metadata 配置: https://github.com/oracle/graalvm-reachability-metadataSpring AOT 接下来我们看一下 Spring AOT 做了哪些编译之前的预处理工作,Spring 框架中有非常多的动态特性,比如自动配置、条件 Bean 等特性。Spring AOT 就是针对针对这些动态特性,在构建阶段进行预处理,生成可供 GraalVM 使用的一系列 Metadata 输入,这里生成的内容包括:
- Spring Bean 定义相关的代码预生成,如下图展示代码段
- 在构建阶段生成动态代理相关代码
- 关于一些反射等使用的 JSON 元数据文件
- SPI 扩展相关的源代码生成
- 一些反射使用的 JSON 配置文件生成
- RPC 代理类代码生成
Aliware 接下来,我们通过一个 Spring6 + Dubbo3 的示例微服务应用,演示如何使用 Spring AOT、Dubbo AOT 等,来实现应用的 Native Image 打包。 完整的代码示例可在这里下载: https://github.com/apache/dubbo-samples/tree/master/1-basic/dubbo-samples-native-image 第一步:安装 GraalVM 1. 在 Graalvm 官网根据自己的系统选取对应 Graalvm 版本: https://www.graalvm.org/downloads/ 2. 根据官方文档安装 native-image: https://www.graalvm.org/latest/reference-manual/native-image/#install-native-image 第二步:创建项目 这个示例应用就是普通的、常见的微服务应用,我们使用 SpringBoot3 进行应用配置开发,使用 Dubbo3 定义并发布 RPC 服务;应用构建工具使用 Maven。
第四步:在 Pom 依赖中添加 native 相关的依赖 另外,对于 Dubbo 而言,由于当前一些 Native 机制依赖 JDK17 等版本,Dubbo 没有将一些包默认打包到发行版本中,因此需要增加两个额外的依赖 dubbo-spring6 适配和 dubbo-native 组件。<profiles><profile><id>native</id><build><plugins><plugin><artifactId>maven-compiler-plugin</artifactId><configuration><release>17</release><fork>true</fork><verbose>true</verbose></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><id>process-aot</id><goals><goal>process-aot</goal></goals></execution></executions></plugin><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><version>0.9.20</version><configuration><classesDirectory>${project.build.outputDirectory}</classesDirectory><metadataRepository><enabled>true</enabled></metadataRepository><requiredVersion>22.3</requiredVersion></configuration><executions><execution><id>add-reachability-metadata</id><goals><goal>add-reachability-metadata</goal></goals></execution></executions></plugin><plugin><groupId>org.apache.dubbo</groupId><artifactId>dubbo-maven-plugin</artifactId><version>${dubbo.version}</version><configuration><mainClass>com.example.nativedemo.NativeDemoApplication</mainClass></configuration><executions><execution><phase>process-sources</phase><goals><goal>dubbo-process-aot</goal></goals></execution></executions></plugin></plugins></build></profile></profiles>
第五步:调整 compiler、proxy、serialization 和 logger 同时,这个示例对于第三方组件的支持目前也是受限的,主要是第三方组件的 Reachability Metadata 。比如目前支持的网络通信或编码组件有 Netty 和 Fastjson2;支持的日志等组件为 Logback;微服务组件有 Nacos、Zookeeper 等。<dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-config-spring6</artifactId><version>${dubbo.version}</version></dependency><dependency><groupId>org.apache.dubbo</groupId><artifactId>dubbo-native</artifactId><version>${dubbo.version}</version></dependency>
- 序列化方式目前支持的比较好的是 Fastjson2
- compiler、proxy 目前只能选择 jdk
- logger 目前需要配置 slf4j,目前仅支持 logback
第六步:编译 在项目根路径下执行以下编译命令:dubbo:application:name: ${spring.application.name}logger: slf4jcompiler: jdkprotocol:name: dubboport: -1serialization: fastjson2registry:id: zk-registryaddress: zookeeper://127.0.0.1:2181config-center:address: zookeeper://127.0.0.1:2181metadata-report:address: zookeeper://127.0.0.1:2181provider:proxy: jdkserialization: fastjson2consumer:proxy: jdkserialization: fastjson2
- API 方式直接执行
mvn clean install -P native -Dmaven.test.skip=true
- 注解和 xml 方式(Springboot3 集成的方式)
mvn clean install -P native native:compile -Dmaven.test.skip=true
第七步:执行二进制文件即可
二进制文件在 target/ 目录下,一般以工程名称为二进制包的名称,比如 target/native-demo。
总结
Aliware GraalVM 技术为 Java 在云计算时代的应用带来了新的变革,帮助解决了 Java 应用启动慢、资源占用,但同时我们也看到了 GraalVM 的使用也存在一些限制,因此 Spring6、SpringBoot3、Dubbo3 都提供了相应的 Native 解决方案。除了 Dubbo 之外,Spring Cloud Alibaba 也正在推进静态化打包方案,接下来,我们将围绕两款框架的周边生态组件如 nacos、sentinel、seata 等推进整体的 Native 静态化验证。 相关链接: [1] Apache Dubbo 博客 https://dubbo.apache.org/zh-cn/blog/