Blog chevron_right Java

当 ReadyNow 只能在流量负载下进行编译时

When ReadyNow Can Only Compile on Traffic Loads

这是有关使用 ReadyNow 加快 Java 应用程序预热系列的第五篇博文。如果您还没有关注本系列,请返回第一篇博文,更快的 Java 预热:CRaC ReadyNow,了解一下。这篇文章探讨了如何解决在 ReadyNow 完成编译和 优化字节码之前流量加载并重定向到应用程序的情况。

ReadyNow 使应用程序能够在启动后不久将字节码编译为本机代码,从而以最佳方式处理负载。它还通过删除未使用的路径、内联等来优化编译后的代码。但在流量重定向到应用程序之前,这种编译和优化在特定情况下无法完全执行。

这篇文章探讨了这样一个场景,并为面临这一挑战的开发团队提供了详细的解决方案。

编译仅发生在流量负载上

在某些情况下,ReadyNow 无法实现预期的预热改进,并且许多编译仅在应用程序首次加载时执行。最常见的原因是流量触发代码第一次执行,并且某些类仅在此时加载和初始化。

下面的第一个图表显示了启动后不久第一组编译的完美示例,以及流量开始时编译器队列中的峰值 [图 1]。在该事件发生之前还剩下一段时间,在此期间,编译器有时间预编译代码并在需要时准备好,但无法完全执行此过程。

第二个图表显示了一个示例,其中大部分编译都是在启动后处理的,并且在流量加载时只需要进行极少量的编译 [图 2]。

ReadyNow 等待类加载

为了将字节码编译并优化为本机代码,ReadyNow 需要初始化类。这发生在“首次访问类”时,并执行诸如调用静态初始化器(类中的 static {} 块)和初始化所有静态变量之类的操作。类仅在需要时才由 JVM 中的类加载器加载。

因此,某些代码无法预先编译,而只能在系统生产负载首次需要时才进行编译。这阻碍了 ReadyNow 实现其主要目标,即在需要之前编译代码。

如何解决这个问题?

识别问题

解决方案很简单:创建一个预热脚本,在流量开始之前调用静态代码。这会加载必要的类,以便 ReadyNow 可以立即优化代码。找出需要调用的方法可能很困难,但日志和分析工具可以提供帮助。

在流量开始之前,可以通过 2 到 3 次迭代的迭代过程实现完全优化的系统,具体步骤如下: 

  1. 获取 Zing 垃圾收集器日志,请参阅 文档
  2. 从运行中获取 ReadyNow 输出文件,最好是从具有实际负载的生产系统中获取。可以通过以下方式完成:
    • 在系统本身上使用 -XX:ProfileLogOut=<path>。
    • 使用 Optimizer Hub 的 ReadyNow Orchestrator 服务时:
  3. 确定负载路由到应用程序的确切时间(X 秒处)。
  4. 在 GCLA 中打开垃圾收集器日志(添加链接),并使用“添加文件”在其上加载 ReadyNow 配置文件。
  5. 选择 ReadyNow 图表(例如,首次调用事件 -> 所有事件)并检查 RAW 数据。
  6. 搜索“CLASS_INIT”或“FIRST_CALL”,并仅筛选这些日志条目。
  7. 继续筛选日志条目(例如,先按组织名称,然后按应用程序名称。例如:“Acme”,然后按“Payroll”)。  或者,您可以筛选掉 lambda、反射、框架和其他非应用程序方法。
  8. 如有必要,请返回步骤 5 并重复操作。
  9. 获得足够小的列表后,检查从 X 秒开始的方法(如上文第 #2 点所述),并找到 1 个或 2 个方法添加到启动脚本中。

调用已识别的代码

确定流量启动后会调用的方法后,您可以创建脚本或自动化程序来触发所有必需代码的编译。根据您的用例,这可以是 Java、Bash、Python 等语言编写的脚本,其中包含模拟请求、集成测试或任何其他触发应用程序调用这些方法的解决方案。

就绪检查

此时,我们知道必须调用哪些方法,并有一个解决方案来执行这些方法以触发代码编译。下一步是了解编译何时完成,以便我们可以将流量发送到应用程序。为了最大限度地提高应用程序的使用率,我们希望最大限度地缩短最后一次编译和流量开始之间的时间。

在 Zing 24.06.0.0 中,MXBean 扩展新增了几种方法,这些方法可以从正在运行的 JVM 中请求多个指标,帮助您确定应用程序何时准备好处理流量:

  • getTotalOutstandingCompiles()
  • getTotalPerformedTier1Compiles()
  • getTotalPerformedTier2Compiles()

这些方法返回已排队和正在进行的编译的总数,并可以返回请求时第 1 层和第 2 层编译的总数。同样,您可以根据您的用例定义阈值来判断应用程序是否已准备就绪。

有关更多信息,请参阅 Zing MXBeans 文档

Spring Petclinic 示例

让我们使用著名的 Spring Petclinic 演示 应用程序来说明问题和解决方案。借助此项目中包含的 JMeter 测试,我们可以模拟应用程序的负载,并使用垃圾收集器日志文件检查编译队列。

在此示例中,应用程序启动后,在一段固定时间后启动 JMeter 测试,测试完成后,再等待一段时间,测试将再次执行。

不使用 ReadyNow 配置文件运行

在第一次运行中,我们没有提供 ReadyNow 配置文件。这意味着 Zing 运行时没有可用的编译信息,无法预先执行任何编译。正如您在图表中看到的,在应用程序启动时,编译器队列中出现了一个小的峰值,因为“基本”类被加载了。当第一个 JMeter 测试启动时,模拟应用程序流量负载的开始,编译器队列数量出现了显著的峰值。当第二个测试执行时,峰值要小得多。

这表明,当我们使用 JMeter 测试作为“虚拟”负载时,我们已经可以让应用程序做好处理流量的准备 [图 3]。

图 3:+第 1 层峰值 +第 1 层活跃 +第 2 层峰值 +第 2 层活跃 +暂存区查找峰值 +暂存区查找活跃 +CNC 队列峰值 +CNC 队列活跃 +第 2 层队列热排名峰值 +第 2 层队列热排名活跃 +第 2 层队列温热排名峰值 +第 2 层队列温热排名活跃放大

使用 ReadyNow 配置文件运行

在执行上述测试时,我指示应用程序使用 -XX:ProfileLogOut=readynow.log 生成 ReadyNow 配置文件并将其用作第二次测试的输入。不出所料,我们获得了预期的效果:应用程序启动后,编译器队列立即出现更大的峰值,这使得大多数代码能够以其原生格式运行,这要归功于 ReadyNow。通过第一次 JMeter 测试,我们可以触发剩余方法的编译,作为改进的预热步骤,并让应用程序做好应对实际流量的准备。如您所见,在第二次测试运行时,编译器队列的数量进一步减少,这表明应用程序已做好充分准备来处理实际流量 [图 4]。

图 4:在第二次测试运行时,编译器队列的数量有所减少。放大

分代 ReadyNow 配置文件

正如之前的博文“如何训练 ReadyNow 以实现最佳 Java 性能”中所解释的,ReadyNow 可以将配置文件作为输入并输出新一代,为下次运行提供更多、更完善的信息。在这篇博文的测试中,我正是这么做的。

您看到的下方图表是第三代配置文件的结果。下方是第一、第二和第三个配置文件的结果。如您所见,初始编译器队列在启动后立即出现峰值,并且随着 ReadyNow 获得有关待编译代码的更多信息而不断增加 [图 5]。更重要的是,当流量从第二次运行 JMeter 测试开始出现时,峰值会变得更小。

结果

由于这些“缺失”的类现在会在启动后通过预热脚本加载并初始化,因此 ReadyNow 可以从启动时或在实际流量开始之前执行其预期任务,并且可以预先编译所有必需的原生代码。这会将编译移至应用程序的最开始部分,但仍需要一些时间才能完成。具体的时长可以通过测试并结合 Readiness Probe 来确定,以便 Zing 中的 Falcon 编译器 或 Optimizer Hub 中的云原生编译器 完成大多数优化。 

根据许多客户生产系统的经验,通常无需在启动时实现 100% 的第 2 层优化。最重要的编译将首先完成,对于大多数客户来说,在流量开始后进行后续优化是可以接受的。

结语

未初始化的类有时会妨碍 ReadyNow 缩短 Java 应用程序预热时间的效果。通过了解这一限制并识别和主动调用静态代码,您可以充分利用 ReadyNow 的全部功能,从启动开始优化应用程序性能。

通过遵循这些最佳实践并正确配置 ReadyNow 和应用程序启动流程,组织可以在启动时间和运行时性能之间实现最佳平衡,确保其 Java 应用程序在生产流量到来时真正“立即就绪”。

要阅读整个 ReadyNow 系列,请从头开始阅读以下博客文章:

更快的 Java 预热:CRaC ReadyNow
ReadyNow 如何缩短 Java 预热时间
如何训练 ReadyNow 以实现最佳 Java 性能
使用 Optimizer Hub 提升 Java 应用程序的启动和编译速度
当 ReadyNow 只能在流量负载下进行编译时