原生直方图于 2022 年 11 月作为实验性特性引入。它们是一个几乎触及 Prometheus 堆栈每个部分的概念。第一个支持原生直方图的 Prometheus 服务器版本是 v2.40.0。必须通过特性标志 --enable-feature=native-histograms
启用支持。(待办:目前发布版本 v2.55 和 v3.00 仍然如此。一旦稳定版本发布,请更新此部分。)
由于与原生直方图相关的更改具有普遍性,这些更改的文档和底层概念的解释广泛分布在各种渠道(例如受影响的 Prometheus 组件文档、源代码中的文档注释、有时甚至是源代码本身、设计文档、会议演讲等)。本文旨在收集所有这些信息,并以简洁的方式在统一的上下文中呈现。本文倾向于链接现有的详细文档,而不是重复其内容,但它包含足够的信息,以便在不参考其他来源的情况下也能理解。尽管如此,需要注意的是,本文既不适合作为初学者的入门指南,也不侧重于开发者的需求。对于前者,计划提供更新版本的关于直方图和 Summary 的最佳实践文章。(TODO: 以及一篇或一系列博客文章。) 对于后者,有 Carrie Edward 的Prometheus 原生直方图开发者指南。
虽然正式规范应在各自的上下文中制定(例如 OpenMetrics 的变更将在通用的 OpenMetrics 规范中指定),但本文的某些部分采取了规范的形式。在这些部分中,关键词 “MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY” 和 “OPTIONAL” 的用法如RFC 2119中所述。
本文仍包含许多 TODO。在大多数情况下,它们不仅指向本文档的不完整之处,更重要的是指向不完整的实现或悬而未决的问题。目前,这本质上是一个持续更新的文档,随着实现和规范的完善而进行更新。
原生直方图的核心思想是将直方图视为 Prometheus 数据模型中的一等公民。将直方图提升为“原生”样本类型是实现以下关键属性的根本前提,这也解释了选择“原生直方图”这个名称的原因。
在引入原生直方图之前,所有 Prometheus 样本值都是 64 位浮点值(简称 float64 或 float)。这些浮点值可以直接表示 gauge 或 counter。Prometheus 指标类型 summary 和(经典版本的)histogram,以暴露格式存在,在摄取时会被分解为浮点组件:对于这两种类型都有 sum 和 count 组件,对于 summary 有若干 quantile 样本,对于(经典)histogram 有若干 bucket 样本。
通过原生直方图,引入了一种新的结构化样本类型。一个样本表示先前已知的 sum 和 count,外加一组动态的桶。这不仅限于摄取,PromQL 表达式也可以返回新的样本类型,而之前只能返回浮点样本。
原生直方图具有以下关键属性:
这些关键属性通过标准分桶模式完全实现。还有其他模式具有不同的权衡,可能只具备这些属性的一部分。详情请参阅下面的Schema 部分。
与先前存在的“经典”直方图相比,原生直方图(使用标准分桶模式)允许在较低的存储和查询成本下,以任意范围的观测值实现更高的桶分辨率,且几乎无需或极少配置。即使按标签对直方图进行分区现在也更加经济实惠。
由于稀疏表示(上述列表中的属性 1)对于原生直方图的许多其他优势至关重要,在设计过程早期,“稀疏直方图”是“原生直方图”的常用名称。然而,其他关键属性,如指数分桶模式或桶的动态特性,也非常重要,但“稀疏直方图”这个术语根本没有涵盖它们。
这些是指导原生直方图开发的设计文档。一些细节现在已经过时,但它们很好地描述了底层概念以及它们是如何演变的。
了解原生直方图的一种更易于接受的方式是观看会议演讲,以下是精选的演讲。作为入门,可以先观看这些演讲,然后回到本文档了解所有细节和技术问题。
本节总体描述了原生直方图的数据模型。它尽可能避免实现细节。这包括术语。例如,本节描述的列表在 protobuf 实现中将成为重复消息,在 Go 实现中(最有可能)成为切片。
与经典直方图类似,原生直方图具有用于记录观测次数的 count 字段和用于记录观测值总和的 sum 字段。此外,它还包含以下组件,这些组件将在下面的专有章节中详细介绍:
任何原生直方图在两个独立的维度上都有特定的风格:
浮点直方图偶尔用于直接的代码埋点,用于“加权”观测值,例如统计观测值落入直方图不同桶中的秒数。然而,浮点直方图更常见的用例是在 PromQL 中。PromQL 通常只对浮点值进行操作,因此 PromQL 引擎将从 TSDB 中检索到的每个直方图首先转换为浮点直方图,并通过记录规则存回 TSDB 的任何直方图都是浮点直方图。如果这样的直方图实际上是一个整数直方图(因为所有非 sum 字段的值都可以精确地表示为 uint64),TSDB 实现 MAY 将它们转换回整数直方图以提高存储效率。(截至 Prometheus v3.00,Prometheus 内的 TSDB 实现并未利用此选项。)然而,请注意,应用于计数器直方图的最常用 PromQL 函数是 rate
,它通常会生成非整数,因此记录规则的结果通常会是包含非整数值的浮点直方图。
将原生直方图明确地视为整数直方图与浮点直方图,与对传统简单数字样本的处理有所不同,后者为了简化起见始终在整个堆栈中被视为浮点数。
更复杂的直方图处理的主要原因是 protobuf 暴露格式中易于实现的效率提升。Protobuf 对整数使用 varint 编码,这在不需要额外的压缩层的情况下减小了小整数值的数据大小。通过整数桶的 delta 编码放大了这种好处,这通常会产生更小的整数值。相比之下,Protobuf 中的浮点数总是需要 8 个字节。实际上,整数直方图中的许多整数将 fits in 1 byte,并且大多数将 fits in 2 bytes,因此 protobuf 暴露格式中整数直方图的显式存在直接导致具有许多桶的直方图的数据大小接近 8 倍的减少。这尤其相关,因为由代码埋点目标暴露的绝大多数直方图都是整数直方图。
出于类似的原因,内存和磁盘中整数直方图的表示通常比浮点直方图更高效。不过,这不如暴露格式中的好处重要。首先,Prometheus 使用 Gorilla 风格的 XOR 编码来减小浮点数的大小,尽管不如用于整数的双 delta 编码。更重要的是,实现可以始终决定内部使用整数表示实际上是整数值的直方图字段(参见上文)。(历史注:Prometheus v1 正是使用了这种方法来改进浮点样本的压缩,Prometheus v3 未来很可能会再次采用这种方法。)
在计数器直方图中,观测的总数 count 和各个桶中的计数都像 Prometheus 计数器一样行为,即它们只在计数器重置时才会下降。然而,由于观测到负值,观测的 sum 可能会下降。PromQL 实现 MUST 根据整个直方图检测计数器重置(详情请参见下面的计数器重置注意事项章节)。 (请注意,这对于经典直方图和摘要的 sum 组件也一直是一个问题。到目前为止的方法是接受在这种情况下计数器重置检测会默默地失效。幸运的是,负观测值是 Prometheus 直方图和摘要非常罕见的用例。)
schema 是一个大小为 8 位(简称 int8)的带符号整数值。它定义了计算任何给定桶边界的方法,桶的索引为 i。目前有效的取值是 -53 以及 -4 到 +8(包括边界)的范围。将来可能会添加更多 schema。-53 是用于所谓的自定义桶边界或简称自定义桶的 schema,而其他 schema 数字代表不同的标准指数 schema(简称标准 schema)。
标准 schema 之间可以合并,并且 RECOMMENDED 用于通用用例。较大的 schema 数字对应更高的分辨率。schema n 的分辨率是 schema n+1 的一半,这意味着 schema n+1 的直方图可以通过合并相邻桶转换为 schema n 的直方图。
对于任何标准 schema n,索引为 i 的桶的边界计算如下(使用 Python 语法):
(2**2**-n)**i
(2**2**-n)**(i-1)
-((2**2**-n)**i)
-((2**2**-n)**(i-1))
i 是一个整数,可能为负数。
对于可表示为 float64 的最大和最小有限值(下文中称为 MaxFloat64
和 MinFloat64
)以及正负无穷大值(+Inf
和 -Inf
),上述规则存在例外情况:
MaxFloat64
的正数桶的上限(包含)是 MaxFloat64
(而不是根据上述公式计算出的会溢出 float64 的上限)。MaxFloat64
,上限(包含)为 +Inf
。(这可以称为正溢出桶。)MinFloat64
的负数桶的下限(包含)是 MinFloat64
(而不是根据上述公式计算出的会下溢 float64 的下限)。MinFloat64
,下限(包含)为 -Inf
。(这可以称为负溢出桶。)+Inf
和 -Inf
桶的桶。对于接近零的值还有更多例外情况,请参阅下面的零桶章节。
当前最低分辨率限制为 -4,最高分辨率限制为 8,是根据实际使用价值选择的。如果将来出现对更高或更低分辨率的实际需求,将考虑扩展范围。然而,schema 大于 52 没有意义,因为从一个桶到下一个桶的增长因子将小于可表示 float64 数字之间的差异。同理,schema 小于 -9 也没有意义,因为增长因子将超过可表示为 float64 的最大浮点数。因此,介于(包括)-9 和 +52 之间的 schema 数字保留用于未来的标准 schema(遵循上述桶边界公式),并且 MUST NOT 用于任何其他 schema。
对于 schema -53,桶边界通过自定义值显式设置,详情请参阅下面的自定义值章节。这导致具有自定义桶边界的原生直方图(或简称自定义桶,通常进一步缩写为 NHCB)。这种直方图可用于将经典直方图表示为原生直方图。如果标准 schema 特有的指数分桶与直方图要表示的分布不匹配,也可以使用它。具有不同自定义桶边界的直方图通常无法相互合并。因此,schema -53 SHOULD 仅在特定用例中,作为有意识的决策使用。
对于标准 schema,桶表示为两个列表,一个用于正数桶,一个用于负数桶。对于自定义桶(schema -53),只使用正数桶列表,但重新用于所有桶。
任何未填充的桶都**可以**从列表中排除。(这就是这些桶通常被称为稀疏桶的原因。)
对于浮点直方图,列表元素为 float64 类型,直接表示桶的填充数。
对于整数直方图,列表元素为有符号 64 位整数(简称:int64),每个元素表示相对于列表中前一个桶的增量填充数。每个列表中的第一个桶包含绝对填充数(也可以视为相对于零的增量)。
为了将列表中的桶映射到上一节定义的索引,有两个所谓的 span 列表,一个用于正数桶,一个用于负数桶。
每个 span 由一对数字组成:一个带符号 32 位整数(简称 int32),称为 offset;以及一个无符号 32 位整数(简称 uint32),称为 length。每个列表中只有第一个 span 可以有负的 offset。它定义了其对应桶列表中的第一个桶的索引。(请注意,对于 NHCB,索引总是正数,详情请参阅下面的自定义值章节。)长度定义了桶列表开始的连续桶的数量。后续 span 的 offset 定义了排除(因此未填充)桶的数量。长度定义了排除桶之后列表中连续桶的数量。
每个 span 列表中所有 length 值的总和 MUST 等于相应桶列表的长度。
空 span(长度为零)是有效的,并且 MAY 使用,尽管它们通常没有用处,并且 SHOULD 通过将其 offset 加到后续 span 的 offset 来消除。类似地,不是列表中第一个 span 的 span 的 offset MAY 为零,尽管这些 offset SHOULD 通过将其 length 加到前一个 span 来消除。允许这两种情况是为了让原生直方图的生产者 MAY 选择在当前最能平衡资源消耗的表示方式。例如,如果直方图经过多个处理阶段,在最后一个处理阶段之后再消除冗余 span 可能最有效。
本着同样的精神,在某些情况下,从桶列表中排除所有未填充的桶是最有效的,但在其他情况下,通过明确表示少量未填充的桶来减少 span 的数量可能更好。
请注意,未来高分辨率的 schema 可能需要 offset 大于 int32 可表示的范围。在这种情况下,需要对数据模型进行扩展。(当前最高分辨率的标准 schema 是 schema 8,其中包含 MaxFloat64
的桶的索引是 262144,因此 +Inf
溢出桶的索引是 262145,而 int32 可表示的最大数是 2147483647。即使使用 int32 offset 仍可工作的最高标准 schema 将是 schema 20,对应于桶与桶之间仅约 1.000000661 的增长因子。)
一个整数直方图具有以下正数桶(索引→总体):
-2→3, -1→5, 0→0, 1→0, 2→1, 3→0, 4→3, 5→2
它们可以这样表示:
[3, 2, -4, 2, -1]
[[-2, 2], [2,1], [1,2]]
如果明确表示索引为 3 的单个未填充桶,则可以将第二个和第三个 span 合并为一个,得到以下结果:
[3, 2, -4, -1, 3, -1]
[[-2, 2], [2,4]]
或者通过明确表示所有上述未填充桶,将所有 span 合并为一个:
[3, 2, -5, 0, 1, -1, 3, -1]
[[-2, 8]]
正好为零的观测值不适合上述标准 schema 定义的任何桶。它们被计入一个专门的桶,称为零桶。
零桶中的观测次数由一个 uint64(对于整数直方图)或 float64(对于浮点直方图)跟踪。
零桶有一个额外的参数,称为零阈值,这是一个 float64 ≥ 0。如果阈值设置为零,则只有正好为零的观测值进入零桶,这就是上述情况。如果阈值为正值,则闭区间 [-threshold, +threshold] 内的所有观测值都进入零桶,而不是常规桶。这有两个用例:
零桶的阈值**应该**与常规桶的边界重合,这可以避免零桶与常规桶部分重叠的复杂性。但是,如果确实发生了这种重叠,在与零桶重叠的常规桶中计数的观察值**必须**在 [-threshold, +threshold] 区间之外。
要合并具有相同零阈值的直方图,只需简单地将两个零桶相加。然而,如果源直方图中的零阈值不同,则选择任何源直方图中的最大阈值。如果该阈值恰好位于其他源直方图中的任何已填充桶内,则会增加该阈值,直到以下情况之一对于每个源直方图成立:
然后将源零桶和现在在新阈值内的任何源桶相加以得出新零桶的总体。
如果 schema 是 -53(自定义桶),则不使用零桶。
自定义值列表对于标准 schema 未使用。非标准 schema 在需要存储额外数据时以自定义方式使用它。
目前唯一使用自定义值的已定义 schema 是 -53(自定义桶)。本节的其余部分更详细地描述了在这种特定情况下自定义值的用法。
自定义值表示自定义桶的上限(包含)。它们按升序排序。自定义桶本身使用正数桶列表和正数 span 列表存储,尽管它们的边界(由自定义值确定)可能是负数。这些“正数”桶中的每一个的索引定义了其上限在自定义值列表中的基于零的位置。
下限(不包含)由上限之前的自定义值定义。对于第一个自定义值(列表中的位置零),没有前一个值,在这种情况下,下限被认为是 -Inf
。因此,索引为零的自定义桶计算所有在 -Inf
和第一个自定义值之间的观察值。在通常只期望正数观察值的情况下,索引为零的自定义桶**应该**有一个零的上限,以便明确标记是否有零或零以下的观察值。(如果确实只有正数观察值,索引为零的自定义桶将保持未填充状态,因此永远不会显式表示。唯一的代价是在自定义值列表开头多一个零元素。)
最后一个自定义值 MUST NOT 是 +Inf
。大于最后一个自定义值的观测值进入上限为 +Inf
的溢出桶。此溢出桶添加的索引等于自定义值列表的长度。
原生直方图样本可以有零个、一个或多个 exemplar。它们的工作方式与传统 exemplar 相同,但它们组织在一个列表中(因为可以有多个),并且它们 MUST 具有时间戳。
如果经典直方图暴露的 exemplar 具有时间戳,则 MAY 被原生直方图使用。
代码埋点代码 SHOULD 避免观测 NaN
和 ±Inf
值,因为它们在直方图的上下文中意义有限。然而,这些值仍然 MUST 正确处理,如下所述。
观测值的总和像往常一样通过将观测值添加到观测值总和中计算,遵循正常的浮点算术。(例如,观测到 NaN
会将总和设置为 NaN
。观测到 +Inf
会将总和设置为 +Inf
,除非总和已经是 NaN
或 -Inf
,在这种情况下总和设置为 NaN
。)
NaN
的观察值不计入任何桶,但会增加观察值的计数。这意味着观察值的计数可能大于所有桶(负桶、正桶和零桶)的总和,差值就是 NaN
观察值的数量。(对于没有任何 NaN
观察值的整数直方图,所有桶的总和等于观察值的计数。在通常的浮点精度限制内,对于没有任何 NaN
观察值的浮点直方图也是如此。)
观测到 +Inf
或 -Inf
会增加观测计数并增加按以下方式选择的桶:- 对于标准 schema,+Inf
观测值会增加上述的正溢出桶。- 对于标准 schema,-Inf
观测值会增加上述的负溢出桶。- 对于 schema -53(自定义桶),+Inf
观测值会增加索引等于自定义值列表长度的桶。- 对于 schema -53(自定义桶),-Inf
观测值会增加索引为零的桶。
具有标准 schema 的 Prometheus (Prom) 原生直方图可以很容易地映射到 OpenTelemetry (OTel) 指数直方图,反之亦然,详细说明如下。
Prom 的模式(schema) 等同于 OTel 中的刻度(scale),但 OTel 允许低于 -4 和高于 +8 的值。如上所述,如果实际需要,Prom 保留了更多的模式编号来扩展其范围。
索引偏移一个位置,即 Prom 桶的索引为 n,OTel 的索引为 n-1。
OTel 采用密集而非稀疏的桶表示。可以将 OTel 视为“只有单个 span 的 Prom”。
Prom 的零桶(zero bucket)在 OTel 中被称为零计数(zero count)。(Prom 也使用零计数来命名存储零桶中观察值计数 Fields。)两者工作方式相同,包括存在零阈值(zero threshold)。请注意,如果未给出阈值,OTel 会默认阈值为零。
(TODO: OTel 规范读取:“当 zero_threshold 未设置或为 0 时,此桶存储无法使用标准指数公式表示的值以及四舍五入为零的值。”仔细检查这是否真的产生相同的行为。如果在接近零时出现问题,我们可以使 Prom 的规范更精确。如果 OTel 在零桶中计数 NaN,我们必须在此处添加注释。)
OTel 指数直方图仅支持标准指数分桶 schema(正如其名称所示)。因此,NHCBs(或具有其他未来分桶 schema 的原生直方图)无法干净地转换为 OTel 指数直方图。但是,仍然可以转换为具有固定桶的传统 OTel 直方图。
任何类型的 OTel 直方图都有用于存储直方图中观察到的最小值和最大值的可选字段。这些字段在 Prometheus 中没有对应的概念,因为计数型直方图在很长且不可预测的时间跨度内累积数据,并且可以随时抓取,因此跟踪最小值和最大值要么不可行,要么用处有限。但是请注意,原生直方图可以在任意时间跨度内相当准确地估计最大和最小观察值,请参阅PromQL 部分。
经典 Prometheus 用例中的指标暴露以字符串为主,因为所有指标名称、标签名称和标签值占用的空间比 float64 样本值大得多,即使后者以潜在更冗长的文本形式表示。这是过去放弃基于 protobuf 暴露格式的原因之一。
相比之下,遵循上述数据模型的原生直方图包含更多数值数据。这放大了基于 protobuf 格式的优势。因此,先前放弃的基于 protobuf 的暴露被重新启用,以高效地暴露和采集原生直方图。
在构思原生直方图时,OpenMetrics 的采用率仍然不足,特别是 OpenMetrics 的 protobuf 版本没有任何已知应用。因此,最初的方法是扩展经典 Prometheus protobuf 格式以支持原生直方图。(另一个实际考虑是 Go 代码埋点库仍使用经典 protobuf 规范作为其内部数据模型,简化了最初的开发。)
经典 Prometheus 文本格式未扩展以支持原生直方图,并且此类扩展没有计划。(另请参阅下面的 OpenMetrics 部分。)
protobuf 规范有 proto2 和 proto3 两个版本,它们都创建相同的 wire format:
这些文件包含全面的注释,这应该可以轻松地将 proto 规范映射到上面描述的数据模型。
以下是 proto3 文件中的相关部分:
// [...]
message Histogram {
uint64 sample_count = 1;
double sample_count_float = 4; // Overrides sample_count if > 0.
double sample_sum = 2;
// Buckets for the classic histogram.
repeated Bucket bucket = 3 [(gogoproto.nullable) = false]; // Ordered in increasing order of upper_bound, +Inf bucket is optional.
google.protobuf.Timestamp created_timestamp = 15;
// Everything below here is for native histograms (also known as sparse histograms).
// Native histograms are an experimental feature without stability guarantees.
// schema defines the bucket schema. Currently, valid numbers are -4 <= n <= 8.
// They are all for base-2 bucket schemas, where 1 is a bucket boundary in each case, and
// then each power of two is divided into 2^n logarithmic buckets.
// Or in other words, each bucket boundary is the previous boundary times 2^(2^-n).
// In the future, more bucket schemas may be added using numbers < -4 or > 8.
sint32 schema = 5;
double zero_threshold = 6; // Breadth of the zero bucket.
uint64 zero_count = 7; // Count in zero bucket.
double zero_count_float = 8; // Overrides sb_zero_count if > 0.
// Negative buckets for the native histogram.
repeated BucketSpan negative_span = 9 [(gogoproto.nullable) = false];
// Use either "negative_delta" or "negative_count", the former for
// regular histograms with integer counts, the latter for float
// histograms.
repeated sint64 negative_delta = 10; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).
repeated double negative_count = 11; // Absolute count of each bucket.
// Positive buckets for the native histogram.
// Use a no-op span (offset 0, length 0) for a native histogram without any
// observations yet and with a zero_threshold of 0. Otherwise, it would be
// indistinguishable from a classic histogram.
repeated BucketSpan positive_span = 12 [(gogoproto.nullable) = false];
// Use either "positive_delta" or "positive_count", the former for
// regular histograms with integer counts, the latter for float
// histograms.
repeated sint64 positive_delta = 13; // Count delta of each bucket compared to previous one (or to zero for 1st bucket).
repeated double positive_count = 14; // Absolute count of each bucket.
// Only used for native histograms. These exemplars MUST have a timestamp.
repeated Exemplar exemplars = 16;
}
message Bucket {
uint64 cumulative_count = 1; // Cumulative in increasing order.
double cumulative_count_float = 4; // Overrides cumulative_count if > 0.
double upper_bound = 2; // Inclusive.
Exemplar exemplar = 3;
}
// A BucketSpan defines a number of consecutive buckets in a native
// histogram with their offset. Logically, it would be more
// straightforward to include the bucket counts in the Span. However,
// the protobuf representation is more compact in the way the data is
// structured here (with all the buckets in a single array separate
// from the Spans).
message BucketSpan {
sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative).
uint32 length = 2; // Length of consecutive buckets.
}
// A BucketSpan defines a number of consecutive buckets in a native
// histogram with their offset. Logically, it would be more
// straightforward to include the bucket counts in the Span. However,
// the protobuf representation is more compact in the way the data is
// structured here (with all the buckets in a single array separate
// from the Spans).
message BucketSpan {
sint32 offset = 1; // Gap to previous span, or starting point for 1st span (which can be negative).
uint32 length = 2; // Length of consecutive buckets.
}
// [...]
(TODO: 上文尚未包含 NHCB 所需的自定义值。我们目前不需要它,因为 NHCB 可以通过采集经典直方图来摄取。然而,最终在暴露格式中包含自定义桶可能仍然有用,例如用于联邦以及未来可能也使用自定义值的 schema。)
请注意以下几点:
Histogram
proto 消息编码,即现有的 Histogram
消息通过添加用于原生直方图的字段进行了扩展。created_timestamp
的字段在经典和原生直方图之间共享,并且两者的工作方式保持一致。sample_count_float
、cumulative_count_float
)。Bucket
字段和 Bucket
消息用于经典直方图的桶。完全可以创建一个 Histogram
消息,它同时表示同一直方图的经典版本和原生版本。解析器可以自由选择其中一个或两个版本(另请参阅抓取配置部分)。sint64
类型使用 varint 编码,从而编码到较小的消息大小。Histogram
消息 MUST 在重复的 positive_span
字段中包含一个“无操作 span”,即一个 BucketSpan
,其 offset
和 length
都设置为 0。Histogram
消息的重复 Exemplar
字段中,但每个 exemplar MUST 具有时间戳。如果没有以这种方式提供 exemplar,解析器 MAY 使用为经典桶提供的带有时间戳的 exemplar(在 Bucket
消息的 Exemplar
字段中,每个桶最多一个 exemplar)。Histogram
消息的剩余部分大很多,并且 exemplar SHOULD 落入不同的桶,并且大致均匀地覆盖整个桶范围。(这通常优于按观测分布比例表示的 exemplar 分布,因为后者很少会从分布的长尾产生 exemplar,而这些 exemplar 通常是最有趣的。)目前(2024-11-03),OpenMetrics 不支持原生直方图。
由于与经典 Prometheus protobuf 格式相似,将支持添加到 OpenMetrics 的 protobuf 版本相对简单。一个以 PR 形式提出的提案正在审查中。
向 OpenMetrics 的文本版本添加支持更困难,但也高度期望,因为在许多情况下生成 protobuf 是不可行的。文本格式必须在人类可读性与机器高效处理(编码、传输、解码)之间进行权衡。相关工作正在进行中。详情请参阅设计文档。
(TODO: 随着进展更新此部分。)
protobuf 规范通过 protobuf 编译器创建的特定语言绑定,可以进行低级指标暴露创建,包括原生直方图。但是,对于直接的代码埋点,需要一个代码埋点库。
目前(2024-11-03),有两个官方 Prometheus 代码埋点库支持原生直方图:
如果库已经支持 protobuf 暴露,将原生直方图支持添加到其他代码埋点库相对容易。对于纯文本库,需要先完成基于文本的暴露格式。(TODO: 根据需要更新此内容。)
本节不详细介绍如何使用各个代码埋点库(有关这方面的详细信息,请参阅上面链接的文档),而是侧重于常见的用法模式,并提供了有关如何作为代码埋点库一部分实现原生直方图支持的通用指南。数据模型和暴露格式的部分对于代码埋点库的实现高度相关(但本节未重复!)。
用于直方图的实际代码埋点 API 对于原生直方图没有改变。经典直方图和原生直方图以相同的方式接收观测值(关于 exemplar 有细微差别,请参阅下一段)。代码埋点库甚至可以维护同一直方图的经典版本和原生版本,并并行暴露它们,以便采集器可以选择要摄取的版本(详情请参阅关于暴露格式的部分)。用户通过配置设置选择暴露经典和/或原生直方图。
经典直方图的 exemplar 通常通过存储和暴露每个桶的最新 exemplar 来跟踪。只要定义了经典桶,代码埋点库 MAY 为同一直方图的原生版本暴露相同的 exemplar,只要每个 exemplar 都有时间戳。(实际上,采集器 MAY 使用经典版本直方图提供的 exemplar,即使它 otherwise 只摄取原生版本,详情请参阅暴露格式部分。)然而,原生直方图 MAY 分配任意数量的 exemplar,并且代码埋点库 SHOULD 使用此自由度来满足暴露格式部分描述的 exemplar 最佳实践。
代码埋点库 SHOULD 为遵循标准 schema 的原生直方图提供以下配置参数。名称是 Go 库中的示例名称——它们必须根据其他语言的习惯风格进行调整。括号中的值是库 SHOULD 提供的默认值。
NativeHistogramBucketFactor
(1.1): 一个大于一的浮点数,用于确定初始分辨率。库选择一个起始 schema,使得桶宽度从一个桶到下一个桶的增长因子不大于提供的数值。示例值请参阅下表。NativeHistogramZeroThreshold
(2-128): 一个大于等于零的浮点数,用于设置零桶的初始阈值。分辨率是通过增长因子而不是直接提供 schema 来设置的,因为大多数用户不知道 schema 数字背后的数学原理。桶与桶之间增长因子的上限概念易于理解,而无需了解原生直方图的内部工作原理。下表列出了每个有效 schema 的示例因子。
NativeHistogramBucketFactor |
结果 schema |
---|---|
65536 | -4 |
256 | -3 |
16 | -2 |
4 | -1 |
2 | 0 |
1.5 | 1 |
1.2 | 2 |
1.1 | 3 |
1.05 | 4 |
1.03 | 5 |
1.02 | 6 |
1.01 | 7 |
1.005 | 8 |
原生直方图的桶在首次填充时动态创建。观测值分布意外过宽可能导致桶数量意外增加,从而需要更多内存。如果观测值分布可以从外部操控,这甚至可能被用作通过耗尽程序可用内存来进行 DoS 攻击的向量。因此,代码埋点库 SHOULD 提供桶数量限制策略。它 MAY 默认设置一种策略,具体取决于库的典型用例。(TODO: 也许我们应该说应该默认设置一种策略。Go 库目前默认不限制桶数量,并且到目前为止尚未报告相关问题。)
以下描述了 Go 代码埋点库实现的桶数量限制策略。其他库 MAY 遵循此示例,但其他策略也可能可行,具体取决于库的典型使用模式。
该策略由三个参数定义:一个无符号整数 NativeHistogramMaxBucketNumber
,一个时长 NativeHistogramMinResetDuration
,以及一个浮点数 NativeHistogramMaxZeroThreshold
。如果 NativeHistogramMaxBucketNumber
为零(默认值),则不限制桶数量,其他两个参数将被忽略。如果 NativeHistogramMaxBucketNumber
设置为正值,库将尝试将每个直方图的桶数量保持在提供的值。一个典型的限制值为 160,这也是 OTel 指数直方图在类似策略中使用的默认值。(请注意,按标签分区将创建多个直方图。此限制适用于每个直方图,而不是所有直方图的总和。)如果超出限制,将按顺序应用一些补救措施,直到桶数量再次在限制范围内:
NativeHistogramMinResetDuration
,则整个直方图将被重置,即所有桶将被删除,观测值的总和和计数以及零桶都将设置为零。Prometheus 将此视为正常的计数器重置,这意味着在采集之间会丢失一些观测值,因此与采集间隔相比,重置应该很少发生。此外,频繁的计数器重置可能导致 TSDB 存储效率降低(详情请参阅TSDB 部分)。NativeHistogramMinResetDuration
设置为一小时在大多数情况下应该能很好地工作。NativeHistogramMinResetDuration
设置为零,默认值),则不执行重置。相反,会增加零阈值,以将接近零的桶合并到零桶中,从而减少桶的数量。阈值的增加受到 NativeHistogramMaxZeroThreshold
的限制。如果已达到此值(或者它设置为零,默认值),则此步骤不执行任何操作。如果步骤 2 或 3 更改了直方图,则在自上次重置以来经过 NativeHistogramMinResetDuration
后将执行重置,不仅是为了移除桶,也是为了将零阈值和桶分辨率返回到初始值。请注意,这在所有方面都被视为出于其他原因的重置,包括更新所谓的created timestamp。
设置非常低的 NativeHistogramBucketFactor
(例如 1.005)和合理的 NativeHistogramMaxBucketNumber
(例如 160)是很诱人的。通过这种方式,每个直方图总是在给定的桶数量“预算”内具有最高可能的分辨率。(这是 OTel 指数直方图使用的默认策略。它从更高的 schema(20)开始,该 schema 目前在 Prometheus 原生直方图中甚至不可用。)然而,这种策略通常 不 RECOMMENDED 用于 Prometheus 用例。创建后和每次重置后,随着观测值的到来,分辨率会经常降低。这在代码埋点程序和 TSDB 中都会产生 churn,这对于后者尤其有问题。所有这些努力大多是徒劳的,因为涉及直方图的典型查询需要合并许多直方图,在此过程中使用最低的共同分辨率,因此用户最终得到较低的分辨率。TSDB 可以通过在摄取时限制分辨率来防止 churn(参见下方),但如果无论如何都要在摄取时强制执行合理的低分辨率,那么在代码埋点时设置此分辨率更直接。然而,这种策略可能在某些特定情况下值得消耗代码埋点程序内的额外资源,即在代码埋点时无法假定合理分辨率,并且采集器应该有灵活性在采集时选择所需的分辨率。
虽然经典直方图按标签分区(如果桶数量很多)需要谨慎进行,但原生直方图的情况比较宽松。对原生直方图进行分区仍然会创建多个独立的直方图。然而,产生的已分区直方图中的每个直方图填充的桶数量通常会少于原始未分区直方图。(例如,如果按 HTTP 状态码对跟踪 HTTP 请求时长的直方图进行分区,跟踪状态码为 404 的请求的单个直方图的桶分布可能会非常集中在识别未知路径所需的时间周围,仅填充少数桶。)所有已分区直方图填充的桶总数仍会增加,但增幅会小于已分区直方图的数量。(例如,如果向一个已经很重的经典直方图添加标签导致 100 个带标签的直方图,总成本将增加 100 倍。在原生直方图的情况下,如果经典直方图具有高分辨率,单个直方图的成本可能已经较低。分区后,带标签原生直方图填充的桶总数将远小于原始原生直方图桶数量的 100 倍。)
目前(2024-11-03),代码埋点库不提供直接配置具有自定义桶边界的原生直方图(NHCBs)的方法。NHCB 的用例是允许启用原生直方图的采集器在摄取时将经典直方图转换为 NHCBs(参见下一节)。然而,在代码埋点期间直接使用自定义桶存在有效用例。在这些情况下,当前的方法是使用经典直方图进行代码埋点,并配置采集器在摄取时将其转换为 NHCB。然而,未来可能会在代码埋点库中更直接地处理 NHCBs。
为了让 Prometheus 服务器采集原生直方图,需要功能标志 --enable-feature=native-histograms
。此标志还会改变内容协商,使得优先选择经典的基于 protobuf 的暴露格式,而不是 OpenMetrics 文本格式。(TODO: 一旦原生直方图成为稳定功能,此行为将改变。)
从 Prometheus v2.49 及更高版本开始,可以通过 scrape_protocols
配置设置全局或按采集配置微调采集协议协商。这是一个列表,定义了内容协商优先级。其默认值取决于 --enable-feature=native-histograms
标志。如果该标志设置,则为 [ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]
,否则列表中第一个元素 PrometheusProto
将被移除,结果为 [ OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ]
。这些默认值导致了上述行为,即在未设置 --enable-feature=native-histograms
标志时 protobuf 未使用,而设置该标志时它具有最高优先级。
该设置可用于配置 protobuf 采集而不摄取原生直方图,或即使设置了 --enable-feature=native-histograms
标志,也对某些目标强制使用非 protobuf 格式。只要经典的 Prometheus protobuf 格式(配置列表中的 PrometheusProto
)是唯一支持原生直方图的格式,要实际摄取原生直方图就需要同时设置功能标志和协商 protobuf。
(TODO: 一旦原生直方图成为稳定功能或原生直方图得到其他格式支持,请更新此部分。)
quantile
标签(用于 summaries)和 le
标签(用于经典 histograms)的标签值格式化。此问题仅影响 Prometheus 服务器 v2(v3 在所有情况下都有一致的格式化),并且与原生直方图没有直接关系,但可能在同一上下文中出现,因为启用原生直方图需要 protobuf 暴露格式。详细信息请参阅 v2.55 的 native-histograms
功能标志文档。虽然代码埋点库 SHOULD 提供配置选项来限制原生直方图的分辨率和桶数量,但仍然需要在摄取时强制执行这些限制。用户可能无法更改给定程序的代码埋点,或者程序可能故意使用高分辨率直方图进行代码埋点,以使不同的采集器可以根据需要降低分辨率。
Prometheus 采集配置提供了两个设置来满足此需求:
native_histogram_bucket_limit
为单个直方图中的桶数设置了一个上限(包含)。如果超出此限制,具有标准模式的直方图的分辨率会重复降低(通过将桶的宽度加倍,即降低模式)直到达到限制。如果 NHCB 超出限制,或者在极少数情况下即使模式为 -4 也无法满足限制,抓取将失败。native_histogram_min_bucket_factor
为桶之间的增长因子设置了一个下限(包含)。此设置仅与标准模式相关,对 NHCB 无效。同样,如果超出限制,直方图的分辨率会重复降低(通过将桶的宽度加倍,即降低模式)直到达到限制。但是,一旦达到模式 -4,即使指定了更高的增长因子,抓取仍会成功。这两个设置都接受零作为有效值,这意味着“无限制”。对于桶限制,这意味着完全不检查桶的数量。对于桶因子,Prometheus 仍然会确保标准模式不会超出所使用存储后端的容量。(待办:这目前意味着模式最多为 +8,这也是我们在暴露格式中允许的限制。OTel 允许更高的指数模式,Prometheus 因此也可能在摄取路径中允许它们,但在摄取时将其降低到 +8,或当前实现所需的任何限制。请参阅https://github.com/prometheus/prometheus/issues/14168 获取最终澄清。)
如果这两个设置都具有非零值,则会充分降低模式以满足这两个限制。
请注意,仪表化期间设置的桶因子是上限(暴露的桶增长因子 ≤ 配置值),而抓取配置中设置的桶因子是下限(摄取的桶增长因子 ≥ 配置值)。因此,某些限制导致的模式略有不同。一些示例:
native_histogram_min_bucket_factor |
结果最大模式 |
---|---|
65536 | -4 |
256 | -3 |
16 | -2 |
4 | -1 |
2 | 0 |
1.4 | 1 |
1.1 | 2 |
1.09 | 3 |
1.04 | 4 |
1.02 | 5 |
1.01 | 6 |
1.005 | 7 |
1.002 | 8 |
设置限制的一般注意事项:native_histogram_bucket_limit
适用于为单个直方图的成本设置硬限制。这无法通过 native_histogram_min_bucket_factor
实现,因为如果观察值的分布足够广泛,即使分辨率较低,直方图也可以包含许多桶。native_histogram_min_bucket_factor
非常适合避免不必要的总体资源成本。例如,如果当前的用例只需要某个分辨率,为所有直方图设置相应的 native_histogram_min_bucket_factor
可能会释放足够的资源来接受一些观察值分布广泛的直方图上非常高的桶计数。另一个例子是某些直方图由于某种原因具有低分辨率(可能已经在仪表化端)。如果聚合经常包含这些低分辨率直方图,结果将具有相同的低分辨率(请参阅下面的 PromQL 详细信息)。以更高的分辨率存储与低分辨率直方图定期聚合的其他直方图可能没有太大用处。
如上所述,由仪表化程序暴露的直方图可能同时包含经典直方图和原生直方图,并且某些部分甚至共享(例如观察值的计数和总和)。本节解释 Prometheus 将抓取哪些部分以及如何控制行为。
如果没有 --enable-feature=native-histograms
标志,Prometheus 在抓取期间会完全忽略原生直方图部分。(待办:一旦特性标志不再生效,请更新此内容。)如果设置了该标志,即使同一直方图同时暴露了经典和原生部分,Prometheus 也将优先选择原生直方图部分。对于没有原生直方图数据的直方图,Prometheus 仍会抓取经典直方图部分。
在迁移场景等情况下,如果仪表化程序暴露了同一直方图的经典版本和原生版本,可能希望同时抓取这两个版本。为了启用此行为,抓取配置中有一个布尔设置 always_scrape_classic_histograms
。它默认为 false,但如果设置为 true,则会抓取并摄取每个直方图的两个版本,前提是至少有一个经典桶和一个原生桶范围(可能是一个无操作范围)。这不会在 TSDB 中引起任何冲突,因为经典直方图被摄取为带有后缀的多个序列,而原生直方图被摄取为只有一个未修改名称的序列。(示例:一个名为 rpc_latency_seconds
的直方图会生成一个名为 rpc_latency_seconds
的原生直方图序列,以及经典部分的多个序列,即 rpc_latency_seconds_sum
、rpc_latency_seconds_count
和带有不同 le
标签的多个 rpc_latency_seconds_bucket
序列。)
前述的 NHCB 能够将经典直方图建模为原生直方图。通过抓取配置布尔选项 convert_classic_histograms_to_nhcb
,可以将 Prometheus 配置为将经典直方图摄取为 NHCBs。
NHCBs 与经典直方图一样存在合并性有限的问题,但它们的存储成本通常要低得多。
TSDB 对整数直方图和浮点直方图的存储方式不同。通常,整数直方图预计具有更好的压缩效果,因此如果所有桶计数和观察计数都在 int64 范围内具有整数值,TSDB 实现**可以**将浮点直方图存储为整数直方图,以便转换为整数直方图能精确表示原始浮点直方图。(请注意,Prometheus TSDB 尚未利用此选项。)
原生直方图在 TSDB 中需要两种新的块编码(Go 类型 chunkenc.Encoding
):用于整数直方图的 chunkenc.EncHistogram
(字符串表示为 histogram
,数值为 2)和用于浮点直方图的 chunkenc.EncFloatHistogram
(字符串表示为 floathistogram
,数值为 3)。
类似地,WAL 和内存快照(Go 类型 record.Type
)也有两种新的记录类型:用于整数直方图的 record.HistogramSamples
(字符串表示为 histogram_samples
,数值为 9)和用于浮点直方图的 record.FloatHistogramSamples
(字符串表示为 float_histogram_samples
,数值为 10)。出于向后兼容性原因,还有两种直方图记录类型:record.HistogramSamplesLegacy
(histogram_samples_legacy
,7)和 record.FloatHistogramSamplesLegacy
(float_histogram_samples_legacy
,8)。它们在引入 NHCB 所需的自定义值之前使用。支持它们是为了仍然可以读取旧的 WAL。
Prometheus 仅通过其标签识别时间序列。序列中的样本是浮点型(因此是计数器或测量值)还是直方图型(无论何种类型)都不会影响序列的标识。因此,一个序列**可以**包含不同类型和风格的样本混合。实践中,时间序列内样本类型的变化预计非常罕见。它们通常发生在目标仪表化发生变化之后(极少数情况下,同一个指标名称在变化前用于测量值浮点型,变化后用于计数型直方图)或记录规则发生变化之后(例如,旧版本的规则创建测量值浮点型,新版本的规则现在创建测量值直方图,同时保留其名称)。频繁的样本类型变化通常是配置错误的结果(例如,两个不同的记录规则创建不同的样本类型并馈送到同一个序列)。因此,TSDB 实现**必须**处理样本类型的变化,但**可以**以相对低效的方式进行。当 Prometheus TSDB 遇到无法写入当前使用的块的样本类型时,它会关闭该块并使用适当的编码启动一个新块。(一个每个样本都在样本类型之间来回切换的时间序列将导致每个样本都有一个新块,这确实非常低效。)
直方图块使用多种自定义编码处理数值,以便通过对常用值使用比不常用值更少的位数来减小数据大小。每种自定义编码的详细信息在低级块格式文档中描述(最终也在此处链接的代码中)。以下三种编码用于许多不同的字段,因此在此处命名以便将来参考:
直方图块通常以块中的样本数量(uint16 类型)开始,后跟一个字节,描述直方图是 Gauge(测量)直方图还是 Counter(计数器)直方图,并为后者提供计数器重置信息。有关详细信息,请参阅下方相应部分。之后是所谓的块布局,它包含以下信息,由块中的所有直方图共享
块布局之后是重复的样本数据序列。整型直方图和浮点直方图的样本数据不同。对于整型直方图,每个样本的数据包含以下内容
浮点直方图的样本数据有以下差异
以下事件会触发切一个新的块(原因在括号中描述)
Span 的差异也会改变块布局,但通过根据需要添加(显式表示的)未填充桶来协调,以便块中的所有直方图共享相同的 Span 结构。如果一个桶消失,这很简单,因为在直方图追加到块时,缺失的桶会简单地作为未填充桶添加到新的直方图中。然而,先前填充的桶消失构成计数器重置(见下文内容),因此这种情况只可能发生在 Gauge 直方图中(Gauge 直方图不涉及计数器重置)。更常见的情况是,新追加的直方图中存在先前追加的直方图中不存在的桶。在这种情况下,这些桶必须作为显式未填充桶添加到所有先前追加的直方图中。这需要对整个块进行完整的重新编码。(仅对受影响部分进行重新编码存在一些优化潜力。实现它会非常复杂。到目前为止,完整重新编码的性能影响尚未突出为问题。)
NaN
值的特定位模式之一表示。这个非常特定的浮点值在以下部分中称为“特殊陈旧 NaN
值”。它(几乎肯定)不会由常规浮点算术运算返回,因此不同于“自然产生的” NaN
值,包括在观测值的特殊情况中讨论的那些。实际上,在查询 TSDB 时,特殊陈旧 NaN
值永远不会直接返回,但在到达调用者之前会在内部处理。为了标记直方图序列中的陈旧,可以使用通常的特殊陈旧 NaN
值。然而,这需要切一个新的块,仅仅为了将序列标记为陈旧,因为直方图值之后的浮点值必须存储在不同的块中(见上文)。因此,也存在直方图版本的陈旧标记,其中观测总和字段设置为特殊陈旧 NaN
值。在这种情况下,所有其他字段都会被忽略,这使得可以将它们设置为适合高效存储的值(因为直方图版本的陈旧标记本质上只是存储优化)。这适用于浮点和整型直方图(因为即使在整型直方图中,总和字段也是浮点值),并且可以使用适当的版本来避免切一个新的块。TSDB 必须将所有版本的陈旧标记(浮点、整型直方图、浮点直方图)视为等效。
浮点块的大小限制为 1024 字节。直方图块通常也使用相同的尺寸限制。然而,单个直方图如果包含许多桶,可能会变得非常大,因此盲目强制执行大小限制可能导致块中直方图数量非常少。(在最极端的情况下,单个直方图甚至可能超过 1024 字节,从而完全无法强制执行大小限制。)每个块中的直方图数量非常少时,压缩率会变差。因此,在 1024 字节的大小限制生效之前,每个块必须至少包含 10 个直方图。这意味着直方图块可能远大于 1024 字节。
要求每个块至少有 10 个直方图是一个初步的、非常简单的方法,未来可能会改进,以在块大小和压缩率之间找到更好的权衡。
通常,Prometheus 认为计数器在其值从一个样本到下一个样本下降时发生了重置(但也请参见下一节关于 created timestamp 的内容)。在检测两个直方图样本之间的计数器重置时,情况更复杂。
首先,Gauge 直方图和 Counter 直方图是明确不同的(而 Prometheus 通常在摄取后平等对待所有浮点样本,无论它们是作为 Gauge 还是 Counter 指标摄取的)。计数器重置不适用于 Gauge 直方图。
如果时间序列中一个 Gauge 直方图后跟一个 Counter 直方图,假定发生了计数器重置,因为从 Gauge 变为 Counter 被认为等同于 Gauge 被删除,并且 Counter 从零开始新建。
最常见的情况是一个 Counter 直方图后跟另一个 Counter 直方图。在这种情况下,可能发生的计数器重置通过以下程序检测
如果两个直方图在 schema 或零值桶宽度上不同,这些变化可能是兼容的分辨率降低的一部分(这通常会发生,以减少直方图的桶数量)。对于兼容的分辨率降低,以下两点都成立
如果任一条件不满足,则变化不是兼容的分辨率降低。因为只有通过重置或新建直方图才可能发生这种变化,它被视为计数器重置,检测程序结束。
如果两个条件都满足,必须转换第一个直方图,使其 schema 和零值桶宽度与第二个直方图匹配。这与之前描述的方式相同:合并相邻的桶以降低 schema,并将常规桶与零值桶合并以增加零值桶的宽度。
在此程序的这一点,两个直方图具有相同的 schema 和零值桶宽度,要么是因为一开始就是这样,要么是因为第一个直方图已经相应地转换了。(请注意,NHCB 不使用零值桶。为了本程序的目的,它们的零值桶宽度和总体数量被视为相等。)在这种情况下,以下任何情况都构成计数器重置
如果以上都不是,则没有计数器重置。
由于整个过程相对复杂,计数器重置检测最好在摄取期间发生一次,结果被持久化以便后续使用。摄取期间必须发生计数器重置检测,因为计数器重置是触发切新块的因素之一。
计数器重置后切新块旨在提高压缩率。计数器重置将所有桶的总体数量设为零,因此需要表示的桶更少。然而,一个块必须表示该块中所有直方图的所有桶的超集,因此切新块可以使新块的桶集更简单。
这反过来意味着块中第一个样本之后永远不会发生计数器重置。因此,唯一需要持久化的计数器重置信息是块中第一个直方图的信息。这发生在所谓的直方图标志中,一个紧接在块中样本数量之后存储的单字节。该字节目前仅用于计数器重置信息,但将来可能用于其他标志。计数器重置信息使用前两位。四种可能的位模式在 chunkenc
包中表示为 CounterResetHeader
类型的 Go 常量。它们的名称和含义如下
GaugeType
(位模式 11
):该块包含 Gauge 直方图。计数器重置与 Gauge 直方图无关。CounterReset
(位模式 10
):在前一个块的最后一个直方图和这个块的第一个直方图之间发生了计数器重置。(很可能计数器重置实际上是切新块的原因。)NotCounterReset
(位模式 01
):在前一个块的最后一个直方图和这个块的第一个直方图之间没有发生计数器重置。(这通常发生在因前一个块达到大小限制而切新块时。)UnknownCounterReset
(位模式 00
):不确定在前一个块的最后一个直方图和这个块的第一个直方图之间是否发生了计数器重置。UnknownCounterReset
始终是安全的选择。它不会阻止计数器重置检测,但仅要求在需要计数器重置信息时(再次)执行计数器重置检测程序。
查询 TSDB 时,计数器重置信息会传播给调用者(在 Go 代码中,作为 Go 类型 Histogram
和 FloatHistogram
中 CounterResetHint
类型的一个字段,使用与上方位模式常量同名的枚举常量)。
对于 Gauge 直方图,CounterResetHint
始终是 GaugeType
。任何其他 CounterResetHint
值都表示该直方图是 Counter 直方图。通过这种方式,查询者(包括 PromQL 引擎,见下文)可以获得直方图是 Gauge 还是 Counter 的信息(这与浮点样本显著不同)。
只要 Counter 直方图从单个块按顺序返回,块中第二个及后续直方图的 CounterResetHint
被设置为 NotCounterReset
。(重叠块和乱序摄取可能导致直方图序列来自多个块,这需要特殊处理,见下文。)
从 Counter 直方图块返回第一个直方图时,CounterResetHint
必须(MUST)设置为 UnknownCounterReset
,除非 TSDB 实现能确保之前返回的直方图确实是摄取时用作前一个直方图来检测计数器重置的同一个直方图。只有在后一种情况下,块中的计数器重置信息可以(MAY)直接用作返回直方图的 CounterResetHint
。
需要采取这种预防措施是因为块可能以各种方式被移除或插入(例如通过逻辑删除标记删除或添加块进行回填)。计数器重置虽然归因于一个样本,但实际上是发生在标记样本与前一个样本之间。删除前一个样本或在这两个样本之间插入另一个样本会使之前执行的计数器重置检测失效。
UnknownCounterReset
。请参阅跟踪问题,了解改变这一现状的努力。如上文所示,如果 CounterResetHint
设置为 UnknownCounterReset
,查询者必须(MUST)再次执行计数器重置检测程序。
在处理重叠块或乱序样本时(用于查询或压缩期间),必须特别小心。在这些情况下,可能会发生计数器重置的过度检测和检测不足,如下例所示
CounterResetHint
为 UnknownCounterReset
,这要求查询者进行计数器重置检测(利用上述安全回退机制)。CounterResetHint
为 CounterReset
返回给查询者,尽管现在 C 和 D 之间没有计数器重置。类似于上一个示例中的情况,必须在 A 和 B 之间执行新的计数器重置检测,并在 C 和 D 之间执行另一次。或者 B 和 D 都必须以 CounterResetHint
为 UnknownCounterReset
返回。总之,每当 TSDB 无法安全确定在摄取时两个样本之间是否发生了计数器重置检测,它要么必须执行另一次计数器重置检测,要么必须为第二个样本返回 CounterResetHint
为 UnknownCounterReset
。
请注意,可能存在上述程序未检测到的计数器重置,即,如果重置的直方图中的计数增长得足够快,以至于计数器重置后的第一个样本与重置前的最后一个样本相比没有减少的计数。(这也是浮点计数器的问题,在那里实际上更容易发生。)借助上述机制,即使在这种情况下也可以存储计数器重置,前提是通过其他方式检测到了计数器重置。然而,由于块的插入和删除、乱序样本以及重叠块(如上文所述)引起的复杂性,如果需要第二轮计数器重置检测,此信息可能会丢失。(待办:目前,此信息确定会丢失,见上文待办事项。)更安全地标记计数器重置的方法是通过 created timestamps(见下一节)。
OpenMetrics 为计数器、摘要和经典计数器直方图引入了所谓的 created timestamps。(这个词可能是“created-at timstamp”的缩写。更合适的术语可能是“creation timestamp”或“reset timestamp”,但“created timestamp”这个术语现已牢固确立。)
created timestamp 提供了指标被创建或重置的最新时间。一份设计文档描述了 Prometheus 如何处理 created timestamps。
created timestamps 对原生直方图也很有用。就像为浮点计数器插入合成零样本一样,为计数器直方图插入直方图样本的零值。直方图的零值没有已填充的桶,并且观测总和、观测计数和零值桶总体数量都为零。直方图的 Schema、零值桶宽度、自定义值以及浮点 vs 整型特性应该(SHOULD)与紧随合成零样本之后的样本匹配(以避免触发虚假的计数器重置)。
合成零样本的计数器重置信息始终设置为 CounterReset
。(待办:目前,Prometheus 可能将系列中第一个样本设置为 UnknownCounterReset
,这虽然没错,但我认为设置为 CounterReset
更合理。)
原生直方图的 Exemplars 作为整体附加到直方图样本上,而不是附加到单个桶上。(另请参见暴露格式部分。)因此,允许(并且实际上是常见情况)单个原生直方图样本附加多个 Exemplars。
Exemplars 可能在一个抓取周期到下一个抓取周期之间发生变化,也可能不变化。Scraper 应该(SHOULD)检测未变化的 Exemplars,以避免存储许多重复的 Exemplars。然而,重复检测可能代价高昂,考虑到单个样本可能有很多 Exemplars,其中任何子集都可能是上次抓取的重复 Exemplars。TSDB 可以(MAY)依赖以下假设:任何新的 Exemplar 都比任何先前暴露的 Exemplar 具有更晚的时间戳。(请记住,原生直方图的 Exemplars 必须(MUST)具有时间戳。)然后可以高效地进行重复检测
仅当摄取的直方图的所有 Exemplars 都排在最后成功附加的 Exemplar 之前时,这些 Exemplars 才被计为乱序。这无法检测到与更新的 Exemplars 或与最后成功附加的 Exemplar 的副本混合的乱序 Exemplars,这是可接受的。
本节描述 PromQL 如何处理原生直方图。它侧重于一般概念,而不是单个操作的每个具体细节。对于后者,请参阅 PromQL 关于运算符和函数的文档。
原生直方图的引入会创建某些 PromQL 表达式返回意外结果的情况,最常见的情况是输出向量中的部分或全部元素意外缺失。为了帮助用户检测和理解这些情况,处理原生直方图的操作通常使用注解(annotations)。注解可以有 warn(警告)和 info(信息)级别,并描述评估期间可能遇到的问题。Warn 级别用于标记用户很可能需要处理的实际问题。Info 级别用于标记可能是有意为之,但仍不寻常到需要标记的情况。
PromQL 始终处理浮点直方图。存储为整型直方图的原生直方图在从 TSDB 检索时会自动转换为浮点直方图。
当运算符或函数处理两个或多个原生直方图时,所涉及的直方图需要具有相同的 schema 和零值桶宽度。在一定范围内,直方图可以即时转换以满足这些兼容性标准
如果不兼容阻止了操作,结果中会添加 warn 级别的注解。
计数器重置的定义如上文所述。从 TSDB 返回的计数器重置提示可以(MAY)被考虑在内,以避免显式的计数器重置检测,并正确处理常规程序无法检测到的计数器重置。(这意味着这些计数器重置仅在尽力而为的基础上考虑。然而,TSDB 本身也是如此,见上文。)与经典直方图和摘要的计数器重置处理的一个显著区别是,观测总和的减少本身不构成计数器重置。(例如,即使直方图观测到负值,计算原生直方图的速率仍然能正常工作。)
请注意,子查询返回的 Counter 直方图的计数器重置提示不得(MUST NOT)被考虑在内,以避免显式计数器重置检测,除非 PromQL 引擎可以安全地检测到子查询返回的连续 Counter 直方图在 TSDB 中也是连续的。
通过从 TSDB 返回的计数器重置提示,PromQL 知道原生直方图是 Gauge 直方图还是 Counter 直方图。为了镜像 PromQL 对浮点样本的处理方式(它无法可靠地区分浮点 Counter 和 Gauge),作用于 Counter 的函数仍会处理 Gauge 直方图,反之亦然,但结果会返回一个 warn 级别的注解。请注意,在这种情况下,必须对 Gauge 直方图执行显式的计数器重置检测,将其视为 Counter 直方图处理。
在估计分位数或分数时,PromQL 必须在桶内应用插值。在经典直方图中,这种插值是线性进行的。它基于观测值在桶内均匀分布的假设。实际上,这个假设可能与实际情况相去甚远。(例如,一个 API 端点可能对几乎所有请求都以 110ms 的延迟响应。那么中位延迟和甚至 90% 分位延迟都将接近 110ms。如果经典直方图的桶边界在 100ms 和 200ms,它将看到该范围内的绝大多数观测值,并估计中位数为 150ms,90% 分位数为 190ms。)最坏的情况是在桶的一端进行估计,而实际值在桶的另一端。因此,最大可能误差是桶的整个宽度。不进行任何插值,并在桶内使用某个固定中点(例如算术平均值,甚至是调和平均值)将最小化最大可能误差(对于算术平均值来说,误差将是桶宽度的一半),但实际上,线性插值得到的平均误差更低。由于这种插值方法在经典直方图的使用中多年来效果良好,原生直方图也应用了插值。
对于 NHCB,PromQL 应用与经典直方图相同的插值方法以保持结果一致。(NHCB 的主要用例是经典直方图的直接替代品。)然而,对于标准指数 schema,线性插值可能被视为不匹配。虽然指数 schema 主要旨在最小化分位数估计的相对误差,但它们也受益于桶的平衡使用,至少在某些观测值范围内。基本假设是,对于大多数实际出现的分布,观测值的密度倾向于在较小观测值处更高。因此,PromQL 对标准 schema 使用指数外插,这模拟了这样一个假设:当 schema 号增加一(即分辨率翻倍)时将一个桶分成两个,平均来说,这两个新桶将看到相似的总体数量。更详细的解释可以在实现插值方法的PR中找到。
一个特例是零值桶内的插值。零值桶打破了指数分桶 schema。因此,在零值桶内应用线性插值。此外,如果直方图的所有已填充常规桶都是正的,假定零值桶中的所有观测值也是正的,即,插值在零和零值桶的上界之间进行。在直方图的所有已填充常规桶都是负数的情况下,情况是镜像的,即,零值桶内的插值在零值桶的下界和零之间进行。
如上文所述,无论是样本类型还是原生直方图的特性都不是序列身份的一部分。因此,同一个序列可能包含不同样本类型和特性的混合。
Counter 直方图和 Gauge 直方图的混合不会阻止任何 PromQL 操作,但如果某些输入样本具有不适当的特性,则结果中会返回 warn 级别的注解(见上文)。
浮点样本和直方图样本的混合更具问题。许多作用于范围向量的函数会从结果中移除输入元素包含浮点数和直方图混合的元素。如果发生这种情况,结果中会添加 warn 级别的注解。具体示例可在下方找到。
一元负号可用于原生直方图。它返回一个直方图,其中所有桶的总体数量、观测计数和观测总和的符号都反转了。其他一切保持不变,包括计数器重置提示。然而请注意,显式的计数器重置检测会因反转的符号而失效。(待办:也许我们应该将所有负向直方图标记为 Gauge?)负向直方图本身没有实际意义,仅应在其他表达式中用作中间结果。
大多数二元运算符不适用于两个直方图之间,或直方图与浮点数之间,或直方图与标量之间。如果运算符处理这种不可能的组合,对应的元素将从输出向量中移除,并在结果中添加 info 级别的注解。(这种情况有点类似于标签匹配,其中样本类型扮演着类似于标签的角色。因此,这种不匹配可能是已知且有意为之的,这就是注解级别仅为 info 的原因。)
以下描述了实际确实有效的操作。
加法(+
)和减法(-
)适用于两个兼容的直方图。这些运算符会加减所有匹配桶的总体数量、观测计数和观测总和。缺失的桶被假定为空并相应处理。减法可能导致负向直方图,见上文注释。通常,两个操作数都应该是 Gauge。加减 Counter 直方图需要谨慎,但 PromQL 允许这样做。加法运算中,Gauge 直方图和 Counter 直方图相加的结果是 Gauge 直方图。两个具有矛盾计数器重置提示的 Counter 直方图相加会触发 warn 级别的注解。(待办:后者尚未实现。此外,减法操作尚未检查/修改计数器重置提示。这应该在 PromQL 文档中详细记录。)
乘法(*
)适用于一侧是浮点样本或标量,另一侧是直方图,顺序不限。它将所有桶的总体数量、观测计数和观测总和乘以浮点值(样本或标量)。这将导致“缩放的”甚至有时是负向的直方图,这通常仅在其他表达式中作为中间结果有用(另见上文注释)。乘法适用于 Counter 直方图和 Gauge 直方图,它们的特性不受操作影响而保持不变。
除法(/
)适用于左侧是直方图,右侧是浮点样本或标量的情况。它等同于乘以浮点值(样本或标量)的倒数。除以零会导致直方图没有常规桶,并且零值桶总体数量、观测计数和观测总和都设置为 +Inf
、-Inf
或 NaN
,取决于它们在输入直方图中的值(分别为正、负或零/NaN
)。
等于(==
)和不等于(!=
)适用于两个直方图之间,包括过滤版本以及带有 bool
修饰符的情况。它们比较 schema、自定义值、零阈值、所有桶的总体数量以及观测总和与计数。直方图是 Counter 特性还是 Gauge 特性与比较无关。(Counter 直方图可能等于 Gauge 直方图。)
逻辑/集合二元运算符(and
、or
、unless
)即使涉及直方图样本也能按预期工作。它们只检查向量元素是否存在,并且不根据元素的样本类型或特性(浮点或直方图,Counter 或 Gauge)改变其行为。
“裁剪”运算符 >/
和 </
是专为原生直方图引入的。它们仅适用于左侧是直方图,右侧是浮点样本或标量的情况。(它们不适用于两侧都是浮点样本或标量的情况。在这种情况下,会返回 info 级别的注解。)这些运算符分别从直方图中移除大于或小于右侧浮点值的观测值,并返回结果直方图。只有当阈值与桶边界重合时,移除才是精确的。否则,必须使用受影响桶内的插值,如上文所述。直方图的 Counter vs Gauge 特性得以保留。(待办:这些运算符尚未实现,细节可能也会改变,见跟踪问题。)
以下聚合运算符与浮点和直方图样本以相同方式工作(原因在括号中说明)
group
(此聚合的结果不依赖于样本值。)count
(此聚合的结果不依赖于样本值。)count_values
(使用 Go FloatHistogram.String
方法生成的文本表示作为直方图的值。)limitk
(采样元素未改变地返回。)limit_ratio
(采样元素未改变地返回。)sum
聚合运算符处理原生直方图的方式是将要聚合的直方图相加(方式与上文描述的 +
运算符相同)。avg
聚合运算符以相同方式工作,但将总和除以聚合直方图的数量(方式与上文描述的 /
运算符相同)。这两个聚合运算符都会从输出向量中移除元素,这些元素需要将浮点样本与直方图样本进行聚合。这种移除会通过 warn 级别的注解进行标记。
所有其他聚合运算符都不适用于原生直方图。输入向量中的直方图会被直接忽略,并且每个被忽略的直方图都会添加一个 info 级别的注解。
以下函数作用于原生直方图的范围向量,通过对匹配的桶(包括零值桶)以及观测总和和计数分别应用通常的浮点操作,从而生成一个新的原生直方图
delta()
(用于 Gauge 直方图。)increase()
(用于 Counter 直方图。)rate()
(用于 Counter 直方图。)idelta()
(用于 Gauge 直方图。)irate()
(用于 Counter 直方图。)如上文所述,这些函数应该(SHOULD)应用于 Gauge 直方图或 Counter 直方图。然而,它们都适用于这两种特性,但如果范围向量中至少包含一个特性不合适的直方图,结果中会添加 warn 级别的注解。
delta()
、increase()
和 rate()
对于范围内包含浮点样本和直方图样本混合的序列不返回结果。idelta()
和 irate()
对于范围内最后两个样本是浮点样本和直方图样本混合的序列不返回结果。在任一情况下,由于这些原因缺失的每个输出元素都会添加 warn 级别的注解。
所有这些函数都返回 Gauge 直方图作为结果。
和往常一样,这些函数尝试通过将 schema 转换为共同 schema 来协调不同的 schema,尽可能实现。然而,应用于 Counter 的函数(increase()
、rate()
、irate()
)如果在第一个和第二个样本之间存在计数器重置,则不会对第一个样本执行此转换。在这种情况下,第一个样本不包含在计算中,因此,第一个样本与其他样本之间不兼容的桶布局会被静默忽略。
avg_over_time()
和 sum_over_time()
函数处理原生直方图的方式与相应的聚合运算符相对应。特别是,如果序列在范围内包含浮点样本和直方图样本的混合,对应的结果将从输出向量中完全移除。这种移除会通过 warn 级别的注解进行标记。
changes()
和 resets()
函数处理原生直方图样本的方式与处理浮点样本的方式相同。它们甚至可以处理同一序列中浮点样本和直方图样本的混合。在这种情况下,从浮点样本到直方图样本的改变(反之亦然)计为 changes()
函数的改变,并计为 resets()
函数的重置。从 Counter 直方图到 Gauge 直方图的特性变化(反之亦然)不计为 changes()
函数的改变。resets()
应该(SHOULD)仅应用于浮点 Counter 和 Counter 直方图,但该函数仍适用于 Gauge 直方图,在这种情况下应用显式计数器重置检测。此外,从 Counter 直方图到 Gauge 直方图的改变(反之亦然)被计为重置。
histogram_quantile()
函数具有非常特殊的角色,因为它是唯一一个特殊处理特定“魔法”标签的函数,即经典直方图使用的 le
标签。histogram_quantile()
也以类似的方式适用于原生直方图,但没有 le
标签的特殊作用。该函数继续以已知的方式处理浮点样本,同时对原生直方图样本使用新的“原生”方式。
经典直方图典型查询示例(包括 rate
和聚合)
histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket[10m])))
这是原生直方图对应的查询: histogram_quantile(0.9, sum by (job) (rate(http_request_duration_seconds[10m])))
与经典直方图一样,可以对直方图中的最大和最小观测值进行估计,分别使用 1 和 0 作为 histogram_quantile
的第一个参数。然而,具有标准 schema 的原生直方图能够提供更有用的结果,不仅因为原生直方图通常具有更高的分辨率,更重要的是,具有标准 schema 的原生直方图在整个 float64 数值范围内保持相同的分辨率。对于经典直方图,最大观测值很可能在 +Inf 桶中,因此估计值只是简单地返回 +Inf 桶之前的最后一个桶的上界。类似地,最小观测值通常会在最低的桶中。
histogram_quantile
将值为 NaN
的观测值(这不应该(SHOULD NOT)发生,见上文)有效地视为 +Inf
的观测值。这遵循 NaN
永远不小于 histogram_quantile
返回的任何值的原理,并且与经典直方图通常如何处理 NaN
观测值一致(在大多数实现中,它们会落在 +Inf
桶中)。(待办:此行为的正确实现仍需要通过测试进行验证。)
以下函数是专为原生直方图引入的。
histogram_avg()
histogram_count()
histogram_fraction()
histogram_sum()
histogram_stddev()
histogram_stdvar()
所有这些函数都静默忽略作为输入的浮点样本。每个函数都返回一个浮点样本向量。
histogram_count()
和 histogram_sum()
分别返回原生直方图中所包含的观测计数或观测总和。由于它们是普通函数,其结果不能在范围选择器中使用。代替使用子查询,计算观测计数或总和的速率的推荐方法是先对直方图进行 rate 运算,然后将 histogram_count()
或 histogram_sum()
应用于结果。例如,以下查询计算原生直方图的观测速率(在本例中对应于“每秒请求数”): histogram_count(rate(http_request_duration_seconds[10m]))
请注意,当对 histogram_sum()
的结果使用子查询时,原生直方图的特殊计数器重置检测不适用,即,负观测值可能导致虚假的计数器重置。
histogram_avg()
返回原生直方图中观测值的算术平均值。(这与对多个原生直方图应用 avg
聚合运算符显著不同。后者返回一个平均后的直方图。)
类似地,histogram_stddev()
和 histogram_stdvar()
分别返回原生直方图中观测值的估计标准差或标准方差。对于此估计,假定桶中的所有观测值都具有桶边界均值的值。对于零值桶和具有自定义边界的桶,使用算术平均值。对于标准指数桶,使用几何平均值。
histogram_fraction(lower, upper, histogram)
返回 histogram
中在给定边界(标量值 lower
和 upper
)之间观测值的估计比例。估计误差取决于底层原生直方图的分辨率,以及给定边界与直方图桶边界的对齐程度。+Inf
和 -Inf
是有效的边界值,并且可用于估计高于或低于某个值的所有观测值的比例。然而,值为 NaN
的观测值始终被视为在指定边界之外(即使是 +Inf
和 -Inf
)。(待办:通过测试验证此行为的正确实现。)给定的边界是包含还是排除仅在给定边界与底层原生直方图的桶边界精确对齐时才有关联。在这种情况下,行为取决于直方图 schema 的精确定义。
以下函数不直接与样本值交互,因此处理原生直方图样本的方式与处理浮点样本的方式相同
absent()
absent_over_time()
count_over_time()
info()
label_join()
label_replace()
last_over_time()
present_over_time()
sort_by_label()
sort_by_label_desc()
timestamp()
本节未提及的所有其余函数都不适用于原生直方图。输入向量中的直方图元素会被静默忽略。对于 deriv()
、double_exponential_smoothing()
、predict_linear()
以及之前未提及的所有 <aggregation>_over_time()
函数,原生直方图样本会从输入范围向量中移除。如果任何系列在范围内包含浮点样本和直方图样本的混合,移除直方图会被 info 级别的注解标记。
记录规则可以(MAY)生成原生直方图值。它们像正常摄取一样存回 TSDB,包括直方图是 Gauge 直方图还是 Counter 直方图。在后一种情况下,由计数器重置提示显式标记的计数器重置也会被存储,否则在摄取期间会启动新的计数器重置检测。
TSDB 实现可以(MAY)将记录规则创建的浮点直方图转换为整型直方图,如果这种转换能够精确表示原始直方图中的所有浮点值。
告警处理原生直方图的方式与往常一样。然而,建议(RECOMMENDED)避免将原生直方图作为告警的输出值。如果在模板中使用原生直方图样本,它们会以简单的文本形式渲染(由 Go FloatHistogram.String
方法生成),这对于人类来说很难阅读。
PromQL 测试框架已得到扩展,以便 PromQL 单元测试以及通过 promtool
进行的规则单元测试都可以包含原生直方图。直方图样本的表示法很复杂,并在规则单元测试文档中进行了解释。
在单元测试框架中,有一个名为 load_with_nhcb
的备用 load
命令,它将经典直方图转换为 NHCB,并加载经典直方图的浮点序列以及转换生成的 NHCB 序列。
虽然不特定于原生直方图,但在其上下文中非常有用的是,单元测试框架中的 expect
关键字,它可以定义对 info 和 warn 级别注解的预期。
和往常一样,PromQL 实现可以(MAY)在行为保持不变的前提下应用任何认为合适的优化。解码原生直方图可能会非常耗费资源,因为它可能包含许多桶。类似地,在 PromQL 引擎内部深度复制一个直方图样本比复制一个简单的浮点样本要昂贵得多。这与始终解码所有内容和始终复制所有内容的简单方法相比,产生了巨大的优化潜力。
Prometheus 目前尝试避免不必要的复制(待办:但仍需实现适当的 CoW (Copy-on-Write) 类方法,因为它会更清晰且更不易出错),并在只需要观测总和和计数的情况下跳过桶的解码。
查询 API 文档包含了对原生直方图的支持。本节侧重于与原生直方图相关的部分,并提供了一些 API 文档中未包含的背景信息。
为了在即时查询(query
端点)和范围查询(query_range
端点)的 JSON 响应中返回原生直方图,vector
和 matrix
结果类型都需要通过一个新键进行扩展。
vector
结果类型在与现有 value
键相同的层级上新增一个键 histogram
。这两个键是互斥的,即,vector
中的每个元素要么有一个 value
键(用于浮点结果),要么有一个 histogram
键(用于直方图结果)。histogram
键的值结构类似于 value
键的值(一个包含两个元素的数组),不同之处在于,表示浮点样本值的字符串被替换为一个下方描述的特定直方图对象。
matrix
结果类型在与现有 values
键相同的层级上新增一个键 histograms
。这些键不互斥。一个序列可能包含浮点值和直方图值,但对于给定的时间戳,只能有一个样本,要么是浮点数,要么是直方图。histograms
键的值结构类似于 values
键的值(一个包含 n 个两个元素数组的数组),不同之处在于,表示浮点样本值的字符串被替换为一个下方描述的特定直方图对象。
注意,更好的键命名方式是 float
/histogram
和 floats
/histograms
,因为浮点值和直方图值都是值。当前的命名有历史原因。(过去只有一种值类型,即浮点数,因此简单地将键称为 value
和 values
是显而易见的选择。)此处的意图是为了不破坏不知道原生直方图的现有消费者。
上述提到的直方图对象具有以下结构
{
"count": "<count_of_observations>",
"sum": "<sum_of_observations>",
"buckets": [ [ <boundary_rule>, "<left_boundary>", "<right_boundary>", "<count_in_bucket>" ], ... ]
}
count
和 sum
直接对应于同名的直方图字段。每个桶(包括零值桶)都显式地表示其边界和计数。因此 Span 和 schema 不包含在响应中,直方图对象的结构不依赖于使用的 schema。
<boundary_rule>
占位符是一个介于 0 到 3 之间的整数,含义如下
对于标准 schema,正向桶是“左开”,负向桶是“右开”,零值桶(具有负左边界和正右边界)是“双闭”。对于 NHCB,所有桶都是“左开”(镜像经典直方图的行为)。未来 schema 可能会使用不同的边界规则。
对于 series
端点,包含原生直方图的序列与仅包含浮点数的常规序列以相同方式包含。该端点不提供包含哪些样本类型的信息(实际上,任何序列都可能包含一种或两种样本类型)。特别注意,如果目标暴露的直方图名为 request_duration_seconds
并且作为原生直方图暴露和摄取,则会产生一个名为 request_duration_seconds
的序列;但如果作为经典直方图暴露和摄取,则会产生一组名为 request_duration_seconds_sum
、request_duration_seconds_count
和 request_duration_seconds_bucket
的序列。如果直方图作为原生直方图和经典直方图同时摄取,series
端点将返回上述所有序列名称。
目标和指标元数据(targets/metadata
和 metadata
端点)工作方式略有不同,因为它们作用于目标暴露的原始名称。这意味着名为 request_duration_seconds
的经典直方图只会由这些元数据端点表示为 request_duration_seconds
(而不是 request_duration_seconds_sum
、request_duration_seconds_count
或 request_duration_seconds_bucket
)。原生直方图 request_duration_seconds
也会以此名称表示。即使 request_duration_seconds
同时作为经典和原生直方图摄取,也不会发生冲突,因为返回的元数据实际上是相同的(最值得注意的是返回的 type
将是 histogram
)。换句话说,目前无法仅通过元数据端点区分原生直方图和经典直方图。需要通过 series
端点进行额外查找。没有改变这一现状的计划,因为现有的元数据端点无论如何都严重受限(没有历史信息,没有规则创建的指标的元数据,处理不同目标之间元数据冲突的能力有限)。然而,Prometheus 总体上计划改进元数据处理。这些努力也将考虑如何妥善支持原生直方图。(待办:随进度更新。)
本节描述 Prometheus 自带 UI 如何渲染直方图。这可以(MAY)用作第三方图形前端的指南。
在表格视图中,直方图数据点以条形图形式图形化呈现,同时呈现所有桶的文本表示,包括它们的下限和上限,以及观测计数和总和。条形图中的每个条代表一个桶。每个条在x轴上的位置由对应桶的下限和上限决定。每个条的面积与对应桶的总体数量成比例(这是渲染直方图的一般核心原则)。
图形直方图允许选择指数或线性x轴。前者是默认选项。它非常适合标准 schema。(待办:考虑将线性作为非指数 schema 的默认选项。)方便的是,指数 schema 的所有常规桶在指数x轴上具有相同的宽度。这意味着y轴可以显示实际的桶总体数量,不违反上述“条的面积(而非高度)代表桶总体数量”的原则。零值桶是例外。技术上讲,它具有无限宽度。Prometheus 简单地以与常规指数桶相同的宽度渲染它(这反过来意味着x轴在零点附近并非严格指数)。(待办:如何渲染非指数 schema。)
使用线性x轴时,桶通常具有不同的宽度。因此,y轴显示桶总体数量除以其宽度。Prometheus UI 不在y轴上渲染值,因为它们无论如何对人类来说都难以理解。总体数量仍可在文本表示中查看。
在图表视图中,Prometheus 显示热力图(待办:尚未实现,见下文),这可以看作是随时间变化的直方图系列,旋转 90 度,并将桶总体数量编码为颜色,而不是条的高度。将 Counter 样直方图渲染为热力图的典型查询是 rate
查询。热力图是一种极其强大的表示形式,使人类能够轻松发现分布随时间变化的特征。
原生直方图在模板扩展中有效。它们以文本形式呈现,这种表示方式受到数学上开区间和闭区间表示法的启发。(这是由 Go 中的 FloatHistogram.String
方法生成的。)由于原生直方图可以有很多桶,桶边界通常有很多小数位,这种表示不一定非常易读。在模板扩展中谨慎使用原生直方图。
浮点直方图文本表示示例。
{count:3493.3, sum:2.349209324e+06, [-22.62741699796952,-16):1000, [-16,-11.31370849898476):123400, [-4,-2.82842712474619):3, [-2.82842712474619,-2):3.1, [-0.01,0.01]:5.5, (0.35355339059327373,0.5]:1, (1,1.414213562373095]:3.3, (1.414213562373095,2]:4.2, (2,2.82842712474619]:0.1}
remote write & read 的 protobuf 规范已为原生直方图进行了扩展,作为实验性功能。无法处理原生直方图的接收端将直接忽略新添加的字段。尽管如此,必须配置 Prometheus 通过 remote write 发送原生直方图(通过将 send_native_histograms
remote write 配置设置为 true)。
在remote write v2中,原生直方图是一个稳定功能。
在发送或接收经典直方图时,将它们转换为 NHCB 可能看起来很诱人。然而,这并不能克服经典直方图通过 remote write 传输时已知的 consistency 问题。相反,经典直方图应该(SHOULD)在抓取期间转换为 NHCB。类似地,显式 OTel 直方图应该(SHOULD)在OTLP 摄取期间就转换为 NHCB。
原生直方图的联邦功能按预期工作,前提是联邦抓取使用 protobuf 格式。一旦 OpenMetrics 文本格式支持原生直方图,通过该格式进行联邦原则上也是可能的,但无论如何,出于效率考虑,更推荐通过 protobuf 进行联邦。
内置于 Prometheus 的 OTLP 接收器将传入的 OTel 指数直方图转换为 Prometheus 原生直方图,利用上文描述的兼容性。使用 schema(OTel 术语中的“scale”)大于 8 的直方图的分辨率将被降低以匹配 schema 8。(在极不可能使用小于 -4 的 schema 的情况下,摄取将失败。)
显式 OTel 直方图等同于 Prometheus 的经典直方图。因此 Prometheus 默认将它们转换为经典直方图,但可选地提供直接转换为 NHCB 的功能。
Pushgateway 已逐步添加原生直方图支持。在 v1.9 版本达到完全支持。Pushgateway 始终基于经典 protobuf 格式作为其内部数据模型,这使得必要的更改很容易(主要涉及 UI)。可以推送组合直方图(包含经典和原生桶),并以此形式通过 /metrics
端点暴露。(然而,可用于以 JSON 格式查询已推送指标的查询 API 只能返回一种桶,如果存在原生桶,则会优先返回原生桶。)
promtool
本节描述为支持原生直方图而添加或更改的 promtool
命令。未明确提及的命令不直接与原生直方图交互,无需更改。
promtool query ...
命令适用于原生直方图。请参阅查询 API 文档了解输出格式。特别添加了一个新命令 promtool query analyze
,用于分析查询 API 返回的经典和原生直方图使用模式。
通过 promtool test rules
进行的规则单元测试适用于原生直方图,使用上文描述的格式。
promtool tsdb analyze
和 promtool tsdb list
与原生直方图正常工作。前者的 --extended
输出中有针对直方图块的特定部分。
promtool tsdb dump
使用原生直方图的常用文本表示(由 Go 方法 FloatHistogram.String
生成)。
promtool tsdb create-blocks-from rules
处理发出原生直方图的规则。
promtool promql ...
命令支持为原生直方图添加的所有 PromQL 功能。
虽然 promtool tsdb bench write
原则上可以包含原生直方图,但目前没有计划支持。
以下命令依赖于 OpenMetrics 文本格式,因此,只要 OpenMetrics 中没有原生直方图支持,这些命令就无法支持原生直方图
promtool check metrics
promtool push metrics
promtool tsdb dump-openmetrics
promtool tsdb create-blocks-from openmetrics
prom2json
prom2json
是一个小型工具,它抓取 Prometheus /metrics
端点,将指标转换为自定义 JSON 格式,并将其转储到标准输出。这对于使用处理 JSON 的工具(例如 jq
)进行进一步处理很方便。
prom2json
v1.4 添加了对原生直方图的支持。如果暴露的直方图至少包含一个桶 Span,prom2json
将用原生直方图的桶替换 JSON 输出中通常的经典桶,采用一种受Prometheus 查询 API启发的格式。
从经典直方图迁移到原生直方图时,有三个重要的潜在问题来源需要考虑
为了解决 (3),当然可以选择不迁移相关的经典直方图,维持现状。另一种选择是保持 instrumentation 不变,但在摄取时将经典直方图转换为 NHCB。这利用了原生直方图更高的存储性能,但仍需要像完全迁移到原生直方图一样解决 (1) 和 (2) 的问题(见下文段落)。
解决 (1) 和 (2) 的保守方法是允许较长的过渡期,这将以并行收集和存储经典和原生直方图一段时间为代价。
第一步是更新 instrumentation,以便同时暴露经典和原生直方图。(如果计划是在 instrumentation 中坚持使用经典直方图,并在抓取期间简单地将它们转换为 NHCB,则可以跳过此步骤。)
然后配置 Prometheus 同时抓取经典直方图和原生直方图,见上文同时抓取经典和原生直方图部分。(如果需要,还可以激活经典直方图到 NHCB 的转换。)
涉及经典直方图的现有查询将继续有效,但从现在开始,用户可以开始使用原生直方图,并开始更改仪表盘、告警、记录规则中的查询……正如上文所述,注意使用较长范围向量的查询非常重要,例如 histogram_quantile(0.9, rate(rpc_duration_seconds[1d]))
。此查询计算过去一天的第 90 百分位数延迟。然而,如果原生直方图收集时间不足一天,则查询将仅覆盖较短的时间段。因此,此查询只有在原生直方图收集时间至少达到一天后才应使用。对于显示过去一个月每日第 90 百分位数延迟的仪表盘,很自然会想要精心设计一个在适当时候从经典直方图正确切换到原生直方图的查询。虽然原则上可行,但这比较棘手。如果可行,经典直方图和原生直方图并行收集的过渡期可以相当长,以尽量减少实现复杂切换的必要性。例如,一旦经典直方图和原生直方图并行收集了一个月,任何不查看一个月前数据的仪表盘都可以简单地从经典直方图查询切换到原生直方图查询,而无需考虑正确的切换时机。
一旦确信所有查询都已正确迁移,请将 Prometheus 配置为仅抓取原生直方图(这是“正常”设置)。(也可以通过抓取配置中的重新标签规则逐步移除经典直方图。)如果一切仍然正常运行,就可以从检测中移除经典直方图了。
Grafana Mimir 文档包含了一份详细的迁移指南,该指南遵循与本节所述相同的理念。
本文档是开源的。请通过提交 issue 或 pull request 帮助改进它。