Luca WintergerstTim Rühsen

Elastic 通用分析:提高性能并降低成本

在本博客中,我们将介绍我们的一位工程师如何通过一项发现,为我们的 QA 环境节省了数千美元的成本,并在将此更改部署到生产环境后节省了更多的成本。

阅读时间 12 分钟
Elastic Universal Profiling: Delivering performance improvements and reduced costs

在当今云服务和 SaaS 平台时代,持续改进不仅仅是一个目标,更是一种必然。在 Elastic,我们始终在寻找微调系统的方法,无论是内部工具还是 Elastic Cloud 服务。我们最近在 Elastic Cloud QA 环境中进行的性能优化调查,在 Elastic 通用分析 的指导下,就是一个很好的例子,说明了我们如何将数据转化为可操作的见解。

在本博客中,我们将介绍我们的一位工程师如何通过一项发现,为我们的 QA 环境节省了数千美元的成本,并在将此更改部署到生产环境后节省了更多的成本。

Elastic 通用分析:我们首选的优化工具

在我们的用于解决性能挑战的解决方案套件中,Elastic 通用分析是一个关键组件。作为利用 eBPF 的“始终在线”分析器,它可以无缝集成到我们的基础架构中,并系统地收集我们整个系统的全面分析数据。由于无需进行零代码检测或重新配置,因此可以轻松地将其部署在我们云中的任何主机(包括 Kubernetes 主机)上——我们已将其部署在整个 Elastic Cloud 环境中。

我们所有的主机都运行分析代理来收集此数据,这使我们可以详细了解我们正在运行的任何服务的性能。

发现机会

这一切都始于对我们的 QA 环境进行的一次看似例行的检查。我们的一位工程师正在查看分析数据。在通用分析的帮助下,这个初步发现相对较快。我们发现了一个未优化的函数,该函数具有高昂的计算成本。

让我们逐步了解一下。

为了发现高成本的函数,我们可以简单地查看 TopN 函数的列表。TopN 函数列表显示了我们在运行的所有服务中使用 CPU 最多的所有函数。

为了按其影响对其进行排序,我们按“总 CPU”降序排序

  • 自身 CPU 衡量函数直接使用的 CPU 时间,不包括在其调用的函数中花费的时间。此指标有助于识别自身使用大量 CPU 功率的函数。通过改进这些函数,我们可以使它们运行得更快并使用更少的 CPU。

  • 总 CPU 累加函数及其调用的任何函数使用的 CPU 时间。这完整地描述了一个函数及其相关操作使用多少 CPU。如果一个函数的“总 CPU”使用率很高,可能是因为它调用了其他使用大量 CPU 的函数。

当我们的工程师查看 TopN 函数列表时,一个名为“... inflateCompressedFrame …”的函数引起了他们的注意。这是一种常见的情况,其中某些类型的函数经常成为优化的目标。以下是关于查找内容和可能改进的简化指南

  • 压缩/解压缩:是否有更高效的算法?例如,从 zlib 切换到 zlib-ng 可能会提供更好的性能。

  • 加密哈希算法:确保正在使用最快的算法。有时,根据安全要求,可以使用更快的非加密算法。

  • 非加密哈希算法:检查您是否正在使用最快的选项。例如,xxh3 通常比其他哈希算法更快。

  • 垃圾回收:尽量减少堆分配,尤其是在常用路径中。选择不依赖于垃圾回收的数据结构。

  • 堆内存分配:这些通常是资源密集型的。考虑使用 jemalloc 或 mimalloc 等替代方法而不是标准 libc malloc() 来减少它们的影响。

  • 页面错误:留意 TopN 函数或火焰图中是否有“exc_page_fault”。它们指示可能需要优化内存访问模式的区域。

  • 内核函数过多的 CPU 使用率:这可能表示系统调用过多。使用较大的缓冲区进行读取/写入操作可以减少系统调用的次数。

  • 序列化/反序列化:诸如 JSON 编码或解码之类的过程通常可以通过切换到更快的 JSON 库来加速。

识别这些区域可以帮助精确定位可以显著提高性能的地方。

从 TopN 视图中单击该函数会在火焰图中显示它。请注意,火焰图显示的是来自整个云 QA 基础结构的样本。在此视图中,我们可以看出,仅此函数就占用了我们 QA 环境的这一部分每年超过 6,000 美元的费用。

在按线程过滤后,该函数的作用变得更加清晰。下图显示了此线程在 QA 环境中运行的所有主机上的火焰图。

除了查看所有主机上的线程外,我们还可以查看仅针对一个特定主机的火焰图。

如果我们一次查看一个主机,我们可以看到影响更加严重。请记住,之前的 17% 是针对整个基础架构的。某些主机甚至可能没有运行此服务,因此会降低平均值。

将范围缩小到运行该服务的单个主机,我们可以看出该主机实际上将其近 70% 的 CPU 周期用于运行此函数。

仅此一个主机的美元成本就相当于每年大约 600 美元。

了解性能问题

在识别出潜在的资源密集型函数后,我们的下一步是与我们的工程团队合作,以了解该函数并研究潜在的修复方法。以下是我们方法的直接分解

  • 理解函数的功能: 我们首先分析了该函数应该执行的操作。它使用 gzip 进行解压缩。这个发现使我们简要考虑了之前提到的降低 CPU 使用率的策略,例如使用更高效的压缩库(如 zlib)或切换到 zstd 压缩。
  • 评估当前的实现: 该函数目前依赖于 JDK 的 gzip 解压缩,预计会在底层使用本地库。我们通常首选 Java 或 Ruby 库(如果可用),因为它们简化了部署。直接选择本地库需要我们管理每个操作系统和我们支持的 CPU 的不同本地版本,这会使我们的部署过程复杂化。
  • 使用火焰图进行详细分析: 对火焰图的仔细检查表明,系统遇到了页面错误,并且花费了大量的 CPU 周期来处理这些错误。

让我们从理解火焰图开始

最后几个非 jdk.* JVM 指令(绿色)显示了由 Netty 的 DirectArena.newUnpooledChunk 启动的直接内存字节缓冲区的分配。直接内存分配是代价高昂的操作,通常应避免在应用程序的关键路径上使用。

Elastic AI Assistant for Observability 在理解和优化火焰图的各个部分也很有用。特别是对于不熟悉 Universal Profiling 的用户来说,它可以为收集的数据添加大量上下文,使用户更好地理解它们并提供潜在的解决方案。

Netty 的内存分配

Netty 是一个流行的异步事件驱动网络应用程序框架,它使用 maxOrder 设置来确定为其应用程序中管理对象分配的内存块的大小。计算块大小的公式是:chunkSize = pageSize << maxOrder。假设页面大小为 8KB,则默认 maxOrder 值 9 或 11 会导致默认内存块大小分别为 4MB 或 16MB。

对内存分配的影响

Netty 采用 PooledAllocator 来进行高效的内存管理,它会在启动时在直接内存池中分配内存块。此分配器通过重用小于定义的块大小的对象的内存块来优化内存使用。任何超过此阈值的对象都必须在 PooledAllocator 之外进行分配。

在池化上下文之外分配和释放内存会因以下几个原因而导致更高的性能成本

  • 增加分配开销: 大于块大小的对象需要单独的内存分配请求。与较小对象的快速池化分配机制相比,这些分配更耗时且资源密集。
  • 碎片化和垃圾回收 (GC) 压力: 在池外分配较大的对象可能会导致内存碎片化增加。此外,如果这些对象分配在堆上,则可能会增加 GC 压力,从而导致潜在的暂停和应用程序性能下降。
  • Netty 和 Beats/Agent 输入: Logstash 的 Beats 和 Elastic Agent 输入使用 Netty 接收和发送数据。在处理接收到的数据批次期间,解压缩数据帧需要创建一个足够大的缓冲区来存储解压缩后的事件。如果此批次大于块大小,则需要一个未池化的块,从而导致直接内存分配,从而降低性能。通用分析器使我们能够从火焰图中的 DirectArena.newUnpooledChunk 调用中确认了这一点。

修复我们环境中的性能问题

我们决定实施一个快速的解决方法来测试我们的假设。除了需要调整一次 JVM 选项外,这种方法没有任何重大的缺点。

直接的解决方法包括手动将 maxOrder 设置调整回其先前的值。这可以通过在 Logstash 中的 config/jvm.options 文件中添加一个特定标志来实现

-Dio.netty.allocator.maxOrder=11

此调整会将默认块大小恢复为 16MB (chunkSize = pageSize << maxOrder,或 16MB = 8KB << 11),这与 Netty 的先前行为一致,从而减少了在 PooledAllocator 之外分配和释放较大对象相关的开销。

在将此更改部署到 QA 环境中的某些主机后,影响在分析数据中立即显现出来。

单主机

多主机

我们还可以使用差异火焰图视图来查看影响。

对于这个特定的线程,我们正在比较 1 月初的一天数据和 2 月初的一天数据,这些数据来自部分主机。整体性能的改进以及二氧化碳和成本节省都是显著的。

对于单个主机也可以进行相同的比较。在这个视图中,我们正在比较 1 月初的一台主机和 2 月初的同一台主机。该主机上的实际 CPU 使用率降低了 50%,每年每台主机为我们节省了约 900 美元。

修复 Logstash 中的问题

除了临时的解决方法外,我们还在努力在 Logstash 中发布对此行为的正确修复。您可以在此 问题 中找到更多详细信息,但可能的候选方案是

  • 全局默认调整: 一种方法是通过在 jvm.options 文件中包含此更改,将所有实例的 maxOrder 永久设置回 11。此全局更改将确保所有 Logstash 实例都使用较大的默认块大小,从而减少在池化分配器之外进行分配的需要。
  • 自定义分配器配置: 为了进行更有针对性的干预,我们可以专门在 Logstash 的 TCP、Beats 和 HTTP 输入中自定义分配器设置。这将涉及在这些输入初始化时配置 maxOrder 值,从而提供一种定制的解决方案,以解决数据摄取中最受影响区域的性能问题。
  • 优化主要分配点: 另一种解决方案侧重于改变 Logstash 中重要分配点的行为。例如,修改 Beats 输入中的帧解压缩过程以避免使用直接内存,而是默认使用堆内存可以显著降低性能影响。这种方法将规避降低的默认块大小所施加的限制,从而最大限度地减少对大型直接内存分配的依赖。

成本节省和性能提升

在 1 月 23 日为 Logstash 实例进行新的配置更改后,平台的每日功能成本从最初的 >6,000 美元大幅降至 350 美元,降幅达到惊人的 20 倍。这一变化表明了通过技术优化实现大幅成本节约的潜力。但是,需要注意的是,这些数字代表的是潜在的节省,而不是直接的成本降低。

仅仅因为主机使用的 CPU 资源较少,并不一定意味着我们也在省钱。为了真正从中受益,现在的最后一步是减少我们正在运行的虚拟机数量,或者缩减每个虚拟机的 CPU 资源以满足新的资源要求。

这次使用 Elastic Universal Profiling 的经验突显了详细的实时数据分析在识别优化领域方面的重要性,这些优化领域可以带来显著的性能提升和成本节约。通过实施基于分析见解的有针对性的更改,我们已在 QA 环境中显著降低了 CPU 使用率和运营成本,这对更广泛的生产部署具有广阔的前景。

我们的发现证明了在云环境中采用始终在线的、以分析驱动的方法的好处,为未来的优化奠定了良好的基础。随着我们扩大这些改进,进一步节省成本和提高效率的潜力将继续增长。

所有这一切在您的环境中也是可行的。 了解如何立即开始

本文中描述的任何特性或功能的发布和时间安排均由 Elastic 自行决定。任何当前不可用的特性或功能可能不会按时交付或根本不交付。

分享这篇文章