指数直方图:更好的数据,零配置

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

直方图是可观测性工具带中的强大工具。OpenTelemetry 支持直方图,因为它能够有效地捕获和传输测量值的分布,从而能够进行统计计算,如百分位数

实际上,直方图有几种不同的形式,每种都有其表示桶和桶计数的策略。OpenTelemetry 的第一个稳定指标版本包含了显式桶直方图,现在 OpenTelemetry 正在引入一种新的指数桶直方图选项。这种令人兴奋的新格式会自动调整桶以反映测量值,并且更压缩,以便通过网络发送。这篇博文将深入探讨指数直方图的细节,解释它们的工作原理、它们解决的问题以及如何立即开始使用它们。

OpenTelemetry 指标简介

在讨论指数桶直方图之前,让我们快速回顾一下一些通用的 OpenTelemetry 指标概念。如果您已经了解,请跳至 直方图的解剖

指标代表了许多测量的聚合。我们使用它们是因为单独导出和分析测量值通常成本过高。想象一下导出响应每秒一百万次请求的 HTTP 服务器的每次请求时间的成本!指标聚合测量值以减少数据量并保留有意义的信号。

与跟踪(以及不久将来的日志)一样,OpenTelemetry 指标分为 APISDK。API 用于仪器化代码。应用程序所有者可以使用 API 编写特定于其域的自定义仪器化,但更常见的是他们会安装其库或框架的预构建仪器化。SDK 用于配置 API 收集的数据的处理方式。这通常包括处理数据并将其导出到进程外进行分析,通常是导出到可观测性平台。

指标的 API 入口点是 MeterProvider。它为不同的范围提供 Meter,范围只是应用程序代码的逻辑单元。例如,HTTP 客户端库的仪器化将具有与数据库客户端库的仪器化不同的范围,因此也具有不同的 Meter。您使用 Meter 来获取 Instrument。您使用 Instrument 来报告测量值,测量值由值和一组属性组成。此 Java 代码片段演示了工作流程

OpenTelemetry openTelemetry = // declare OpenTelemetry instance
Meter meter = openTelemetry.getMeter("my-meter-scope");
DoubleHistogram histogram =
    meter
        .histogramBuilder("my-histogram")
        .setDescription("The description")
        .setUnit("ms")
        .build();
histogram.record(10.2, Attributes.builder().put("key", "value").build());

SDK 提供了 MeterProvider、Meter 和 Instrument 的实现。它聚合 Instrument 报告的测量值,并根据应用程序配置将它们导出为指标。

OpenTelemetry 指标目前有六种类型的 Instrument:Counter、UpDownCounter、Histogram、AsyncCounter、AsyncUpDownCounter 和 Gauge。请仔细考虑选择哪种 Instrument 类型,因为每种类型都隐含了有关其记录的测量值性质及其分析方式的某些信息。例如,当您想计数时,并且计数的总和比其单个值更重要时(例如跟踪网络上传输的字节数),请使用 Counter。当测量的分布与分析相关时,请使用 Histogram。例如,直方图是跟踪 HTTP 服务器响应时间的自然选择,因为分析响应时间分布以评估 SLA 和识别趋势很有用。要了解更多信息,请参阅 Instrument 选择指南。

我之前提到 SDK 聚合了 Instrument 的测量值。每种 Instrument 类型都有一个默认的聚合策略(或简称为 聚合),该策略反映了 Instrument 类型选择所隐含的测量值的预期用途。例如,Counter 和 UpDownCounter 聚合为它们的总和。Histogram 聚合为 Histogram 聚合。(请注意,Histogram 既是 Instrument 类型,也是 聚合。)

直方图的解剖

什么是直方图?暂时放下 OpenTelemetry,我们都或多或少熟悉直方图。它们由桶和桶中的出现次数组成。

例如,一个直方图可以跟踪掷出特定数字的次数(使用两个六面骰子),每个可能结果(2-12)都有一个桶。在大量掷骰子后,您期望 7 号桶的计数最高,因为您更有可能掷出总数为 7,而 2 号和 12 号桶的计数最少,因为这些是可能性最小的掷骰结果,如本示例直方图所示。

histogram outcomes 200 rolls two 6 sided dice

OpenTelemetry 有两种类型的直方图。让我们从相对简单的 显式桶直方图开始。它具有在初始化期间显式定义的桶边界。例如,如果您使用边界 [0,5,10] 配置它,则有 N+1 个桶,边界为 (-∞, 0],(0,5],(5,10], (10,+∞]。每个桶跟踪值在其边界内的出现次数。此外,直方图还跟踪所有值的总和、所有值的计数、最大值和最小值。有关完整定义,请参阅 opentelemetry-proto

在讨论第二种类型的直方图之前,请停下来思考一下当数据这样组织时,您可以回答的其中一些问题。假设您使用直方图来跟踪响应请求的毫秒数,您可以确定

  • 请求的数量。
  • 最小、最大和平均请求延迟。
  • 延迟小于特定桶边界的请求的百分比。例如,如果桶边界为 [0,5,10],您可以将 (-∞,0],(0,5],(5,10] 这几个桶的计数相加,然后除以总计数,以确定延迟小于 10 毫秒的请求的百分比。如果您有一个 SLA,要求 99% 的请求必须在 10 毫秒以上得到解决,那么您就可以确定是否满足了它。
  • 模式,通过分析分布。例如,您可能会发现大多数请求响应很快,但一小部分请求需要很长时间,从而拉低了平均值。

OpenTelemetry 直方图的第二种类型是 指数桶直方图。指数桶直方图具有桶和桶计数,但不是显式定义桶边界,而是根据指数尺度计算边界。更具体地说,每个桶由索引 i 定义,其桶边界为 (base**i, base**(i+1)],其中 base**i 表示 base 提高到 i 次幂。基数源自一个比例因子,该比例因子可调整以反映报告的测量值的范围,并且等于 2**2**-scale。桶索引必须是连续的,但可以定义正或负的非零偏移量。例如,在 scale 0 时,base = 2**2**-0 = 2,索引 [-2,2] 的桶边界定义为 (.25,.5],(.5,1],(1,2],(2,4],(4,8]。通过调整 scale,您可以表示大值和小值。与显式桶直方图一样,指数桶直方图还跟踪所有值的总和、所有值的计数、最大值和最小值。有关完整定义,请参阅 opentelemetry-proto

为何使用指数桶直方图

表面上看,指数桶直方图与显式桶直方图似乎没有太大区别。实际上,它们微妙的差异会产生截然不同的结果。

指数桶直方图是更压缩的表示。显式桶直方图使用桶计数列表和 N-1 个桶边界列表来编码数据,其中 N 是桶的数量。每个桶计数和桶边界都是一个 8 字节的值,因此一个 N 桶的显式桶直方图被编码为 2N-1 个 8 字节的值。

相比之下,指数桶直方图的桶边界是根据比例因子和定义桶起始索引的偏移量计算的。每个桶计数都是一个 8 字节的值,因此一个 N 桶的指数桶直方图被编码为 N+2 个 8 字节的值(N 个桶计数和 2 个常数)。当然,这两种表示形式在通过网络发送时通常都会进行压缩,因此进一步减小尺寸是可能的,但指数桶直方图从根本上包含的信息更少。

指数桶直方图基本上是无需配置的。显式桶直方图需要一个显式定义的桶边界集,这些边界需要配置在某个地方。提供了一组默认边界,但直方图的用例差异很大,您很可能需要调整边界以更好地反映您的数据。View API 通过机制来选择特定的 Instrument 并重新定义显式桶直方图聚合的桶边界。

相比之下,指数桶直方图唯一可配置的参数是桶的数量,默认情况下正值为 160。该实现根据记录值的范围和可用桶的数量自动选择比例因子,以最大化围绕记录值的桶密度。我怎么强调这个功能的有用性都不为过。

指数桶直方图捕获高密度值分布,自动根据测量值的比例和范围进行调整,无需配置。捕获纳秒级测量的同一个直方图同样擅长捕获秒级测量。无论比例如何,它们都能保持保真度。

考虑一下捕获 HTTP 请求响应时间的毫秒数。使用显式桶直方图,您需要猜测桶边界,并希望它们能准确地捕获值的分布。但如果情况发生变化,延迟飙升,您的假设可能不再成立,所有值都可能被归入同一类。突然之间,您就失去了对数据分布的可见性。您知道总体延迟很高。但您无法知道有多少请求高但可容忍,有多少请求非常慢。相比之下,使用指数桶直方图,比例会自动调整以适应延迟峰值,从而选择最佳的桶范围。即使测量值范围很大,您仍然可以洞察分布情况。

示例场景:显式桶直方图 vs. 指数桶直方图

让我们通过一个适当的演示来总结所有内容,比较显式桶直方图和指数桶直方图。我准备了一些示例代码,模拟以毫秒为单位跟踪 HTTP 服务器的响应时间。它向具有默认桶的显式桶直方图记录了一百万个样本,并向具有大约相同大小的OTLP 编码的 Gzip 压缩有效载荷的指数桶直方图记录了样本(该数量与显式桶默认值大致相当)。通过反复试验,我确定大约 40 个指数桶产生的有效载荷大小与具有 11 个桶的默认显式桶直方图相当。(您的结果可能会有所不同。)

我希望样本的分布能够反映实际 HTTP 服务器的情况,响应时间范围对应不同的操作。它看起来会像这个例子一样

target probability distribution response time

为了实现这一点,我使用了各种不同的概率分布,每个分布对应曲线上的不同范围,并且每个范围占样本的百分比。

我运行了模拟,并通过导出直方图来比较显式桶直方图和指数桶直方图。接下来的两个图表显示了结果。指数桶直方图具有更多的细节,而显式桶直方图的桶数量有限,因此无法获得这些细节。

注意:这些可视化来自 New Relic 平台,我使用该平台是因为我在那里工作,并且这是我可视化直方图的最简单方式。每个平台都有其存储和检索直方图的机制,这些机制通常会进行一些有损的桶转换到一个标准化的存储格式,New Relic 也不例外。此外,可视化没有清楚地划分桶,这会导致具有相同计数的相邻桶看起来像一个桶。

这是毫秒级的指数桶直方图

millisecond scale exponential bucket histogram

这是毫秒级的显式桶直方图

millisecond scale explicit bucket histogram

这个演示对显式桶直方图相当有利,因为我选择在默认桶的最佳范围内报告值(例如,0 到 1000)。接下来的两个示例显示了当以纳秒精度而不是毫秒精度记录相同值时会发生什么(所有值乘以 106)。这就是指数桶直方图的无配置自动缩放特性真正发挥作用的地方。在默认的显式桶边界下,所有样本都落入显式桶直方图的一个桶中。与前一个示例中的毫秒版本相比,指数桶类型失去了一些定义,但您仍然可以看到响应时间范围。

这是纳秒级的指数桶直方图

nanosecond scale exponential bucket histogram

这是纳秒级的显式桶直方图

nanosecond scale explicit bucket histogram

下一步

指数桶直方图是指标领域中一个强大的新工具。尽管在发布本文时实现仍在进行中,但在使用 OpenTelemetry 指标时,您肯定会想启用它们。

如果您正在使用 opentelemetry-java(以及最终其他语言),启用指数桶直方图的最简单方法是通过以下命令设置 环境变量

export OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION=exponential_bucket_histogram

有关在其他语言中启用的说明,请查看有关仪器化github.com/open-telemetry 上的相关文档。

本文的一个版本最初发布在 New Relic 博客上。