作者:来自 Elastic Luca Wintergerst•Tim Rühsen
在这篇博客中,我们将介绍我们的一位工程师的一项发现如何帮助我们在 QA 环境中节省数千美元的成本,并且一旦我们将这一变化部署到生产中,还可以节省更多的成本。
在当今的云服务和 SaaS 平台时代,持续改进不仅仅是一个目标,而是一种必需品。在 Elastic,我们一直在寻找方法来微调我们的系统,无论是我们的内部工具还是 Elastic Cloud 服务。我们最近在 Elastic Cloud QA 环境中进行的性能优化调查,由 Elastic Universal Profiling 指导,这是我们如何将数据转化为可操作见解的一个很好的例子。
在本博客中,我们将介绍我们的一位工程师的一项发现如何在我们的 QA 环境中节省数千美元,并且在我们将这一变化部署到生产中后,节省了更多。
Elastic Universal Profiling:我们用于优化的首选工具
在我们用于解决性能挑战的解决方案套件中,Elastic Universal Profiling 是一个关键组件。作为使用 eBPF 的 “always-on - 始终在线” 分析器,它无缝集成到我们的基础设施中,并系统地收集整个系统的综合分析数据。由于存在零代码检测或重新配置,因此很容易在我们的云中的任何主机(包括 Kubernetes 主机)上部署它 —— 我们已经将它部署到我们的 Elastic Cloud 环境中。
我们所有的主机都运行分析代理来收集这些数据,这让我们可以详细了解我们正在运行的任何服务的性能。
发现机会
这一切都始于对我们的 QA 环境的例行检查。我们的一名工程师正在查看分析数据。在 Universal Profiling 的帮助下,这一初步发现相对较快。我们发现了一个未优化且计算成本很高的函数。
让我们一步一步地进行。
为了发现耗费资源的函数,我们可以简单地查看 TopN 函数列表。TopN 函数列表向我们展示了我们运行的所有服务中使用最多 CPU 的所有函数。
为了按其影响对它们进行排序,我们按 “total CPU” 降序排序:
- Self CPU 测量函数直接使用的 CPU 时间,不包括其调用的函数所花费的时间。此指标有助于识别自身使用大量 CPU 能力的函数。通过改进这些函数,我们可以使它们运行得更快,使用更少的 CPU。
- Total CPU 将函数及其调用的任何函数使用的 CPU 时间相加。这可以完整地展示函数及其相关操作使用了多少 CPU。如果某个函数的 “total 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% 是针对整个基础设施的。有些主机甚至可能没有运行此服务,因此拉低了平均值。
将内容过滤到正在运行服务的单个主机,我们可以看出,该主机实际上将其 CPU 周期的近 70% 花费在运行此功能上。
仅此一台主机的美元成本就将使该功能每年花费约 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。默认的 maxOrder 值为 9 或 11 时,默认内存块大小分别为 4MB 或 16MB(假设页面大小为 8KB)。
对内存分配的影响
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 自行决定。任何当前不可用的特性或功能可能无法按时交付或根本无法交付。
原文:Elastic Universal Profiling: Delivering performance improvements and reduced costs — Elastic Observability Labs