暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Dubbo 启动提升10倍?

技术对话 2024-03-18
117

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

1、云环境下Java应用的劣势

从各个统计机构给出的数据来看,Java 语言仍然是当今最受开发者欢迎的编程语言之一,仅次于一些脚本开发语言。使用 Java 语言可以非常高效的开发业务应用,丰富的生态使得 Java 具有非常高的开发和运行效率,有无数的应用基于 Java 语言开发。但在来到云计算时代,Java 应用的部署和运行开始面临非常多的问题。

云计算时代比较显著的特点包括:

  • 基于云计算的基础设施,我们的应用能够在云上快速、轻松且高效地做到弹性。尤其是无状态的应用,能够轻易地基于同一个镜像构建实例,当然也能轻易地收缩多余的实例,实现弹性伸缩容。

  • 基于容器化技术,系统资源被切分的更细,资源的利用也变得更优。

  • 基于云计算的开发平台,应用部署更加容易,应用开发更加敏捷。

但在云计算时代,Java 应用存在劣势:

  • 冷启动速度较慢:Java 应用启动需要经历包括 JVM 的初始化、类加载等过程,导致启动速度相较于其他语言来说是处于劣势的。

  • 应用预热时间过长,无法立即达到性能峰值:比如如果没有对应用做一些预热机制,并且对 RT 又比较敏感的应用,会导致发布时有一定的接口超时情况。

  • 运行环境要求较高:往往需要较大的内存、计算资源。而且当内存、CPU占用过高时,不得不为 Java 应用提供更高规格的实例,而切分大规格的实例,这也会导致切分后造成的碎片更大,从而导致资源的浪费。

  • Java 应用打出来的包或者镜像也是非常大:一定程度上也影响存储、拉取的效率。

这些劣势最明显的影响就是在 Serverless 场景。因为 Serverless 除了能够简化应用的开发外,最关键的就是能够做到让开发者的应用服务做的秒级弹性扩容。除了容器调度和新的 Pod 创建的时间损耗以外,镜像下载耗时、应用冷启动耗时、以及应用服务预热所需的耗时,都是影响弹性扩容的因素。

而面对 Java 语言的这些问题,GraalVM 也是应运而生。(官网:https://www.graalvm.org/)

2、GraalVM

2.1、GraalVM简介

GraalVM compiles your Java applications ahead of time into standalone binaries. These binaries are smaller, start up to 100x faster, provide peak performance with no warmup, and use less memory and CPU than applications running on a Java Virtual Machine (JVM).

GraalVM reduces the attack surface of your application. It excludes unused classes, methods, and fields from the application binary. It restricts reflection and other dynamic Java language features to build time only. It does not load any unknown code at run time.

GraalVM includes the Java Development Kit (JDK), the just-in-time compiler (the Graal compiler), Native Image, and standard JDK tools. You can use the GraalVM JDK just like any other JDK in your IDE, so having installed GraalVM, you can run any Java application unmodified.

以上为GraalVM官网的介绍,从介绍中可知:

  • GraalVM 是一个完整的 JDK 发行版本, 从这一点它是与 OpenJDK 对等的,可以运行任何面向 jvm 的语言开发的应用;

  • 除了传统的JIT打包技术,GraalVM还提供了 Native Image 打包技术(AOT), 这可以将应用打包为可以独立运行的二进制包,这个包是自包含的、可脱离 JVM 运行的应用程序。

  • 对于 JIT 而言,我们都知道Java类会被编译为 .class 格式的文件,这里编译后就是 jvm 识别的字节码,在 Java 应用运行的过程中,而 JIT 编译器又将一些热点路径上的字节码编译为机器码,已实现更快的执行速度;

  • 对于 AOT 模式来说,它直接在编译期间就将字节码转换为机器码,直接省去了运行时对jvm的依赖,由于省去了 jvm 加载和字节码运行期预热的时间,AOT 编译和打包的程序具有非常高的运行时效率。

总的来说,JIT 使得应用可以具备更高的极限处理能力,可以降低请求的最大延迟这一关键指标;而 AOT 则可以进一步的提升应用的冷启动速度、具有更小的二进制包提及、在运行态需要更少的内存等资源。

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

2.2、Native Image

Native Image是一种将Java代码提前编译成二进制文件的技术,即本地可执行文件。本地可执行文件只包含运行时所需的代码,包括应用程序类、标准库类、语言运行时以及从JDK静态链接的本地代码。Native Image是用Java编写的,它以Java字节码作为输入,生成一个独立的二进制文件(可执行文件或共享库)。

Native Image的优点:

  • 仅包含 JVM 运行所需的一部分资源,运行成本更低

  • 毫秒级的启动时间

  • 启动后即进入最佳状态,无需预热

  • 可打包为更轻量的二进制包,让部署速度更快更高效

  • 安全程度更高

在生成二进制文件的过程中,Native Image可以运行用户代码,即代码在构建时被执行了。二进行文件生成完成后,Native Image将编译后的用户代码、部分Java运行时(例如垃圾回收器、线程支持)以及代码执行的结果链接到二进制文件中。用户在使用Native Image时,可自行指定类在运行时还是在构建时初始化。

运行时 vs 构建时的区别,以Hello World代码为例,其中Greeter为HelloWorld的内部静态类:

public class HelloWorld {
    static class Greeter {
        static {
            System.out.println("Greeter is getting ready!");
        }

        public static void greet() {
          System.out.println("Hello, World!");
        }
    }

  public static void main(String[] args) {
    Greeter.greet();
  }
}

常规运行以上代码的输出:

javac HelloWorld.java
java HelloWorld 
Greeter is getting ready!
Hello, World!

使用native-image构建该类并指定Greeter在运行时初始化,即JIT模式:

native-image HelloWorld
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
...
Finished generating 'helloworld' in 14.9s.
./helloworld 
Greeter is getting ready! //运行时输出
Hello, World!

让我们看看若指定Greeter在构建时初始化时,会产生怎样的效果:

native-image HelloWorld --initialize-at-build-time=HelloWorld\$Greeter
========================================================================================================================
GraalVM Native Image: Generating 'helloworld' (executable)...
========================================================================================================================
Greeter is getting ready! //构建时输出
[1/7] Initializing...                                                                                    (3.1s @ 0.15GB)
 Version info: 'GraalVM dev Java 11 EE'
 Java version info: '11.0.15+4-jvmci-22.1-b02'
 C compiler: gcc (linux, x86_64, 9.4.0)
 Garbage collector: Serial GC
...
Finished generating 'helloworld' in 13.6s.
./helloworld 
Hello, World!

相比于传统的 JVM 部署模式,GraalVM Native Image 模式带来的非常大的不同:

  • GraalVM 在编译构建期间就会以 main 函数为入口,完成对应用代码的静态分析

  • 在静态分析期间无法被触达的代码,将会被移除,不会包含在最终的二进制包中

  • GraalVM 无法识别代码中的一些动态调用行为,如反射、resource资源加载、序列化、动态代理等都动态行为都将受限

  • Classpath 在构建阶段就固化下来,无法修改

  • 不再支持延迟的类加载,所有可用类和代码在程序启动阶段就确定了

  • 还有一些其他的 Java 应用能力是受限使用的(比如类初始化提前等)

2.3、Ahead of Time(AOT)

Java 应用或框架中的反射等动态特性的使用是影响 GraalVM 使用的障碍,而大量的框架都存在这个限制,如果都要求应用或者开发者提供 Metadata 配置的话将会是一项非常有挑战的任务,因此,Spring 和 Dubbo 等框架都在 AOT Compilation 即 AOT 编译之前引入了 AOT Processing 即 AOT 预处理的过程,AOT Processing 用来完成自动化的 Metadata 采集,并将 Metadata 提供给 AOT 编译器使用。

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

3、Dubbo集成GraalVM

3.1、发展

早在 21 年 6 月份正式发布 Dubbo3.0时,Dubbo 社区的成员就初步调研了 GraalVM Native Image 技术,并在 3.0 的其中一个迭代版本中初步支持了 Native Image,但当时仅仅是以实验性 Demo 呈现,并没有过多的考虑生产环境的用户如何使用、Dubbo 的贡献者的维护成本等问题。

2022 年 11 月份时,Spring6 和 Spring boot 3.0 发布了,而GraalVM Native Image正是其中一项比较亮眼的特性,Dubbo社区的开发者也开始进行Dubbo对 GraalVM Native Image集成的重构。于2023 年 4 月发布的Dubbo 3.2版本(预计今年底发布的dubbo 3.3 版本中,将在支持 xml 和注解的接入方式上支持 native image),解决了之前试验性版本的各项问题:

  1. 编译阶段自动识别所需的 SPI 接口,并自动生成 Adaptive Source code。并且按照标准的 Java 编译产物目录结构,这些 Source Code 被生成在 target 目录下。并且这部分的 Source code 也不再 dubbo 核心仓库内维护,它是编译阶段动态生成的。

  2. 支持编译阶段自动生成 Dubbo 框架所需的 Reachability Metadata(可达性的元数据)。同样无需开发者再维护这部分的配置信息。

  3. 除此之外,新增了 dubbo-maven-plugin,希望能够替代 dubbo-native-plugin。dubbo开发者认为 dubbo-maven-plugin 应该作为 dubbo 框架对开发人员输出的唯一一个 maven plugin,而所有需要借助 maven plugin 构建出来的新特性,都应该迁移到该 maven plugin 中。这样能够降低开发者的心智负担。

3.2、Reachability Metadata

在前面的介绍中有提到一个词Reachability Metadata(可达性元数据),Dubbo 在集成GraalVM Native Image 也主要是围绕着 Reachability Metadata 的处理。

可达性元数据:JVM 的动态语言功能(包括反射和资源处理)计算动态访问的程序元素,例如运行时调用的方法或资源 URL。在构建本机二进制文件时,native-image会执行静态分析以确定这些动态功能,但它不能总是详尽地预测所有用途。为了确保将这些元素包含到本机二进制文件中,您应该向构建器提供可达性元数据 。为构建器提供可达性元数据还可以确保在运行时与第三方库的无缝兼容性。

AOT 也存在自己的局限性,那就是它遵循封闭世界假设原则。也就是需要依赖于能够“看到全部的字节码”才能正确工作,这将导致 AOT 无法支持动态语言的功能,比如 JNI(Java Native Interface)、Java 反射、动态代理、ClassPath 资源的获取等都不再支持。

在 Java  开发过程中,这些 Java 的动态能力被运用在各种场景中,它们早已经是 Java 开发者非常熟练的编码手段。所以 GraalVM 同样也考虑到了这个情况。在既不打破“封闭时间假设”原则的前提下,通过可达性的元数据来解决这类问题。既然需要在编译器中能够确定所有的字节码和资源,那么就让开发者在编码阶段就确定这些元数据信息。

可达性的元数据目前主要用到五种:涉及了JNI、反射、序列化、resource、dynamic proxy相关的元数据的配置信息,它可以让开发者在编译前就提供好这些元数据信息,提前打包到可执行的二进制文件中。

GraalVM 提供了 Tracing Agent,来辅助开发者在运行时采集对应的可达性元数据。但是通过 Tracing Agent 采集的元数据并不能保障能够采集完整,因为 Tracing Agent 只跟踪和采集执行的代码,若程序输入没有覆盖的代码路径,将无法采集到,因此GraalVM官网也建议采集后还需要手动 check 元数据。

在Java开发过程中还会使用很多组件,开发人员通过这些组件能够实现相应的业务功能。GraalVM也提供了可达性的元数据仓库(gitHub:Reachability Metadata Repository),通过这个仓库,可以吸纳来自不同的组件的可达性的元数据。

与 Dubbo 相关的可达性的元数据有:

(1)Reflection Metadata

第一个场景就是服务,Dubbo 是一个 RPC 框架,定义服务接口是最基本的需求,同时运行时通过反射获取接口的方法等操作非常频繁。这里区分了内外部的服务接口,原因是 Dubbo 框架内还有一些内建的服务,比如 MetricService、MetadataService 等。

第二个 SPI Extension 类和 Adaptive 类,我们知道 Dubbo 强大且灵活的扩展性得益于它自有的一套 SPI 机制,其中定义为 SPI 接口的实现类,以及 Adaptive 类都需要用到反射。当然这里的 SPI 扩展实现类,也包含业务自己实现的类,比如大家最熟悉和应用最广泛的 Dubbo 执行链中的 Filter,即使业务有自己的实现类,Dubbo aot 也能扫描到并且加载。

第三类是多实例的启动时候需要提前加载一些相关的类。

第四类是 Dubbo 核心的配置类,有通过 API 接入的方式使用经验的朋友应该清楚,比如 ServiceConfig、RegistryConfig 等。

最后是一些其他反射行为,这里包括的是 Dubbo 依赖的组件,存在一些反射行为,比如 Zookeeper。

(2)Resource Metadata

Dubbo 相关有四个资源文件(META-INF/dubbo/internal、META-INF/dubbo、META-INF/services、security)。如果业务上要做扩展,配置文件,Resource Metadata主要涉及的就是META-INF下的三个路径下的资源文件,在使用 Dubbo 的 SPI 时,必须配置扩展实现的配置。才能保证 SPI 的实现被加载到。

(3)Serialization Metadata

作为rpc调用的框架,它的接口定义、方法定义、内外部分的服务、parameter、请求参数、返回类型等都需要用到序列化的接口。

(4)Dynamic Proxy Metadata

在 Dubbo 中主要是在 Consumer 需要有生成动态代理类,用于代理远程的服务接口。屏蔽掉一些网络传输、序列化等行为的细节,让调用方能够像本地方法调用一样使用。

(5)JNI Metadata

目前 Dubbo 内还没有相关的元数据信息。

3.3、Dubbo AOT

Spring 和 Dubbo 都有自己的 AOT 处理逻辑,但是它们之间的处理又有些不同,下图是 Spring AOT 的处理逻辑,可以看到从源码编译开始,Spring 会直接从 main 函数启动应用程序,并且会将 Spring bean 生成的 source code 生成,以及对应的 Reachability Metadata 生成。对于 Spring 而言,在启动和扫描过程已经能够完成所有 Metadata 的扫描。

而 Dubbo 的 AOT 则是在源码开始编译后,会启动一个扫描的进程,来完成刚刚列举的与 Dubbo 有关的可达性的元数据和对应的源码。

Dubbo AOT 做的事情与 Spring AOT 非常类似,只不过 Dubbo AOT 是专门针对 Dubbo 框架特有的使用方式进行预处理,这包括:

  • SPI 扩展相关的源代码生成

  • 一些反射使用的 JSON 配置文件生成

  • RPC 代理类代码生成

下图是 Dubbo AOT (左)以及 Spring AOT(右)生成的内容对比,左图下半部分就是Dubbo相关的自适应源码,后续 native executable的时候会读取到这里所有的配置。

如下图,Dubbo AOT与Spring AOT 是存在边界的,首先在 API 接入的方式中,Dubbo 不依赖于 Spring,所以它能够完成所有的内容,包括所需的 Adaptive Source code 和 Reachability Metadata。其次因为 XML 和注解都依赖于 Spring,所以 Dubbo 内的 Bean 都将会依托于 Spring AOT 的能力来实现,包括 ServiceBean、ReferenceBean 以及 Dubbo 框架内的与 Spring Bean 有关的内容。除此之外,Spring 自身也会生成所需的 Reachability Metadata。

4、Dubbo性能影响

4.1、启动速度

两张图描述的是分别在 Native Executable 和 Jar package 的场景下,仅提供一个 Dubbo 服务的应用启动耗时对比。这里的数据都统计与 4c16g 的 macOS 系统下,并且每个场景都跑了 10 组数据。这里的启动耗时也将 JVM 启动耗时考虑进去,比如 Jar package 场景下的耗时计算的是从 java -jar 启动 Java 应用直到应用启动就绪,所需的耗时。

从对比图可以看出,Native Executable 比 Jar package 的启动耗时降低了 12.4 倍,也就是启动速度提升了 12.4 倍。第二张图则是仅存在一个 Dubbo 服务 Consumer 的应用启动耗时对比,从对比图可以看出,Native Executable 比 Jar package 的启动快了 11 倍。由此看出集成 GraalVM Native Image 后,Dubbo 应用真正的能够实现毫秒级启动。

4.2、性能到达峰值时间

从统计图表上看,当 Consumer 和 Provider 都为 Native Executable 时,比他两都是 Jar package 的情况第一次调用的耗时足足减少了 6 倍。使 Dubbo 应用能够在启动后立即达到性能巅峰。启动耗时和启动即达到性能巅峰,以上两个技术红利也让 Dubbo 有机会拓展在 Serverless 或者说是 Faas 技术场景的应用范围。

4.3、内存消耗

在这里同样给出了 Provider 与 Consumer 应用在 Native Executable 与 Jar Package 两个场景下的内存对比统计图,拿第一张图,可以看到,Jar Package 启动后,所需的内存是 200 多兆,而 Native Executable 仅占了 60M 左右,在内存损耗上整整降低了 3.5 倍,在如今降本的大环境下,使得我们能够对 Java 应用降低内存使用,节省系统资源充满了更多的想象空间。

总结

Dubbo 的 AOT 技术通过与 GraalVM 和 GraalVM Native Image 工具的集成,实现了服务端和消费端的 AOT 编译,从而提升了性能和启动速度。经过技术演进的过程,Dubbo 的 AOT 技术逐渐成熟并被广泛应用。未来,随着 AOT 技术的进一步发展和应用,我们可以期待更多的框架和工具在提升 Java 生态系统性能方面取得突破,为开发者带来更好的开发体验和用户体验。

参考资料

1. 万物云原生下的服务进化 | 京东云技术团队:https://zhuanlan.zhihu.com/p/636986137

2. 启动速度提升 10 倍:Apache Dubbo 静态化方案深入解析:https://mp.weixin.qq.com/s/8OjLmixbpBSnnIjGsdD0kg

3. 走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解:https://segmentfault.com/a/1190000043988170

4. Spring AOT、Dubbo AOT 执行Native Image 打包示例:https://github.com/apache/dubbo-samples/tree/master/1-basic/dubbo-samples-native-image

文章转载自技术对话,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论