指数直方图
之前,在 为什么使用直方图? 和 直方图 vs 摘要 中,我详细介绍了直方图和摘要的基础知识,解释了它们的权衡、优点和限制。由于易于理解和演示,这些文章侧重于所谓的显式分桶直方图。指数分桶直方图,也被称为 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 可以通过首先将 A 中的每对相邻桶求和形成缩放因子为2的直方图 A’。然后,将 A’ 中的每个桶与 B 中相同索引的桶求和以形成 C。
相对误差
直方图不会为每个点存储精确值,而是将每个点表示为由一系列可能点组成的桶。这可以类比为有损压缩。就像无法从压缩的 JPEG 中恢复出原始图像一样,无法从直方图中恢复出精确的输入数据集。输入数据与数据的估计重建之间的差异就是直方图的误差。理解直方图的误差非常重要,因为它会影响 φ 分位数估计,并可能影响你如何定义SLO(服务级别目标)。
直方图的相对误差被定义为半个桶宽度除以桶中点的中点。由于相对误差在所有桶中都相同,我们可以使用具有基数上界的第一个桶来简化计算。下面是一个使用缩放因子为3的示例:
缩放因子 = 3
# 基数计算,请参见上文
基数 = 1.090508
相对误差 = (桶宽度 / 2) / 桶中点
= ((上界 - 下界) / 2) / ((上界 + 下界) / 2)
= ((基数 - 1) / 2) / ((基数 + 1) / 2)
= (基数 - 1) / (基数 + 1)
= (1.090508 - 1) / (1.090508 + 1)
= 0.04329
= 4.329%
有关直方图误差的更多信息,请参见 OTEP 149 和 指数直方图聚合的规范。
选择缩放因子
由于增加缩放因子会增加分辨率并减小相对误差,可能会倾向于选择一个较大的缩放因子。毕竟,为什么要引入误差呢?答案是,缩放因子和在指定范围内表示值所需的桶数量之间存在正相关关系。例如,使用160个桶(OpenTelemetry 默认值),缩放因子为3的直方图可以表示值介于1和约100万之间;而缩放因子为4的直方图相同数量的桶只能表示值介于1和约1000之间,虽然相对误差减半。要想用缩放因子为4的直方图表示与缩放因子为3的直方图相同范围的值,就需要两倍数量的桶,即320个。
这把我带到了选择缩放因子的第一个重要点,数据对比度。数据对比度描述了数据集中最小可能值 x 与最大可能值 y 之间的缩放差异,并被计算为常数倍数 c,即 y = c * x
。例如,如果数据介于1毫秒和1000毫秒之间,数据对比度为1000。如果数据介于1千字节和1太字节之间,数据对比度为10亿。数据对比度、缩放因子和桶数量之间的关系密切,如果其中两个已知,可以计算第三个。
幸运的是,如果你使用 OpenTelemetry,选择缩放因子的工作基本上都是由它来完成的。在 OpenTelemetry 中,你可以配置一个最大缩放(默认为20)和一个最大尺寸(默认为160),或者直方图的桶数量。直方图最初被假设具有最大缩放。在添加额外数据点后,直方图将自动调整缩放,使数据点始终适合最大桶数量之内。OpenTelemetry 作者选择了160个桶作为默认值,以便在1毫秒到10秒的典型Web请求范围内,相对误差不超过5%。如果你的数据对比度较小,则误差会更小。
负值或零值
在本文的大部分内容中,我们忽略了零值和负值,但负数的桶工作方式与此类似,随着桶远离零点而变大。上面的所有数学和解释都以同样的方式适用于负值,但需要用它们的绝对值代替,并将桶的上界替换为下界(或上绝对值界限)。零值,或者绝对值小于可配置阈值的值,放入一个特殊的零桶中。在合并具有不同零阈值的直方图时,取较大阈值,并将绝对值上界在零阈值范围内的任何桶添加到零桶中并丢弃。
OpenTelemetry 和 Prometheus
OpenTelemetry 和 Prometheus 之间的兼容性可能是一个足够大的主题,单独一篇文章可能不够。现在我只想说,在所有实际目的上,OpenTelemetry 的指数直方图与 Prometheus 的原生直方图是1:1兼容的。缩放计算、桶边界、误差率、零桶等等都是相同的。如果需要更多信息,我建议你观看 Ruslan Vovalov 和 Ganesh Vernekar 所做的这个演讲:在 Prometheus 中使用 OpenTelemetry 的指数直方图。
本文的一个版本最初发布在作者的博客上。