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

直方图是一种在可观测性工具中非常有用的工具。OpenTelemetry 支持直方图,因为它们能够高效地捕获和传输测量数据的分布情况,从而进行统计计算,如百分位数

实际上,直方图有多种类型,每种类型都有自己的策略来表示桶和桶计数。OpenTelemetry 首个稳定的指标发布包括显示桶直方图,并且现在 OpenTelemetry 正在引入一种新的指数桶直方图选项。这种令人兴奋的新格式会自动调整桶以反映测量数据,并且在传输过程中更加压缩。本文深入介绍了指数直方图的细节,讲解它们的工作原理,解决的问题以及如何立即开始使用它们。

OpenTelemetry 中的指标简介

在谈论指数桶直方图之前,让我们快速回顾一下一些通用的 OpenTelemetry 指标概念。如果您已经了解这些内容,可以跳到直方图的解剖学部分。

指标是许多测量数据的汇总。我们使用指标是因为单独导出和分析每个测量数据往往代价过高。想象一下,对于每秒处理一百万个请求的 HTTP 服务器来说,导出每个请求的时间的成本是多么高昂!指标汇总测量数据可以减少数据量并保留有意义的信号。

与跟踪(以及不久之后的日志)类似,OpenTelemetry 指标分为APISDK。API 用于对代码进行仪表化。应用程序所有者可以使用 API 编写特定于其领域的自定义仪表化代码,但更常见的是为他们的库或框架安装预构建仪表化工具。SDK 用于配置 API 收集的数据的后续处理和导出,通常是导出到可观测性平台进行分析。

指标的 API 入口点是meter provider。它为不同作用域提供了计量器,其中作用域只是应用程序代码的一个逻辑单元。例如,HTTP 客户端库的仪表化将具有不同的作用域和不同的计量器,而数据库客户端库的仪表化将具有不同的作用域和不同的计量器。您可以使用计量器获取仪表。您可以使用仪表报告测量数据,其中包括一个值和一组属性。以下 Java 代码片段演示了工作流程:

OpenTelemetry openTelemetry = // 声明 OpenTelemetry 实例
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 提供了计量器提供程序、计量器和仪表的实现。它聚合由仪表报告的测量数据,并根据应用程序配置将其作为指标导出。

OpenTelemetry 指标当前有六种类型的instruments:计数器、上下计数器、直方图、异步计数器、异步上下计数器和异步仪表。在选择仪表类型时请仔细考虑,因为每种类型都对记录的测量数据的性质和分析方式有着特定的要求。例如,当您想要计数某些事物时,且这些事物的总和比它们的个别值更重要时(例如,跟踪通过网络发送的字节数),请使用计数器。当测量数据的分布对分析有意义时,请使用直方图。例如,对于跟踪 HTTP 服务器的响应时间,直方图是一个自然的选择,因为对响应时间的分布进行分析对于评估服务级别协议(SLA)和识别趋势非常有用。要了解更多信息,请参阅[仪表选择][]的准则。

我之前提到过,SDK 会聚合由仪表报告的测量数据。每种仪表类型都有一个默认的聚合策略(或简单地称为[聚合方式][]),这反映了通过仪表类型选择隐含的测量数据的使用意图。例如,计数器和上下计数器会将值聚合为其总和。直方图会聚合为直方图聚合(请注意,直方图既是[仪表类型][]又是[聚合方式][])。

直方图的解剖学

什么是直方图?在暂时将 OpenTelemetry 放在一边之前,我们对直方图都有一定的了解。直方图由桶和在这些桶内发生的次数组成。

例如,直方图可以跟踪用两个六面骰子投掷的结果之和为特定数字的次数,每个可能的结果对应一个桶,范围从 2 到 12。通过大量的投掷,您可以预期 7 桶的次数最高,因为投掷出的数字总和为 7 的概率更高,而 2 和 12 桶的次数最低,因为这些是最不可能的投掷结果。下面是一个示例直方图。

直方图结果:200 次掷两个六面骰子

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]。通过调整比例,您可以表示大值和小值。与显式桶直方图一样,指数桶直方图也跟踪所有值的总和、次数、最大值和最小值。有关完整定义,请参阅[opentelemetry-proto][]。

为什么要使用指数桶直方图

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

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

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

指数桶直方图基本上无需配置。显式桶直方图需要显式定义一组桶边界,并且需要在某个位置进行配置。提供了一个默认设置的桶边界,但是直方图的用途因应用而异,这可能意味着您需要调整边界以更好地反映数据。视图 API 可以帮助您,通过机制选择特定的仪表并重新定义显式桶直方图的聚合桶边界。

相比之下,指数桶直方图的唯一可配置参数是桶的数量,默认为正值时为 160。实现会根据记录的值的范围和可用的桶数量自动选择比例因子,以最大限度地接近记录值的桶密度。我无法过分强调这一点的实用性。

指数桶直方图会自动调整根据测量数据的规模和范围而自动调整的高密度值分布,无需任何配置。捕获纳秒级测量数据的相同直方图同样适用于捕获秒级测量数据。无论规模如何,它们都可以保持精确度。

考虑捕获 HTTP 请求时间的场景。使用显式桶直方图时,您会对希望准确捕获值分布的桶边界进行猜测。但是,如果条件发生变化并且延迟激增,您的假设可能不成立,所有值可能会被合并在一起。突然之间,您将丧失对数据分布的可见性。您知道整体上延迟很高。但是您无法得知延迟很高但可忍受的请求数量与极慢的请求数量之间的区别。相比之下,使用指数桶直方图时,比例会自动根据延迟激增而调整,选择最佳的桶范围。即使在测量值范围很大的情况下,您仍然可以了解数据分布。

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

让我们通过一个适当的实例演示显式桶直方图和指数桶直方图的区别。我编写了一些[示例代码][],模拟了以毫秒为单位跟踪对 HTTP 服务器的响应时间。该代码会将一百万个样本记录到一个具有默认桶的显式桶直方图和一个产生与默认显式桶直方图的 OTLP 编码、Gzip 压缩数据大小大致相同的指数桶直方图。通过反复试验,我确定 ~40 个指数桶和显式桶的默认值(例如 11 个桶)产生了相同的负载大小(结果可能会有所不同)。

我希望样本的分布能够反映在实际的 HTTP 服务器中可能遇到的情况,不同操作的响应时间带对应于曲线中的不同带,每个操作带占比不同。

我运行了模拟,并导出了直方图以比较显式桶直方图和指数桶直方图。下面的两个图表显示了结果。指数桶直方图有明显更多的细节,而显式桶直方图的较少的桶无法提供这些细节。

注意: 这些可视化效果来自我使用的 New Relic 平台,因为我在那里工作,并且这是我可视化直方图最简单的方式。每个平台都有自己的存储和检索直方图的机制,通常会将桶转换为规范化的存储格式- New Relic 平台也不例外。此外,可视化效果不清晰地显示桶,使具有相同计数的相邻桶呈现为单个桶。

这是以毫秒为单位的指数桶直方图:

毫秒级别的指数桶直方图

这是以毫秒为单位的显式桶直方图:

毫秒级别的显式桶直方图

此示例对显式桶直方图非常慷慨,因为我选择以默认桶的最佳范围报告值(例如 0 到 1000)。下面的两个示例显示了当相同值以纳秒精度而不是毫秒精度记录时会发生什么情况(所有值乘以 10 的 6 次方)。这是指数桶直方图的非配置自动缩放性真正发挥作用的地方。由于默认的显式桶边界,所有样本都会落入单个桶中,而指数桶直方图在前面的毫秒级别版本中失去了一些定义。但是,仍然可以看到响应时间带。

这是以纳秒为单位的指数桶直方图:

纳秒级别的指数桶直方图

这是以纳秒为单位的显式桶直方图:

纳秒级别的显式桶直方图

下一步

指数桶直方图是一种强大的新型指标工具。虽然在发布本文时,实现仍在进行中,但在使用 OpenTelemetry 指标时,您一定要启用它们。

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

export OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION=exponential_bucket_histogram

有关在其他语言中启用的说明,请查阅有关[仪表化][]或github.com/open-telemetry的相关文档。

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