云原生时代,阿里怎么让 Java 冷启动提速两个数量级?

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
函数计算FC,每月15万CU 3个月
云解析 DNS,旗舰版 1个月
简介: Java 语言自从诞生以来就被打上了“慢”的标签。经过 25 年的发展,Java 程序的峰值性能在实时编译(JIT)技术的支持下已能超越 C/C++ 程序,但实时编译仍无法解决冷启动速度慢的问题。在云原生的场景下,往往需要快速拉起新的服务以响应用户持续增长的请求,此时服务的启动时间就显得至关重要。

嘉宾 | 林子熠

整理 | 李慧文

阿里巴巴探索了两种不同维度上的冷启动加速技术,经过双 11 大促的检验都取得了良好的效果:AppCDS 技术在传统 Java 环境的维度上,利用类数据共享特性改进启动速度和减少内存开销;静态编译技术则在更为激进的维度上,将 Java 程序提前编译为二进制机器码,实现以 Native Code 的速度启动 Java 程序,带来最多两个数量级的启动性能提升。在 QCon 全球软件开发大会(2021)北京站上,阿里巴巴 JVM 团队技术专家林子熠博士介绍了这两种技术的特点和适用场景。我们整理了本次分享,以期帮你实现云原生场景下 Java 快速冷启动(下文以林子熠老师第一人称叙述)

Java 诞生至今的 25 年里,凭借其峰值性能高、语言功能强、生态支持好等特点赢得了语言市场的霸主地位。但 Java 冷启动开销大,而云原生时代下的应用程序短小,启动频繁,冷启动问题的解决机不容发。

冷启动问题根因及应对

下图为典型 Java 应用的生命周期:

image.png

如图,Java 应用生命周期分为 5 个阶段:VM 初始化阶段、APP 初始化阶段、APP 初活跃阶段、APP 稳定执行期、结束阶段。

VM 初始化(图中红色)和 Class loading(图中蓝色)的开销为冷启动的根因。阿里巴巴实现了两类改造:一类为改良型技术,调整优化现有 Java 的框架和运行模型,另外一类为革新型的技术,摆脱原有 Java 框架另起炉灶。

EagerAppCDS

改良型技术中,阿里巴巴主要实现了基于传统 CDS(Class Data Sharing)的 EagerAppCDS。传统 CDS 包括 mark、Klass*、fields 三部分,如下图左所示:

image.png

Klass*指针指向内存中 class 实例 InstanceKlass,该实例为 bytecode Class Loading 解析后生成的,但在多次解析时内容均不变,因此可固定存储磁盘文件 Shared Archive 中,下次运行时从文件中可省略解析直接读取,实现提速。

面对 system class,CDS 可根据 name 快速匹配,但面对 customized class 时,JVM 无法辨认 customized class loader 的身份,因此需在 classpath 上扫描 jar 包,根据 name、crc 校验后方可完成匹配。jar 包即为 libs 包,包含大量 I/O 操作,开销大。EagerAppCDS 用 identity 固定了 customized class loader 的名字,可直接通过 identity+name 匹配找到所需 class。

下图为 EagerAppCDS 在阿里巴巴内部实践的脱敏数据,如图所示性能提升效果从 12%~95%不等。

image.png

EagerAppCDS 虽未开源但已在阿里云 SAE(Serverless 微服务 PaaS 平台)上线。线上可公开实测数据中应用启动耗时降低 5%~45%,提升效果与启动时加载类数量成正比。

image.png

除此之外,我们还实现了以下改进型技术:

  • JWarmup:共享预热后的 code cache,减小 JIT 开销;
  • PGO AOT:提前准备 runtime 信息,指导改进 AOT 代码质量;
  • Class Preinit:类预先初始化,降低运行时初始化类的开销。

Graal VM 静态编译技术

革新型技术中,阿里巴巴采用了基于 Graal VM 的静态编译技术。Graal VM 为 Oracle 主导的基于 Java 的开源高性能多语言平台:C++、Kotlin、python 等多种语言可通过 Truffle 框架运行在 GraalVM 上,Java 和其他 JVM 语言(如 Groovy、Kotlin 和 Scala 等)编译成 bytecode 后可直接运行。

Substrate VM(SVM)为 Graal VM 的静态编译组件,可将 Java 程序静态编译为可执行文件或共享库文件 Native Image,实现直接编译 Java 代码。

Java 最初依靠解释器实现无需编译实时执行;该解释器性能较差,因此引入了 JIT(Just In Time)实时编译技术,将高热度函数送到编译器中编译;为解决编译器开销大的问题,引入了 AOT(Ahead Of Time)编译,提前编译部分代码;AOT 缺乏 runtime 数据,运行后即丧失转为 JIT 的机会,运行速度慢;静态编译技术将 AOT 扩大,彻底摒弃 JVM,由 SVM 提供运行环境。

image.png

静态编译基本原理

传统 Java 执行模型如下图左所示:Application(应用本身)在 libs 的支持下运行在 JDK 上在 JVM 中执行。

image.png

静态编译在 Graal Compiler 编译器中编译 Application、libs、JDK,同时编译 Substrate VM Runtime,获得 Native Image。Native Image 包含 code(编译后的代码)和 Image heap(存储数据)两部分。Image heap 为运行时 heap 的起点,直接读取 Image heap 可以提高运行时的性能。

静态编译必须遵循封闭性原则(the closed-world assumption),即所有运行时信息均需在编译时可见。该原则带来两个基本问题:如何确定封闭的边界?如何处理 Java 的动态特性?

静态分析

Java bytecode 编译为 Native code 时,代码抽象性降低体积增大,如若编译所有代码,Native Image 体积将过于庞大,因此需确定封闭边界。SVM 通过静态分析上实现了从给定入口开始确定程序可达范围的功能。

该技术应用广泛,例如 main 函数调用 Virtue call 必须先明确其 type,type 和 Virtue call 有时可唯一绑定,但通常不能唯一绑定。此时使用静态分析技术,可明确 Virtue call type 的可能范围,实现封闭。

image.png

受静态分析本身的特性和能力所限,静态分析得到的可达代码集合(蓝色)略大于实际执行代码集合(绿色)。静态分析精度越高、冗余越少、image 越小。

静态分析无法分析出 Java 的许多动态特性运行时的行为,如反射、动态代理、JNI、序列化(阿里巴巴贡献,从 21.0 开始支持)、动态类加载(阿里巴巴贡献,patch 已经通过评审)等。此时需提前获取所需信息,方可封闭此类动态特性的触达范围——即需基于配置进行动态特性支持。

image.png

以反射为例。SVM 提供了 native-image-agent,可记录 APP 运行时所有的反射。编译时只需解析配置文件,即可注册反射目标,扩大编译范围;同时获取反射信息后可放入 ReflectionData 缓存中,将反射调用替换为直接调用。运行时如遇反射可查找 ReflectionData,获取目标值,通过 Method.invoke 直接调用目标函数。

下图为通过静态编译和传统 Java 两种方式,分别用反射调用空函数 30 次性能对比测试结果:

image.png

由于峰值过高,该图进行了对数修正。传统 Java 编译空函数耗时(深蓝色)为 3000ns,峰值由于反射开销为 4000ns,静态编译后(深蓝色)稳定在 150ns 内。

SVM 的静态编译实现的编译优化包括标准优化如:Method inlining, constant folding and arithmetic optimization, loop optimization, partial escape analysis 等。

此外还有因为静态分析而引入的新的编译优化,例如未被标为 final 的 field 通过静态分析发现只读不写,即可当作常量处理,做常量折叠等优化;又如,静态分析出某虚函数 type 唯一绑定,即可优化为直接调用,进而实现 inline;再如,消除部分编译时已知变量类型状态的类型检查和空指针检查。

主要运行时组件

静态编译由于所有的类均已被编译因此只有一个类加载器,实际只执行类查找功能。

传统 Java 一边检查异常一边运行,如遇异常直接处理即可。SVM 考虑到在不同平台兼容性,异常处理采用非信号处理机制:检测无错方可正常运行。该检测对性能影响小。

此外,静态编译的 GC 为 Oracle 开源版本中的单线程 stop-and-copy 顺序 GC,性能一般。

性能对比-实验室数据

下图为 Graal VM 官方的实验数据:

image.png

如上图所示,在只执行 Hello world 程序时,Native Image 性能次于 C,与 Go 相当,远快于传统 JDK;内存使用次于 C,只有 Go 的一半,远低于传统 JDK,具有高性能低内存占用的优点。图中红色数据为受测语言数据除以 Native Image 数据所得比值。

性能对比-实际场景数据

Javac 为 Java 编写的编译器:可以在 Java 程序中来调用 API 编译,也可用 stand alone 工具编译。通过 API 调用,实际上已完成 VM 启动,因此两者对比可观察冷启动带来的性能差异。

通过 API 调用 Javac 耗时 250ms,使用 Native Image 后耗时达到 35ms,实现了 1 个数量级的飞跃。

Javac 中使用的反射、Serverless 较少,其他项目静态分析性能提升效果更加显著。下图为基于 spring boot 的应用 greeting-service 部署在阿里云函数计算平台上的数据。greeting-service 收到请求会返回“ hallo”,功能简单但需要 spring boot 全流程支持。

image.png

如图所示,Native Image 静态编译(橙色)相比传统 Java(蓝色):内存占用从 128MB 降至 21MB;实际第一次调用耗时从 454 ms 降至 4.27 ms,提升了两个数量级;阿里云服务计费从 500ms 降至 100ms,事半功倍。

静态编译局限性

image.png

静态编译的局限性如上表所示:

  • 为实现封闭性,反射、动态代理、JNI、序列化、动态类加载均需要通过配置支持;
  • 不支持 InvokeDynamic(开发人员使用)、Method Handles(开发人员使用)、Security Manager、多 classloader、Finalizers、过时 Thread 函数(如 Thread.stop())等;
  • Java 程序被静态编译后不再保留 bytecode,因此存在监控、调试方面的问题:不支持 JVMTI、JMX、agent,只能使用 GDB 调试,无法通过 Eclipse IDE、IntelliJ IDEA 等调试。

GraalVM 生态发展

GraalVM 静态编译目前生态如下:

  • 阿里云:通过阿里云函数计算平台进行支持部署 serverless Native Image 应用,通过 Apache RocketMQ 为 C++客户端提供使用静态编译的 Java 共享库;
  • Spring 社区:发布了针对于静态编译 Spring-Native beta 版本,完全支持 Spring 的运算;
  • MICRONAUT:实现了支持 Native Image 的去反射微服务框架;
  • Facebook&Twitter:均在生产环境下使用 Graal 编译器代替 C2 编译器。

总之,在 Serverless 场景下 Java 的冷启动问题与应用对快速响应、实时扩展的需求形成突出矛盾。阿里巴巴一方面在现有技术上不断改进,最终形成突破:EagerAppCDS 提升最多 45%的启动速度;另一方面积极参与开源社区探索创新型的前沿技术,打磨成熟用于实践:GraalVM 静态编译技术最多提升百倍启动速度。但 GraalVM 存在兼容性和改造成本的问题,适合新项目。

嘉宾介绍

林子熠,目前主要负责 Java 静态编译技术的开发与应用。2017 年于上海交通大学获得软件工程专业工学博士学位,攻读博士期间曾赴美国伊利诺伊大学香槟分校访学。毕业后一直从事 Java 静态编译方面的工作,曾于华为编译器与编程语言实验室参与方舟 Java 移动端静态编译器的研发。加入阿里巴巴集团后主要研究服务端 Java 程序的静态编译技术,致力于 Serverless 场景下静态编译的规模化应用、编程模型和标准探索,以及生态建设。在 2019 年的双 11 大促时小范围上线实验了静态编译的 sofa-boot 中间件应用,取得启动速度提升 20 倍,峰值性能和内存消耗与传统 Java 应用基本持平的良好效果。

了解更多软件开发与相关领域知识,点击访问 InfoQ 官网:https://www.infoq.cn/,获取更多精彩内容!

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
目录
相关文章
|
4月前
|
Kubernetes Cloud Native Java
云原生之旅:从容器到微服务的演进之路Java 内存管理:垃圾收集器与性能调优
【8月更文挑战第30天】在数字化时代的浪潮中,企业如何乘风破浪?云原生技术提供了一个强有力的桨。本文将带你从容器技术的基石出发,探索微服务架构的奥秘,最终实现在云端自由翱翔的梦想。我们将一起见证代码如何转化为业务的翅膀,让你的应用在云海中高飞。
|
2月前
|
Kubernetes Cloud Native 流计算
Flink-12 Flink Java 3分钟上手 Kubernetes云原生下的Flink集群 Rancher Stateful Set yaml详细 扩容缩容部署 Docker容器编排
Flink-12 Flink Java 3分钟上手 Kubernetes云原生下的Flink集群 Rancher Stateful Set yaml详细 扩容缩容部署 Docker容器编排
91 3
|
3月前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
4月前
|
小程序 JavaScript Java
【资料】阿里Java开发手册
本文是关于分享阿里Java开发手册资源及促进编程规范学习的指南。作者以个人经历引入,讲述了公司领导通过细致讲解阿里Java开发手册,提升了团队对代码质量和编程规范的认识
1016 0
【资料】阿里Java开发手册
|
3月前
|
Kubernetes Cloud Native Java
探索未来编程新纪元:Quarkus带你秒建高性能Kubernetes原生Java应用,云原生时代的技术狂欢!
Quarkus 是专为 Kubernetes 设计的全栈云原生 Java 框架,凭借其轻量级、快速启动及高效执行特性,在 Java 社区脱颖而出。通过编译时优化与原生镜像支持,Quarkus 提升了应用性能,同时保持了 Java 的熟悉度与灵活性。本文将指导你从创建项目、编写 REST 控制器到构建与部署 Kubernetes 原生镜像的全过程,让你快速上手 Quarkus,体验高效开发与部署的乐趣。
55 0
|
4月前
|
Cloud Native 数据库 开发者
云原生数据库2.0问题之帮助阿里云数据库加速技术更新如何解决
云原生数据库2.0问题之帮助阿里云数据库加速技术更新如何解决
|
4月前
|
缓存 监控 Java
"Java垃圾回收太耗时?阿里HBase GC优化秘籍大公开,让你的应用性能飙升90%!"
【8月更文挑战第17天】阿里巴巴在HBase实践中成功将Java垃圾回收(GC)时间降低90%。通过选用G1垃圾回收器、精细调整JVM参数(如设置堆大小、目标停顿时间等)、优化代码减少内存分配(如使用对象池和缓存),并利用监控工具分析GC行为,有效缓解了高并发大数据场景下的性能瓶颈,极大提升了系统运行效率。
103 4
|
4月前
|
安全 Java 程序员
阿里开发手册 嵩山版-编程规约 (四)OOP规约-Java程序员必看知识点!!!
《阿里开发手册 嵩山版》的OOP规约部分强调了面向对象编程的最佳实践,包括正确使用静态方法、覆写方法的注解、可变参数的使用、接口的稳定性、equals和compareTo方法的使用、BigDecimal的正确比较、包装类与基本数据类型选择、POJO类的属性和方法设计等,以提升代码的质量和维护性。
|
5月前
|
Cloud Native 关系型数据库 Java
云原生数据仓库使用问题之如何安装PL/Java插件
阿里云AnalyticDB提供了全面的数据导入、查询分析、数据管理、运维监控等功能,并通过扩展功能支持与AI平台集成、跨地域复制与联邦查询等高级应用场景,为企业构建实时、高效、可扩展的数据仓库解决方案。以下是对AnalyticDB产品使用合集的概述,包括数据导入、查询分析、数据管理、运维监控、扩展功能等方面。
|
5月前
|
Java Docker 容器
Java演进问题之Java的资源占用如何解决
Java演进问题之Java的资源占用如何解决
下一篇
DataWorks