kl个人博客 首页>>quarkus>>阿里巴巴的GraalVM Native-image(10)

阿里巴巴的GraalVM Native-image(10)

阿里巴巴的GraalVM Native-image(10)

前言

这是阿里巴巴jvm团队供稿给graal官方的一篇博文,原文是英文,博主英文水平有限,这里直接Google机译成中文分享下。

原文地址:https://medium.com/graalvm/alibaba-at-scale-2944163c92e

背景

云计算旨在提供计算资源即服务,而云计算的核心原理是仅使用运行应用程序所需的那些资源,并在需要时进行扩展。为了利用云计算的优势,开发人员应根据此原则设计和编写应用程序。

微服务架构将整体应用程序分解为许多微应用程序(微服务)。对于针对云计算平台的应用程序来说,这是一种有吸引力的方法。我们可以从处理初始负载所需的微服务实例数量开始,并在需求较高时扩展更多实例,从而通过利用云的水平扩展能力来提高弹性。

Java平台已成为使用最广泛的平台之一。尽管Java广受欢迎,但它受到了许多批评,例如Java的启动速度很慢; Java占用太多内存; Java语法很冗长值得注意的是,Java中较长的启动时间抑制了水平可伸缩性。从业务的角度来看,客户可能必须等待很长时间才能启动应用程序,然后才能接收到请求结果。加快Java应用程序在水平可扩展平台上的启动时间是我们的动力。为此,我们在无服务器计算中采用了GraalVM本机映像

阿里巴巴的GraalVM本机映像

多年来,Java在阿里巴巴激增。许多应用程序都是用Java编写的。大约10,000名Java开发人员编写了超过十亿行Java代码!阿里巴巴已根据活跃的开源生态系统定制了大部分Java软件。在阿里云中,这些Java程序是为在线交易,支付和物流操作而开发的。它们中的许多都是作为微服务开发的,运行在Kubernetes本地环境之上,以服务于在线请求。

在阿里巴巴,我们使用GraalVM的本机映像技术将微服务应用程序静态编译为ELF可执行文件,从而缩短了Java应用程序的本机代码启动时间。这是解决上述水平缩放挑战所必需的。

在我们的场景中,此无服务器应用程序是基于SOFABoot框架开发的它的胖子大小为120MB +。包括了Java Enterprise空间中的许多典型组件,例如Spring,Spring Boot,Tomcat,MySQL-Connector等。我们将使用此框架的应用程序称为SOFABoot应用程序。SOFABoot应用程序最初运行在阿里巴巴Dragonwell(基于OpenJDK)的顶部,该设计用于分布式体系结构,处理在线事务并通过RPC与许多其他不同的应用程序进行通信。

在去年的全球在线购物节(也称为Double 11,即11月11日)中,我们部署了许多编译为本地映像SOFABoot应用程序他们成功地在一天中以最高交易量在我们的生产环境中满足了真正的在线请求。

除SOFABoot应用程序外,我们还探讨了将静态编译的应用程序引入阿里云的可能性。我们已阿里云功能计算平台成功部署了Micronaut演示应用程序的本机映像版本

在以下各节中,我们将介绍使用GraalVM本机映像进行静态编译以在生产环境中实现性能提升所克服的挑战。

我们如何做到的

GraalVM本机映像为开发人员提供了很多工具,以消除传统Java和静态编译Java之间的鸿沟,并提供了从前者迁移到后者的方法。在本节中,我们将重点介绍我们面临的挑战以及在阿里巴巴开发的用于将Java应用程序编译为本地映像的方法。我们还为GraalVM社区贡献了许多解决方案。

尽管本机映像支持大多数传统Java功能来构建和运行应用程序,但是仍然存在一些限制,阻止了从传统Java到静态编译Java程序的自动迁移。本机映像要求程序员提供其他信息或修改应用程序的原始实现,以使程序按预期方式编译和运行。在适应SOFABoot应用程序时我们面临的挑战是:

生成时间慢:静态编译会消耗大量内存资源和时间。建立时间很长。最初,构建SOFABoot应用程序需要大约100 GB的内存和4000秒的时间我们观察到在静态分析阶段,大部分时间都花在类型流分析上。因此,对于需要快速构建的场景,我们采用了一种不太精确但轻量级的CHA分析来代替原始类型流分析。在采用CHA方法之后,构建所需的内存从100GB减少到20GB,构建时间从4000秒减少到不到1000秒。我们很高兴看到构建时间加快了4倍,这有助于加快应用程序的部署。

类初始化在传统Java程序中在运行时初始化本机映像尽可能在构建时启用类初始化,以提高运行时性能。在构建时急于进行类初始化并不总是安全的,它仍然需要程序员手动调整类初始化时间。类初始化可能在链中发生,因此将一个类初始化推迟到运行时而不延迟其在链中的前任可能会在构建时导致类初始化错误。例如,以下代码具有A-> B-> C的类初始化链。

class A {
    static B b = new B();
}
class B {
static {
    C.dosomething();
}
}
class C {
  static long currentTime;
static {
    currentTime=System.currentTimeMillis();
}
static void dosomething(){…}
}

为了确保应用程序的正确性,由于对System.currentTimeMillis()的调用,必须在运行时初始化类C。结果,用户也必须在运行时对类A进行初始化,因为类A是该类初始化链的根-当类A被初始化时,它会触发B的初始化,最后是C的初始化。但是,在实际情况下,当开发人员发现类C在构建时被错误地初始化时,他们很难发现类A是问题的根本原因,即,开发人员错误地将类A配置为在构建时被初始化。本机映像提供了基于检测的初始化跟踪功能来解决此类问题,但是当无法检测该类时(例如,引导类加载器加载该类时),本机映像将失败。在我们的解决方案中,我们修改了Hotspot代码以在VM级别上跟踪类初始化链,并帮助我们的开发人员跟踪类初始化链并找出此类错误的根本原因。因此,我们的解决方案使Java开发人员可以更广泛地使用提前编译。

动态类加载:动态类加载是在运行时使用在构建时未知的类的字节码定义和加载类。动态类加载已在现实世界的应用程序,库和框架中广泛使用。一些典型的示例包括Java中的序列化/反序列化机制,该机制依赖于动态生成的构造函数访问器; Spring使用cglib作为代理; Derby使用动态生成的类用于SQL语句。我们通过4个步骤支持动态类加载:1)将生成的类的动态名称修改为固定名称。我们保证同一类在不同的运行过程中始终具有相同的名称。2)在本机映像代理中实现方法拦截器将具有固定名称模式的动态生成的类转储到文件系统。3)在构建时将转储的类编译为本地映像。4)在本地映像运行时查找准备好的“动态生成”的类,而不是对其进行定义。我们已经向社区承诺了此功能

GC性能缓慢

在静态编译的世界中,垃圾回收仍然是必不可少的组件。本机映像中的默认垃圾收集器是纯净的“复制” GC,它将堆空间分为两部分:年轻空间和旧空间。Java线程继续在年轻空间中分配对象,并且当年轻空间已满时,通过将所有活动对象从年轻空间移到旧空间来执行“ Young GC”。当旧空间已满时,通过压缩Java堆中的所有活动对象并释放可用空间来执行“完整GC”。

这种方法相对简单明了,并且对许多小型工作负载很有用,但是当我们尝试支持较大的工作负载(例如基于Spring的服务)时,完整的GC时间和频率变得令人头疼。我们观察到某些Java服务的单个GC暂停时间可能超过1.5秒。这对于在线应用程序是不可接受的。因此,我们对本地映像的垃圾收集器组件进行了一些改进,如下所示:

-将年龄信息添加到年轻一代的对象中年龄被添加到一组对象的内存块中,如果这些对象在年轻的GC中幸存下来,则年龄增加1;否则,年龄增加1。只有在达到一定年龄阈值后,活物才被提升为老一代。

使用后台线程异步取消映射内存本机映像使用内存块来保存Java对象。当它想向操作系统释放空闲块以减少占用空间时,它只是取消映射该块。我们观察到,对于典型的Java应用程序而言,取消映射的内存可能会花费很长时间,因此我们使该操作异步进行并在世界停顿之外执行它。

根据年轻GC中的卡表扫描图像根对于某些特定的工作负载,静态编译后最终的可执行映像可能很大,该映像通常拥有大量的GC根目录,因此必须彻底扫描所有GC。在本机图像垃圾收集器的现有设计中,这可能会花费很多时间。我们为图像根添加了一张卡片表,对于年轻的GC操作,我们仅扫描自上次GC暂停以来被弄脏的那些引用。

其中一些更改已提交到GraalVM项目中

绩效提升

启动时间加速

进行更改以解决使用本机映像的挑战后,我们在生产环境中收集了静态编译的SOFABoot应用程序的性能数据如图所示,启动时间从60秒减少到3秒,即Java应用程序的启动时间加快了20倍。此外,GC暂停时间控制在100毫秒以下。

Sofaboot应用程序启动时间比较

我们还在阿里云的功能计算平台上运行了基于Micronaut的应用程序的静态编译版本。结果也令人着迷。native_image_hello是一个静态编译的应用程序,并且springboot_hello与以jar形式部署并在传统Java运行时之上运行的应用程序相同。我们在下图中显示了结果:native_image_hello启动速度提高了100倍,内存成本为1/6,可以帮助客户节省80%(“计费时间”是客户在云平台上收费的时间)。这两个已部署应用程序的响应时间几乎相同。

传统Java函数与静态编译函数

GC性能

通过在GC中进行的上述增强,我们成功地将典型Java微服务的p90暂停时间从1.5秒以上减少到了约100毫秒。

GC时间改进

结论

如果您正在探索为云开发无服务器应用程序的方法,那么值得评估GraalVM本机映像,尤其是在寻找最佳启动性能和降低内存占用的情况下。

我们对生产环境中的结果感到非常满意。我们期待通过与GraalVM社区的合作来推动创新。


kl个人博客