指数直方图
博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。
此前,在《为什么需要直方图?》和《直方图与摘要的对比》中,我介绍了直方图和摘要的基础知识,解释了它们各自的权衡、优点和局限性。由于它们易于理解和演示,因此那些文章主要关注所谓的显式存储桶直方图。指数存储桶直方图,在 Prometheus 中也称为原生直方图,是显式存储桶直方图的一种低成本、高效的替代方案。在这篇文章中,我将介绍它们是什么、它们如何工作以及它们解决了显式存储桶直方图难以解决的问题。
直方图类型
就本博客文章而言,直方图主要有两种类型:显式存储桶直方图和指数存储桶直方图。在之前的文章中,我主要介绍了 OpenTelemetry 称为显式存储桶直方图,而 Prometheus 仅称为直方图的类型。顾名思义,显式存储桶直方图的每个存储桶都由用户或一组默认存储桶显式配置。指数直方图通过指数增长函数计算存储桶边界来工作。这意味着每个连续的存储桶都比前一个存储桶大,并确保每个存储桶具有恒定的相对误差。
指数直方图
在 OpenTelemetry 指数直方图中,存储桶根据一个整数*比例因子*自动计算,比例因子越大,存储桶越小,精度越高。选择一个适合您正在收集的值的分布的比例因子非常重要,以便最大限度地减少误差、提高效率并确保收集到的值适合合理的存储桶数量。在接下来的几个部分中,我将详细介绍比例和误差的计算。
比例因子
指数直方图中最重要的、最基础的部分也是最难理解的部分之一:比例因子。从比例因子开始,可以推导出存储桶边界,进而推导出分辨率、范围和误差率。第一步是计算直方图基数。
基数是根据比例因子直接导出的一个常数,使用公式 `2 ^ (2 ^ -scale)` 进行计算。例如,给定比例因子 3,基数可以计算为 `2^(2^-3) ~= 1.090508`。由于计算取决于负比例因子的幂,因此随着比例因子的增长,基数会缩小,反之亦然。正如稍后将展示的那样,这是比例因子越大导致存储桶越小、直方图分辨率越高的根本原因。
存储桶计算
给定比例因子及其产生的基数,我们可以计算出直方图中所有可能的存储桶。从基数开始,索引为 `i` 的每个存储桶的上限定义为 `base ^ (i + 1)`,第一个存储桶的下限为 1。因此,索引为 0 的第一个存储桶的上限也恰好是基数。目前,我们只考虑非负索引,但负索引的存储桶也是可能的,它们定义了 0 到 1 之间的所有存储桶。以比例因子 3 和产生的基数 1.090508 为例,索引为 2 的第三个存储桶的上限为 `1.090508^(2+1) = 1.29684`。下表显示了不同比例因子的前 10 个存储桶的上限。
| 索引 | 比例 -1 | 比例 0 | 比例 1 | 比例 3 |
|---|---|---|---|---|
| -1 | 1 | 1 | 1 | 1 |
| 0 | 4 | 2 | 1.4142 | 1.0905 |
| 1 | 16 | 4 | 2 | 1.1892 |
| 2 | 64 | 8 | 2.8284 | 1.2968 |
| 3 | 256 | 16 | 4 | 1.4142 |
| 4 | 1024 | 32 | 5.6569 | 1.5422 |
| 5 | 4096 | 64 | 8 | 1.6818 |
| 6 | 16384 | 128 | 11.3137 | 1.8340 |
| 7 | 65536 | 256 | 16 | 2 |
| 8 | 262144 | 512 | 22.6274 | 2.1810 |
| 9 | 1048576 | 1024 | 32 | 2.3784 |
我在这里加粗了一些值,以展示指数直方图一个重要的特性,称为*完美子集*。
完美子集
在上表所示的图表中,一些存储桶边界在具有不同比例因子的直方图之间共享。事实上,每次比例因子增加 1,就在每个现有边界之间插入 1 个边界。这个特性被称为完美子集,因为给定比例因子的每个边界集都是任何具有更大比例因子的直方图边界的完美子集。
因此,具有不同比例因子的直方图可以通过组合相邻的存储桶来归一化为具有较小比例因子的直方图。这意味着不同比例因子的直方图仍然可以合并为单个直方图,其精度与合并的最低精度直方图完全相同。例如,比例因子为 3 的直方图 A 和比例因子为 2 的直方图 B 可以合并为一个比例因子为 2 的直方图 C,方法是首先将 A 中每对相邻的存储桶相加,形成比例因子为 2 的直方图 A'。然后,将 A' 中的每个存储桶与 B 中具有相同索引的相应存储桶相加,得到 C。
相对误差
直方图不存储每个点的精确值,而是将每个点表示为一个存储桶,该存储桶包含一系列可能的点。这可以被视为类似于有损压缩。就像无法从压缩的 JPEG 中恢复原始图像一样,也无法从直方图中恢复精确的输入数据集。输入数据与估计重建数据之间的差异是直方图的误差。理解直方图误差很重要,因为它会影响 φ-分位数估计,并可能影响您如何定义 SLO。
直方图的相对误差定义为存储桶宽度的一半除以存储桶中点。由于相对误差在所有存储桶中都相同,我们可以使用上限为基数的第一个存储桶来简化数学计算。下面是一个使用比例因子 3 的示例。
scale = 3
# For base calculation, see above
base = 1.090508
relative error = (bucketWidth / 2) / bucketMidpoint
= ((upper - lower) / 2) / ((upper + lower) / 2)
= ((base - 1) / 2) / ((base + 1) / 2)
= (base - 1) / (base + 1)
= (1.090508 - 1) / (1.090508 + 1)
= 0.04329
= 4.329%
有关直方图误差的更多信息,请参阅OTEP 149以及指数直方图聚合规范。
选择比例
由于增加比例因子会增加分辨率并减小相对误差,因此选择较大的比例因子可能很有诱惑力。毕竟,为什么您要引入误差呢?答案是,比例因子与表示特定范围内的值所需的存储桶数量之间存在正相关关系。例如,使用 160 个存储桶(OpenTelemetry 默认设置),比例因子为 3 的直方图 A 可以表示 1 到约一百万之间的值;使用相同数量存储桶的比例因子为 4 的直方图 B 只能表示 1 到约 1000 之间的值,尽管相对误差减半。要用 B 表示与 A 相同范围的值,则需要两倍的存储桶;在这种情况下是 320。
这引出了我关于选择比例的第一个最重要的观点——*数据对比度*。数据对比度是您描述数据集中最小可能值 x 和最大可能值 y 之间的比例差异的方式,计算方法是找到一个常数 c,使得 `y = c * x`。例如,如果您的数据在 1 到 1000 毫秒之间,则数据对比度为 1000。如果您的数据在 1 千字节到 1 太字节之间,则数据对比度为 1,000,000,000。数据对比度、比例和存储桶数量相互关联,如果您知道其中两个,就可以计算出第三个。
幸运的是,如果您使用 OpenTelemetry,比例的选择在很大程度上已经为您完成了。在 OpenTelemetry 中,您配置最大比例(默认为 20)和最大大小(默认为 160),即存储桶数量。最初假定直方图具有最大比例。随着附加数据点的添加,直方图将向下重新缩放,使数据点始终包含在您的最大存储桶数量内。OpenTelemetry 作者选择默认的 160 个存储桶是为了能够在小于 5% 的相对误差下覆盖典型 Web 请求(在 1 毫秒到 10 秒之间)。如果您的数据对比度较低,误差将更小。
负值或零值
在本文的大部分内容中,我们忽略了零值和负值,但负值存储桶的工作方式与此类似,随着存储桶远离零而增大。以上所有的数学公式和解释都适用于负值,但应该用它们的绝对值代替,并且存储桶的上限是下限(或绝对值上限)。零值,或者绝对值小于可配置阈值的值,会进入一个特殊的零存储桶。合并具有不同零阈值的直方图时,将采用较大的阈值,并将绝对值上限在零阈值内的所有存储桶添加到零存储桶中并丢弃。
OpenTelemetry 与 Prometheus
OpenTelemetry 与 Prometheus 之间的兼容性可能是一个足够大的话题,可以单独发一篇文章。目前我只想说,对于所有实际目的,OpenTelemetry 指数直方图与 Prometheus 原生直方图是 1:1 兼容的。比例计算、存储桶边界、误差率、零存储桶等都相同。如需了解更多信息,我建议您观看 Ruslan Vovalov 和 Ganesh Vernekar 的讲座:在 Prometheus 中使用 OpenTelemetry 的指数直方图。
本文的一个版本最初发布在作者的博客上。