关于云原生:基于静态编译构建微服务应用

7次阅读

共计 6938 个字符,预计需要花费 18 分钟才能阅读完成。

作者:饶子昊(铖朴)

Java 的局限性

传统的一个 Java 利用从代码编写到启动运行大抵能够分为如下步骤:

  1. 首先,编写 .java 源代码程序。
  2. 而后,借助 javac 工具将 .java 文件翻译为 .class 的字节码,字节码是 Java 中十分重要的内容之一,正是因为它的呈现,Java 才实现对底层环境的屏蔽,达到 Write once, run anywhere 的成果!
  3. 基于步骤 2 的 .class 文件会被打包成 jar 包或者 war 包进行部署执行,部署过程中通过 Java 虚拟机加载应用程序而后解释字节码运行业务逻辑。

整个过程如下图所示:

图 1:Java 程序运行过程

上述过程既给 Java 程序带来了其余编程语言不具备的劣势,比方跨平台,易上手等。但同时也给 Java 程序带来了一些性能问题,比方启动速度慢和运行时内存占用低等。

冷启动问题

图 1 中 Java 程序启动运行具体过程如下图 2 所示:

图 2:Java 程序的启动过程剖析 [ 1]

一个 Java 利用启动过程首先须要加载该应用程序对应的 JVM 虚拟机软件程序到内存中,如上图红色局部形容所示。而后 JVM 虚拟机再加载对应的应用程序到内存中,该过程对应上图中的浅蓝色类加载(Class Load,CL)局部。在类加载过程中,应用程序就会开始被解释执行,对应上图中浅绿色局部。解释执行过程 JVM 对垃圾对象进行回收,对应上图中的黄色局部。随着程序的运行的深刻,JVM 会采纳及时编译(Just In Time,JIT)技术对执行频率较高的代码进行编译优化,以便晋升利用程序运行速度。JIT 过程对应上图中的红色局部。通过 JIT 编译优化后的代码对应图中深绿色局部。

通过上述剖析,不难看出,一个 Java 程序从启动到达到被 JIT 动静编译优化会通过 VM init,App init 和 App active 几个阶段,相比于其余一些编译型语言,其冷启动问题比较严重。

运行时内存占用高问题

除了冷启动问题,从上述剖析中不难看出,一个 Java 程序运行过程中,什么都不做首先就须要加载一个 JVM 虚拟机,该操作个别占用肯定内存。另外,因为 Java 程序是先解释执行字节码,而后再做 JIT 编译优化。

因为相比于一些编译型语言其将编译优化的动作后置到运行时,因而非常容易呈现理论加载的代码比理论须要运行的代码多很多的状况,造成了一些有效内存占用状况。综上所述就是为什么很多人常诟病 Java 程序运行内存占用高的几点次要起因。

更轻量化的 Java 程序

动态编译技术

既然,先解释执行再动静编译的 Java 传统程序运行形式存在上述诸多问题,那有没有一些形式能够让 Java 程序也跟其余程序语言,比方 C/C++ 一样,先编译后执行解决上述问题呢?

答案是必定的,提前编译(Ahead-of-Time Compilation,AOT Compilation)或者叫动态编译在 Java 畛域很早就被提了进去。其核心思想就是将 Java 程序的编译阶段提前到程序启动前,而后在编译阶段进行代码编译优化,让程序启动既巅峰,打消冷启动,升高运行时内存开销。

Java 畛域动态编译的实现技术有很多,其中最具代表性的还属 Oracle 推出的 GraalVM 开源高性能多语言运行时平台 [ 2]。看到这里有的读者可能会问:“高性能多语言运行时平台是什么?它跟动态编译自身有什么关系?”。

图 3:GraalVM 多语言运行时平台

如上图 3 所示,GraalVM 中通过提供 Truffle 解释器实现框架,让开发人员能够应用 Truffle 提供的 API 疾速实现特定语言的解释器从而实现对上图中各种编程语言所写的程序都能进行编译运行的成果,从而成为一个多语言运行时平台。GraalVM 实现动态编译能力的编译器就是 GraalVM JIT Compiler。动态编译框架和运行时由 Substrate VM 子项目实现,兼容 OpenJDK 运行时实现,提供了原生镜像程序运行时的异样解决、同步调度、线程治理、内存治理等性能。

因而,GraalVM 不仅能够作为一个多语言运行时平台,而且因为其中提供的 GraalVM JIT Compiler 动态编译器,其可用来对 Java 程序进行动态编译。

说完动态编译和 GraalVM 之间的关系,有的读者可能会好奇,基于 GraalVM 的动态编译与惯例的 JVM 解释执行形式有哪些区别?基于动态编译的 Java 程序相比于目前利用宽泛的 JVM 运行时编译 Java 程序整个从代码编写到编译执行的区别如下图 4 所示:

图 4:动态编译与传统 JVM 运行过程比照

相比于 JVM 运行时形式,动态编译在运行之前会先对程序解析编译,而后生成一个跟运行时环境强相干的 native image 可执行文件,最初间接执行该文件即可启动程序进行执行。

说到这里可能有的读者又好奇,上图 4 中的动态编译过程到底会对 Java 程序做哪些解析操作?动态编译后的可执行程序垃圾回收问题怎么解决?如下图 5 所示,其形容了 GraalVM 动态编译技术实现中编译过程的输出与输入内容。

图 5:动态编译输入输出

图 5 中左侧前三个输出内容 Applicaton,Libraries 和 JDK 是一个 Java 程序编译运行必备的三局部,不用多说。而 Substrate VM 就是 GraalVM 中实现动态编译的外围局部,在整个动态编译过程中表演了重要作用。

其中在动态剖析过程中,如上图 5 两头局部中所绘制,Substrate VM 通过上下文不敏感的指向剖析(Points-to Analysis)来对应用程序做动态剖析,其能够在不须要运行程序的状况下,基于源程序剖析给出所有可能的可达函数列表而后作为后续编译阶段的输出对程序进行动态编译。该过程因为动态剖析的局限性,无奈笼罩 Java 中的反射、动静代理、JNI 调用等动静个性。这也造成了很多的 Java 框架因为在实现过程中应用了大量的上述个性,因而,都难以间接基于 Substrate VM 实现对本身所有代码的动态剖析,须要通过额定的内部配置 [ 3] 来解决动态剖析自身的有余。

例如像 Spring 社区因而开发了 AOT Engine [ 4] 如下图 6 所示来帮忙解决 Spring 我的项目对其中的反射,动静代理等内容进行动态剖析解决并将其转换为 Substrate VM 能在编译阶段可辨认的内容,确保对 Spring 利用可基于 Substrate VM 顺利完成动态编译。

图 6:Spring AOT Engine

在动态剖析实现后,基于动态剖析后果的可达函数列表,调用上文介绍的 GraalVM 中的 GraalVM JIT Compiler 编译器将应用程序编译为与指标平台强相干的本地代码以实现编译过程。

编译实现后,就会进入到上图 5 右侧 Native 可执行文件生成阶段。在该过程中,Substrate VM 会将动态编译阶段确定和初始化的内容以及跟 Substrate VM 运行时以及 JDK 库中的数据一起保留到最终可执行文件的 Image Heap 中。其中 Substrate VM 运行时就为最终可执行文件提供了运行过程中所需的垃圾回收、异样解决等能力。对于垃圾回收这块,在一开始的 GraalVM 社区版中仅提供了 Serial GC。企业版中提供了能力更强的 G1 GC。不过在最新的社区版中 GraalVM 团队也引入了 G1 GC [ 5] 以便为宽广开发者提供更弱小的动态编译应用能力。

适配 GraalVM 动态编译

上节,简略介绍了动态编译技术以及其自身的局限性当前,很多内部社区开发者这时可能会疑难,一个 Java 开源我的项目如何疾速进行动态编译适配?对于这个问题,其实最外围要解决的实质问题就是将开源框架中的 GraalVM 无奈辨认和解决的动静内容转换为其可辨认的内容即可。因而该问题因为不同框架状况不一样,因而解决形式也会有一些差别。例如在 Spring 中,针对其本身框架开发的 AOT Engine 能够解决其框架提供的通过 @Configuration 注解注册类初始化过程无奈在动态编译阶段被辨认、提前在动态编译期生成本来在运行阶段能力生成的动静代理类解决间接动态编译代理类无奈被无效生成等问题 [ 6] 从而实现 Spring 利用的动态编译适配。

对于很多基于 Spring 实现的开源框架,如果自身无奈被 GraalVM 辨认的动静个性都是因为 Spring 规范的那一套用法所导致,因为本身属于 Spring 体系,动态编译过程就必定少不了 Spring AOT Engine 的参加,因而,框架本身就不须要再提供任何适配就能够具备动态编译能力。

对于非 Spring 体系我的项目或者本身应用了一些 JDK 中原生的反射或者其余 Java 动静个性,针对本身代码中的 Java 动静用法须要在我的项目中提供对应的动态配置文件能力在动态编译过程中让编译器辨认其中的动静个性,对其进行编译构建能力实现我的项目的顺利编译与执行。针对这种状况,GraalVM 提供了一个名叫 native-image-agent 的 Tracing Agent 来帮忙大家更不便地收集元数据并筹备配置文件。该 Agent 会在惯例 Java VM 上的利用程序运行过程中主动收集其中的动静个性应用状况并将其转换为 GraalVM 能够辨认的配置文件。最初,将通过 Agent 生成的框架本身的动静配置文件寄存在我的项目的:META-INF/native-image/<group.id>/<artifact.id> 目录下,就能够在动态编译过程中依据这些配置内容,辨认我的项目包中的动静个性。

Spring Cloud Alibaba 2022.0.0.0 版本所蕴含的所有中间件客户端目前已实现了构建 GraalVM 原生利用的适配。因为我的项目本身的特定,我的项目整体实现中有大量的 Spring 语法导致的无奈被 GraalVM 辨认的动静个性用法,这块内容间接交由 Spring AOT Engine 来进行解决,社区未做额定适配工作。

除了 Spring 体系语法,我的项目自身还是有一些其余 Java 动静用法的,这块社区通过 native-image-agent 来进行解析与动静配置生成。

基于动态编译构建微服务

Spring Cloud Alibaba 2022.0.0.0 版本所蕴含的所有中间件客户端已实现了构建 GraalVM 原生利用的适配。为用户提供了开箱即用的动态编译能力。相干性能体验过程如下:

环境筹备

首先须要在首先在机器上装置 GraalVM 发行版。您能够在 Liberica Native Image Kit 页面上手动下载它,也能够应用像 SDKMAN! 这样的下载管理器。本文演示环境为 MacOS,如果是 Windows 可参考相应文档 [ 7] 进行操作。执行以下命令装置 GraalVM 环境:

$ sdk install java 22.3.r17-nik
$ sdk use java 22.3.r17-nik

通过查看 java -version 的输入来验证是否配置了正确的版本:

$ java -version
openjdk version "17.0.5" 2022-10-18 LTS
OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode)

利用构建

要应用 GraalVM 动态编译能力构建微服务,首先确保您我的项目的 Spring Boot 版本为 3.0.0 或以上,Spring Cloud 版本为 2022.0.0 或以上。而后在我的项目中引入 Spring Cloud Alibaba 2022.0.0.0 版本的所需模块依赖即可。

通过以下命令生成利用中反射、序列化和动静代理所需的 Hints 配置文件,前提是利用中引入了 spring-boot-starter-parent 父模块:

$ mvn -Pnative spring-boot:run

之后利用会启动,进行预执行,须要尽可能残缺的测试一遍利用的所有性能,保障利用的大部分代码都被测试用例笼罩,该过程会基于 GraalVM 的 native-image-agent 收集程序中的动静个性,这样能够确保残缺生成利用运行过程中的所有必须的动静属性。运行完所有测试用例后,咱们发现 resource/META-INF/native-image 目录下会生成以下一些 hints 文件:

  • resource-config.json:利用中资源 hint 文件
  • reflect-config.json:利用中反射定义 hint 文件
  • serialization-config.json:利用中序列化内容 hint 文件
  • proxy-config.json:利用中 Java 代理相干内容 hint 文件
  • jni-config.json:利用中 Java Native Interface(JNI)内容 hint 文件

注意事项:Spring Cloud Alibaba 2022.0.0.0 正式版本所有外围模块都曾经默认将本身组件相干动静个性所需的配置内容都蕴含在了依赖中,因而上述预执行过程次要为了扫描利用本身业务代码以及其余第三方包中的动静个性,以便后续动态编译过程能顺利进行,利用能失常启动。

动态编译

以上步骤所有准备就绪后,通过以下命令来构建原生镜像:

$ mvn -Pnative native:compile

胜利执行后,咱们在 /target 目录能够看到生成的可执行文件。

程序运行

与一般可执行文件无异,通过 target/xxx 启动利用, 能够察看到相似如下的输入:

2023-08-01T17:21:21.006+08:00  INFO 65431 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-08-01T17:21:21.008+08:00  INFO 65431 --- [main] c.a.cloud.imports.examples.Application   : Started Application in 0.553 seconds (process running for 0.562)

采纳 GraalVM 动态编译技术的新版本 Spring Cloud Alibaba 利用,所有外围能力在启动速度和内存占用率方面如下表所示都有显著改善。

阐明: 上述测试代码样例来自 Spring Cloud Alibaba 我的项目中的 examples 模块,4c16g Mac 环境,每组数据测试 3 次取均匀,具体数据因机器不同可能会有差别。

其余社区动静

新官网上线

从 2018 年的开源,到 2019 年第一个 GA 版本公布,社区曾经走过了 5 个年头,我的项目公布了 35 个版本,累计 Star 数目超过 26k,Spring Cloud Alibaba 微服务解决被宽泛地利用在各行各业的数字化转型过程中。

因为 Spring Cloud Alibaba 我的项目是作为跟 Spring 官网共建的一个基于 Spring Cloud 微服务解决方案规范,融入了阿里巴巴数十年微服务技术教训的一套微服务技术计划实现。我的项目晚期的文档等相干内容始终都微服务在 spring.io 官网当中。但缓缓地社区也发现,spring.io 中因为无奈增加中文内容,我的项目太多导致单个我的项目所能承载的我的项目或社区内容较少等问题。

因而,社区通过跟 Spring 官网沟通,在他们的反对下,基于国内开发者习惯,社区的全新社区官网正式上线。相干域名为:sca.aliyun.com,其中 sca 是我的项目的 Spring Cloud Alibaba 的首字母缩写。

新 Committer 介绍

Spring Cloud Alibaba 社区近几个月涌现了一些积极参与社区保护迭代的内部贡献者,在此,向他们表示感谢!另外,对于其中始终参加社区活动,做出重要奉献 feature 的刘子明,社区依照新 Committer 提名与投票制度正式提名其为社区 Committer 并投票通过,胜利入选,在此也向其表示祝贺!欢送更多内部同学关注 Spring Cloud Alibaba 开源社区和奉献开源社区。

相干链接:

[1] Java 程序的启动过程剖析
https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

[2] GraalVM 开源高性能多语言运行时平台 https://www.oracle.com/java/graalvm/

[3] 内部配置 https://www.graalvm.org/latest/reference-manual/native-image/…

[4] AOT Engine
https://spring.io/blog/2021/12/09/new-aot-engine-brings-sprin…

[5] G1 GC
https://medium.com/graalvm/a-new-graalvm-release-and-new-free…

[6] 动静代理类等问题
https://docs.spring.io/spring-boot/docs/current/reference/htm…

[7] 相应文档
https://medium.com/graalvm/using-graalvm-and-native-image-on-…

正文完
 0