Java致命伤:JVM的JIT在微服务快速交付时变成鸡肋 - astradot


Java的JVM JIT编译器存在一个假设前提:JVM是长时间运行的进程,基于这种假设才有JIT,但是持续交付以及由此导致的JVM频繁重启意味着这种假设前提却不存在了。
在Astradot,我们相信JVM的时代即将结束。我们正在用AOT编译语言编写后端,以便100%的时间为您提供出色的体验。我们最近将微服务从Kotlin转换为Go,发现这是一个受欢迎的变化。
 每个Java基准测试都会告诉您,在评估其性能之前,首先要多次运行代码以进行“预热”,以便通过JVM的C2编译器对其进行JIT和优化。
但是,在现实世界中,在“预热”代码之前对应用程序进行的调用在很大程度上是应用程序体验的一部分。
 
JVM JIT遭遇“注册”页面
Astradot拥有Kotlin微服务,可处理诸如注册和登录之类的身份验证活动。如果您在重新部署服务后立即尝试注册,则单击“注册”按钮后感觉注册页面已冻结了一样。该页面可能需要几秒钟才能响应。这是因为JVM第一次加载大量的Kotlin / Spring类的代码,并在没有优化的情况下通过解释器运行它。
当然,单击登录按钮越多,响应时间就会越长,但是第一次进行注册的用户可能会认为我们的系统已冻结并消失了。
由于每个微服务都有多个实例正在运行,因此您可能第二次尝试注册请求时,请求将转到另一个JVM实例。对于该JVM,这是第一次加载注册代码,因此您再次遇到冻结行为。从最终用户的角度来看,他现在尝试多次注册,并且每次遇到缓慢的行为。因此,这就是他现在对我们产品的印象。
 
持续交付破坏了JIT编译器的核心假设
Astradot的一项指标收集器服务每个JVM每秒获取500个请求。全新部署后,即使达到如此高的吞吐量,也要花费整整2个小时,JVM C2编译器才能完全优化该代码路径,以使响应时间降至最低。
为了说明这2个小时,以下是Sysdig最新容器使用情况调查的结果:

74%的容器寿命≤1小时。这改变了JIT编译器背后的核心假设,即JVM是一个长期运行的过程。在通过JVM C2编译器优化容器之前,将对其进行重新部署。因此,您的用户将永远无法体验到所有这些JVM基准测试所承诺的惊人性能。
对于低吞吐量的部分代码,情况会更糟。想想我之前提到的“注册”页面。即使我们的auth微服务已部署了好几天,signup()函数仍将无法获得足够的调用来触发C2编译器对其进行完全优化。因此,用户将始终体验该代码的未优化版本。
 
现代编译语言的兴起
JVM JIT编译器的卖点之一是它具有运行时信息,因此可以进行更好的优化。20年前可能是这样。但是从那以后,Ahead(AOT)编译语言得到了发展。Go(类似于Java一样被垃圾收集,但是经过AOT编译)可以实现类似或更好的性能。Rust能够在基准测试中始终击败Java。
这是由于Java的基本设计。它鼓励在堆上使用虚拟方法和分配。JIT优化的很大一部分围绕尝试将这些虚拟调用转换为静态调用,内联它们,执行转义分析以将那些堆分配转换为堆栈分配。
Go和Rust缺省情况下鼓励在任何地方使用静态方法调用和堆栈分配,因此它们不需要大型JIT的所有复杂性和开销即可在运行时对其进行优化。
 
AOT编译的Java
有迹象表明Java人员正在意识到JIT的危险。GraalVM具有AOT编译器,并且Quarkus和Micronaut之类的框架以使用它们。Java的动态特性意味着动态类加载,反射,代理等功能在AOT中不可用或受限制。生产Java应用程序通常还与依赖于运行时字节码检测的APM跟踪代理一起运行。整个JVM生态系统根本不是围绕AOT编译设计的。
具有25年历史的运行时生态系统以适应AOT编译的感觉就像在猪上涂上口红一样。使用Go和Rust等现代编译语言重新开始比较容易。