Jonas Kunz

将 Elastic 通用分析与 Java APM 服务和追踪相结合

了解如何将 Elastic 通用分析的强大功能与 Java 服务的 APM 数据相结合,以轻松查明 CPU 瓶颈。兼容 OpenTelemetry 和经典的 Elastic APM 代理。

Combining Elastic Universal Profiling with Java APM Services and Traces

之前的博文中,我们介绍了如何将 eBPF 分析数据与 APM 追踪相关联的技术细节。这次,我们将向您展示如何启动并运行此功能,以查明 Java 服务中的 CPU 瓶颈!OpenTelemetry 和经典的 Elastic APM 代理都支持这种关联。我们将向您展示如何为两者启用它。

演示应用程序

对于这篇博文,我们将使用 cpu-burner 演示应用程序来展示 Elastic 中 APM、追踪和分析的关联功能。此应用程序旨在持续执行多个 CPU 密集型任务

  • 它使用朴素的递归算法计算斐波那契数。
  • 它使用 SHA-2 和 SHA-3 哈希算法对随机数据进行哈希处理。
  • 它执行大量后台分配以强调垃圾收集器。

斐波那契数和哈希的计算都将作为 Elastic 中的事务可见:它们已使用 OpenTelemetry API 手动检测。

设置分析和 APM

首先,我们需要在演示应用程序将运行的主机上设置通用分析主机代理。从 8.14.0 版本开始,分析器默认支持与 APM 数据的关联并启用。无需特殊配置;我们只需遵循标准设置指南即可。请注意,在撰写本文时,通用分析仅支持 Linux。在 Windows 上,您必须使用 VM 来试用该演示。在 macOS 上,您可以使用 colima 作为 Docker 引擎,并在容器映像中运行分析主机代理和演示应用程序。

此外,我们需要使用 APM 代理检测我们的演示应用程序。我们可以使用经典的 Elastic APM 代理Elastic OpenTelemetry 分布

使用经典的 Elastic APM 代理

从 1.50.0 版本开始,经典的 Elastic APM 代理附带了将其捕获的追踪与通用分析的分析数据相关联的功能。我们只需通过 universal_profiling_integration_enabled 配置选项显式启用它即可。以下是使用启用设置运行演示应用程序的标准命令行

curl -o 'elastic-apm-agent.jar' -L 'https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&g=co.elastic.apm&a=elastic-apm-agent&v=LATEST'
java -javaagent:elastic-apm-agent.jar \
-Delastic.apm.service_name=cpu-burner-elastic \
-Delastic.apm.secret_token=XXXXX \
-Delastic.apm.server_url=<elastic-apm-server-endpoint> \
-Delastic.apm.application_packages=co.elastic.demo \
-Delastic.apm.universal_profiling_integration_enabled=true \
-jar ./target/cpu-burner.jar

使用 OpenTelemetry

该功能也可以作为 OpenTelemetry SDK 扩展使用。这意味着您可以将其用作 Vanilla OpenTelemetry 代理的插件,或者如果您不使用代理,则可以将其添加到您的 OpenTelemetry SDK。此外,该功能默认随附 Elastic OpenTelemetry Java 分布,并且可以通过任何可能的使用方法使用。虽然该扩展目前是 Elastic 特有的,但我们已经在与各种 OpenTelemetry SIG 合作,以标准化关联机制,尤其是在eBPF 分析代理已贡献之后。

对于此演示,我们将使用 Elastic OpenTelemetry Distro Java 代理来运行该扩展

curl -o 'elastic-otel-javaagent.jar' -L 'https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&g=co.elastic.otel&a=elastic-otel-javaagent&v=LATEST'
java -javaagent:./elastic-otel-javaagent.jar \
-Dotel.exporter.otlp.endpoint=<elastic-cloud-OTLP-endpoint> \
"-Dotel.exporter.otlp.headers=Authorization=Bearer XXXX" \
-Dotel.service.name=cpu-burner-otel \
-Delastic.otel.universal.profiling.integration.enabled=true \
-jar ./target/cpu-burner.jar

在这里,我们通过 elastic.otel.universal.profiling.integration.enabled 属性显式启用了分析集成功能。请注意,在即将发布的通用分析功能中,这将不再是必需的!然后,OpenTelemetry 扩展将自动检测分析器的存在,并基于此启用关联功能。

演示存储库还附带一个 Dockerfile,因此您也可以选择在 Docker 中构建和运行应用程序

docker build -t cpu-burner .
docker run --rm -e OTEL_EXPORTER_OTLP_ENDPOINT=<elastic-cloud-OTLP-endpoint> -e OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer XXXX" cpu-burner

设置到此为止;我们现在可以检查相关的分析数据了!

分析服务 CPU 使用率

我们现在可以做的第一件事是前往通用分析中的“火焰图”视图,并检查按 APM 服务过滤的火焰图。如果没有 APM 关联,通用分析只能过滤基础结构概念,例如主机、容器和进程。以下是一个截屏视频,显示了按我们的演示应用程序的服务名称过滤的火焰图

应用此筛选器后,我们得到了一个聚合了我们服务的所有实例的火焰图。如果不需要这样做,我们可以缩小筛选范围,例如基于主机或容器名称。请注意,相同的服务级别火焰图视图也可在 APM 服务 UI 中的“通用分析”选项卡上找到。

火焰图精确地显示了演示应用程序如何花费其 CPU 时间,而无需考虑是否被检测覆盖。从左到右,我们可以首先看到应用程序任务花费的时间:我们可以识别出未被 APM 事务覆盖的后台分配,以及 SHA 计算和斐波那契事务。有趣的是,这个应用程序逻辑只占总 CPU 时间的约 60%!剩余的时间主要花费在 G1 垃圾回收器上,这是由于我们的应用程序的高分配率所致。火焰图显示了所有与 G1 相关的活动以及并发任务各个阶段的时序。我们可以根据本地函数名称轻松识别这些活动。这得益于通用分析能够分析和符号化 JVM 的 C++ 代码以及 Java 代码。

精确定位事务瓶颈

虽然服务级别的火焰图已经很好地洞察了我们的事务在何处消耗了最多的 CPU,但这主要是因为演示应用程序的简单性。在实际应用中,很难精确定位某些堆栈帧主要来自某些事务。因此,APM 代理还在事务级别上关联来自通用分析的 CPU 分析数据。

我们可以导航到事务详情页面的“通用分析”选项卡,以获取每个事务的火焰图

例如,让我们看一下我们计算随机生成数据的 SHA-2 和 SHA-3 哈希的事务的火焰图

有趣的是,火焰图揭示了一些意想不到的结果:事务花费更多的时间计算要哈希的随机字节,而不是哈希本身!因此,如果这是一个实际应用程序,可能的优化方法可能是使用性能更高的随机数生成器。

此外,我们可以看到用于计算哈希值的 MessageDigest.update 调用分叉为两个不同的代码路径:一个是调用 BouncyCastle 加密库,另一个是 JVM 存根例程,这意味着 JIT 编译器已为某个函数插入了特殊的汇编代码。

屏幕截图中的火焰图显示了给定时间过滤器中所有“shaShenanigans”事务的聚合数据。我们可以使用顶部的事务过滤器栏进一步缩小范围。为了充分利用这一点,演示应用程序通过 OpenTelemetry 属性注释所使用的哈希算法的事务

public static void shaShenanigans(MessageDigest digest) {
    Span span = tracer.spanBuilder("shaShenanigans")
        .setAttribute("algorithm", digest.getAlgorithm())
        .startSpan();
    ...
    span.end()
}

因此,让我们根据使用的哈希算法过滤我们的火焰图

请注意,“SHA-256”是 JVM 内置 SHA-2 256 位实现的名称。现在得到以下火焰图

我们可以看到 BouncyCastle 堆栈帧消失了,而 MessageDigest.update 将所有时间都花在 JVM 存根例程中。因此,存根例程很可能是 JVM 维护人员为 SHA2 算法手工制作的汇编代码。

如果我们改为过滤“SHA3-256”,我们将得到以下结果

现在,正如预期的那样,MessageDigest.update 将所有时间都花在 BouncyCastle 库中用于 SHA3 实现。请注意,这里的哈希计算相对于随机数据生成花费了更多时间,这表明 SHA2 JVM 存根例程明显快于 BouncyCastle Java SHA3 实现。

这种过滤不限于此演示中显示的自定义属性。您可以根据任何事务属性进行过滤,包括延迟、HTTP 标头等等。例如,对于典型的 HTTP 应用程序,它允许根据有效负载大小分析所使用的 JSON 序列化器的效率。请注意,虽然可以基于单个事务实例(例如,基于 trace.id)进行过滤,但不建议这样做:为了允许在生产系统中进行持续分析,分析器默认以 20hz 的低采样率运行。这意味着,对于典型的实际应用程序,当查看单个事务执行时,这不会产生足够的数据。相反,我们可以通过随着时间的推移监控一组事务的多次执行并聚合其样本来获得见解,例如在火焰图中。

总结

应用程序性能下降的一个常见原因是 CPU 使用率过高。在这篇博客文章中,我们展示了如何将通用分析与 APM 结合使用,以找到这种情况下的实际根本原因:我们解释了如何在服务和事务级别上使用分析火焰图来分析 CPU 时间。此外,我们还使用自定义过滤器深入研究了数据。我们为此目的使用了一个简单的演示应用程序,所以请继续使用您自己的实际应用程序尝试一下,以发现该功能的实际强大之处!

分享这篇文章