乐趣区

关于阿里云:走向-Native-化SpringDubbo-AOT-技术示例与原理讲解

作者:刘军

Java 利用在云计算时代面临“冷启动”慢、内存占用高、预热工夫长等问题,无奈很好的适应 Serverless 等云上部署模式,GraalVM 通过动态编译、打包等技术在很大水平上解决了这些问题,同时针对 GraalVM 的一些应用限度,Spring 和 Dubbo 等支流框架也都提供了相应的 AOT 解决方案。

本文咱们将详细分析 Java 利用在云时代面临的挑战,GraalVM Native Image 是如何解决这些问题,GraalVM 的基本概念与工作原理,最初咱们通过一个 Spring6 + Dubbo3 的微服务利用示例演示了如何将一个一般微服务利用进行动态化打包。

本文次要分为以下四个局部开展:

  1. 首先咱们会先看一下在云计算疾速倒退的当下,云上利用应该具备的特点,Java 利用在云上所面临的挑战有哪些。
  2. 其次,我会介绍一下 GraalVM,什么是 Native Image,如何通过 GraalVM 对 Java 利用进行动态化打出 Native Image 可执行的二进制程序。
  3. 第三局部,咱们晓得 GraalVM 的应用是有肯定限度的,比方 Java 的反射等动静个性是不被反对的,因而咱们须要提供非凡的 Metadata 配置来绕过这些限度,在这一部分咱们会解说如何退出引入 AOT Processing 来实现自动化的 Metadata 配置,包含 Spring6 框架中 AOT 解决、Dubbo3 框架的 AOT 解决等。
  4. 最初,咱们将通过一个 Spring6+Dubbo3 的利用示例,来演示如何将这么一个 Java 利用进行动态化打包。

Java 利用在云时代所面临的挑战

首先,咱们先看一下云计算时代的利用特点,以及 Java 在云时代所面临的挑战。从各个统计机构给出的数据来看,Java 语言依然是当今最受开发者欢送的编程语言之一,仅次于一些脚本开发语言。应用 Java 语言能够十分高效的开发业务利用,丰盛的生态使得 Java 具备十分高的开发和运行效率,有有数的利用基于 Java 语言开发。

但在来到云计算时代,Java 利用的部署和运行开始面临十分多的问题。咱们以 Serverless 为例,Serverless 是云上的一种越来越支流的部署模式,它让开发者更专一业务逻辑、通过疾速弹性等帮忙解决资源问题,依据最新的一些数据,Java 在所有云计算厂商的 Serverless 运行时中所占比例并不高,远远不能和它在传统利用开发中所占的比例相匹配。

呈现这样的起因,次要是 Java 利用不能很好的满足 Serverless 场景的几个要害要求。

  • 首先是启动速度问题,Java 冷启动的耗时是比拟长的。 这对于 Serverless 须要疾速弹起的场景是一个十分大的挑战,因为 Java 利用的拉起工夫可能是秒、数十秒级别的;
  • 第二点,Java 利用往往都须要肯定的预热工夫, 能力达到最佳的性能状态,刚刚拉起的利用如果调配比拟大的流量是不适合的,往往会呈现申请超时、资源占用过低等问题,这就进一步拉长了 Java 利用的无效拉起工夫;
  • 第三点是 Java 利用对运行环境的要求, 它往往须要较大的内存、计算资源,而这些真正调配给业务本身的并不多,都耗费在一些 JVM 运行时上,这与用云降本提效的指标并补匹配;
  • 最初,Java 利用打进去的包或者镜像也是十分大, 从总体上也影响存储、拉取的效率。

接下来,咱们具体看一下针对 Java 利用所面临的这些问题,GraalVM 这样一种打包和运行时技术是如何解决的。

GraaIVM 简介

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 运行的应用程序。

如上图所示,GraalVM 编译器提供了 JIT 和 AOT 两种模式。

  • 对于 JIT 而言,咱们都晓得 Java 类会被编译为 .class 格局的文件,这里编译后就是 jvm 辨认的字节码,在 Java 利用运行的过程中,而 JIT 编译器又将一些热点门路上的字节码编译为机器码,已实现更快的执行速度;
  • 对于 AOT 模式来说,它间接在编译期间就将字节码转换为机器码,间接省去了运行时对 jvm 的依赖,因为省去了 jvm 加载和字节码运行期预热的工夫,AOT 编译和打包的程序具备十分高的运行时效率。

总的来说,JIT 使得利用能够具备更高的极限解决能力,能够升高申请的最大提早这一要害指标;而 AOT 则能够进一步的晋升利用的冷启动速度、具备更小的二进制包提及、在运行态须要更少的内存等资源。

什么是 Native Image?

咱们下面屡次提到 GraalVM 中 Native Image 概念,Native Image 是一项将 Java 代码编译打包为可执行二进制程序的技术,打出的包中仅蕴含运行期所须要的代码,包含利用本身代码、规范依赖包、语言运行时、JDK 库关联的动态代码。这个包的运行不再须要 jvm 环境,当然它是和具体的机器环境相绑定的,须要为不同的机器环境独自打包。Native Image 有这里列出来的一系列特点:

  • 仅蕴含 JVM 运行所需的一部分资源,运行老本更低
  • 毫秒级的启动工夫
  • 启动后即进入最佳状态,无需预热
  • 可打包为更轻量的二进制包,让部署速度更快更高效
  • 平安水平更高

总结起来就是这里的要害几项:更快的启动个速度、更少的资源占用、更小的安全漏洞危险、更紧凑的二进制包体积。解决 Java 利用在 Sererless 等云计算利用场景中面临的突出问题。

GraaIVM Native Image 的基本原理与应用

接下来,咱们看一下 GraalVM 的根本应用形式,首先,须要装置 native-image 须要的相干根底依赖,依据不同的操作系统环境会有所差别,接下来能够应用 GraalVM JDK 下载器下载 native-image。都装置好之后,第二步,就能够应用 native-image 命令编译和打包 Java 利用了,输出能够是 class 文件、jar 文件、Java 模块等,最终打包为一个可独立运行的可执行文件,比方这里的 HelloWorld。另外,GraalVM 也提供了对应的 Maven 和 Gradle 构建工具插件,让打包过程更容易。

GraalVM 基于叫做“closed world assumption”即关闭世界假如的概念,要求在编译期间程序的所有运行时资源和行为即能被齐全确定下来。图中是具体的 AOT 编译和打包过程,左侧利用代码、仓库、jdk 等全副作为输出,GraalVM 以 main 为入口,扫描所有可触达的代码与执行门路,在处理过程中可能会波及到一些前置初始化动作,最终 AOT 编译的机器码和一些初始化资源等状态数据,被打包为可执行的 Native 包。

相比于传统的 JVM 部署模式,GraalVM Native Image 模式带来的十分大的不同。

  • GraalVM 在编译构建期间就会以 main 函数为入口,实现对利用代码的动态剖析
  • 在动态剖析期间无奈被触达的代码,将会被移除,不会蕴含在最终的二进制包中
  • GraalVM 无奈辨认代码中的一些动静调用行为,如反射、resource 资源加载、序列化、动静代理等都动静行为都将受限
  • Classpath 在构建阶段就固化下来,无奈批改
  • 不再反对提早的类加载,所有可用类和代码在程序启动阶段就确定了
  • 还有一些其余的 Java 利用能力是受限应用的(比方类初始化提前等)

GraalVM 不反对反射等动静个性,而咱们的很多利用和框架中却大量应用了反射、动静代理等个性,如何能力将这些利用打包为 Native Image 实现动态化那?GraalVM 提供了元数据配置入口,通过为所有动静个性提供配置文件,“closed world assumption”模式还是成立的,能够让 GraalVM 在编译期晓得所有的预期行为。这里给了两个例子:

  1. 编码方式上,比方这里反射的编码方式,能够让 GraalVM 通过代码剖析计算 Metadata。
  1. 另一个示例是提供额定的 json 配置文件并放在指定的目录 META-INF/native-image// 下。

AOT Processing

Java 利用或框架中的反射等动静个性的应用是影响 GraalVM 应用的阻碍,而大量的框架都存在这个限度,如果都要求利用或者开发者提供 Metadata 配置的话将会是一项十分有挑战的工作,因而,Spring 和 Dubbo 等框架都在 AOT Compilation 即 AOT 编译之前引入了 AOT Processing 即 AOT 预处理的过程,AOT Processing 用来实现自动化的 Metadata 采集,并将 Metadata 提供给 AOT 编译器应用。

AOT 编译机制是对所有 Java 利用通用的,但相比于 AOT 编译,AOT Processing 采集 Metadata 的过程是每个框架都不同的,因为每个框架对于反射、动静代理等都有本人的用法。

咱们以一个典型的 Spring + Dubbo 的微服务利用为例,要实现这个利用的动态化打包,这里波及到 Spring、Dubbo 以及一众第三方依赖的 Metadata 处理过程。

  • Spring – Spring AOT processing
  • Dubbo – Dubbo AOT processing
  • Third-party libraries – Reachability Metadata

对于 Spring 来说,Spring6 中公布了 Spring AOT 机制,用来反对 Spring 利用的动态化预处理;Dubbo 最近也在 3.2 版本中公布了 Dubbo AOT 机制,让 Dubbo 相干组件能够自动化实现 Native 预处理;除了这两个与业务开发密切相关的框架,一个利用中往往还有大量的第三方依赖,这些依赖的 Metadata 也是影响动态化的要害,如果它们中有反射、类加载等行为,那么须要为它们提供 Metadata 配置,对于这些第三方利用目前有两个渠道,一个是 GraalVM 官网提供的共享空间,这里有相当一部分依赖的 Metadata 配置可供使用,另一种形式则是要求组件官网公布的公布中蕴含 Metadata 配置,对于这两种状况 GraalVM 都能够做到对于 Metadata 的主动读取。

Metadata 配置:https://github.com/oracle/graalvm-reachability-metadata

Spring AOT

接下来咱们看一下 Spring AOT 做了哪些编译之前的预处理工作,Spring 框架中有十分多的动静个性,比方主动配置、条件 Bean 等个性。Spring AOT 就是针对针对这些动静个性,在构建阶段进行预处理,生成可供 GraalVM 应用的一系列 Metadata 输出,这里生成的内容包含:

  • Spring Bean 定义相干的代码预生成,如下图展现代码段
  • 在构建阶段生成动静代理相干代码
  • 对于一些反射等应用的 JSON 元数据文件

Dubbo AOT

Dubbo AOT 做的事件与 Spring AOT 十分相似,只不过 Dubbo AOT 是专门针对 Dubbo 框架特有的应用形式进行预处理,这包含:

  • SPI 扩大相干的源代码生成
  • 一些反射应用的 JSON 配置文件生成
  • RPC 代理类代码生成

Spring6+Dubbo3 示例演示

接下来,咱们通过一个 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/…

第二步:创立我的项目

这个示例利用就是一般的、常见的微服务利用,咱们应用 SpringBoot3 进行利用配置开发,应用 Dubbo3 定义并公布 RPC 服务;利用构建工具应用 Maven。

第三步:配置 Maven 插件

重点是减少 spring-boot-maven-plugin、native-maven-plugin、dubbo-maven-plugin 三个插件配置,开启 AOT 处理过程,批改 dubbo-maven-plugin 中的 mainClass 为所需的启动类全门路。(其中 API 应用形式无需增加 spring-boot-maven-plugin 依赖。)

<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>

第四步:在 Pom 依赖中增加 native 相干的依赖

另外,对于 Dubbo 而言,因为以后一些 Native 机制依赖 JDK17 等版本,Dubbo 没有将一些包默认打包到发行版本中,因而须要减少两个额定的依赖 dubbo-spring6 适配和 dubbo-native 组件。

<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>

第五步:调整 compiler、proxy、serialization 和 logger

同时,这个示例对于第三方组件的反对目前也是受限的,次要是第三方组件的 Reachability Metadata。比方目前反对的网络通信或编码组件有 Netty 和 Fastjson2;反对的日志等组件为 Logback;微服务组件有 Nacos、Zookeeper 等。

  • 序列化形式目前反对的比拟好的是 Fastjson2
  • compiler、proxy 目前只能抉择 jdk
  • logger 目前须要配置 slf4j,目前仅反对 logback

示例配置如下:

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    compiler: jdk
  protocol:
    name: dubbo
    port: -1
    serialization: fastjson2
  registry:
    id: zk-registry
    address: zookeeper://127.0.0.1:2181
  config-center:
    address: zookeeper://127.0.0.1:2181
  metadata-report:
    address: zookeeper://127.0.0.1:2181
  provider:
    proxy: jdk
    serialization: fastjson2
  consumer:
    proxy: jdk
    serialization: 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。

总结

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/

退出移动版