原生直方图 [实验性]

原生直方图于 2022 年 11 月作为一项实验性功能引入。它是一个几乎触及 Prometheus 技术栈各个部分的概念。首个支持原生直方图的 Prometheus 服务器版本是 v2.40.0。该支持需要通过功能开关 --enable-feature=native-histograms 启用。(TODO: 当前版本 v2.55 和 v3.00 仍然是这种情况。一旦发布稳定版本,将更新此部分。)

由于与原生直方图相关的变更具有普遍性,这些变更的文档和底层概念的解释广泛分布在各种渠道(如受影响的 Prometheus 组件的文档、源代码中的文档注释、有时是源代码本身、设计文档、会议演讲等)。本文档旨在收集所有这些信息,并以统一的背景简明地呈现它们。本文档倾向于链接现有的详细文档,而不是重述它,但它包含了足够的信息,无需参考其他来源即可理解。话虽如此,需要注意的是,本文档既不适合作为初学者的入门介绍,也不专注于开发人员的需求。对于前者,计划是提供更新版本的关于直方图和摘要的最佳实践文章。(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*)。这些浮点数可以直接表示*仪表盘 (gauges)*或*计数器 (counters)*。Prometheus 的指标类型*摘要 (summary)*和(经典版本的)*直方图 (histogram)*,在暴露格式中存在,在摄取时被分解为浮点组件:两种类型都有一个*总和 (sum)*和一个*计数 (count)*组件,摘要有若干*分位数 (quantile)*样本,而(经典)直方图有若干*桶 (bucket)*样本。

随着原生直方图的引入,一种新的结构化样本类型被引入。单个样本代表了先前已知的*总和 (sum)*和*计数 (count)*,外加一组动态的桶。这不仅限于摄取,PromQL 表达式现在也可以返回新的样本类型,而以前只能返回浮点样本。

原生直方图具有以下关键属性:

  1. 稀疏桶表示,使得空桶的成本(几乎)为零。
  2. 覆盖完整的 float64 值范围。
  3. 在仪表化期间无需配置桶边界。
  4. 根据简单的配置参数选择动态分辨率。
  5. 复杂的指数分桶方案,确保使用这些方案的所有直方图之间都具有可合并性。
  6. 为暴露和存储提供高效的数据表示。

这些关键属性通过标准分桶方案得以完全实现。还有其他具有不同权衡的方案,可能只具备这些属性的一个子集。详见下文的方案部分

与之前存在的“经典”直方图相比,原生直方图(使用标准分桶方案)可以在任意观测值范围内提供更高的桶分辨率,同时存储和查询成本更低,并且几乎不需要配置。现在,即使按标签对直方图进行分区也变得更加经济实惠。

由于稀疏表示(上文列表中的属性1)对于原生直方图的许多其他好处至关重要,因此在设计过程的早期,*稀疏直方图*是*原生直方图*的常用名称。然而,其他关键属性如指数分桶方案或桶的动态性也非常重要,但*稀疏直方图*这个术语完全没有体现这些特性。

设计文档

这些是指导原生直方图开发的设计文档。一些细节现在可能已经过时,但它们很好地描述了底层概念及其演变过程。

会议演讲

学习原生直方图的一个更平易近人的方式是观看会议演讲,下面精选了一些。作为入门,观看这些演讲,然后返回本文档来学习所有细节和技术要点可能是一个好方法。

术语表

  • 一个**原生直方图**是本文档所讨论的新复杂样本类型的实例,它代表一个完整的直方图。在上下文足够清晰的情况下,下文通常简称为*直方图*。
  • 一个**经典直方图**是旧样本类型的实例,它代表一个具有固定桶的直方图,以前简称为*直方图*。它以这种形式存在于暴露格式中,但在摄取到 Prometheus 时被分解为多个浮点样本。
  • **稀疏直方图**是*原生直方图*的一个较早的、现已弃用的名称。这个名称可能偶尔还会在旧文档中找到。**稀疏桶**仍然是一个有意义的术语,用于描述原生直方图的桶。

数据模型

本节描述了原生直方图的通用数据模型。它尽可能避免了具体的实现细节。这包括术语。例如,本节中描述的*列表 (list)*在 protobuf 实现中将成为一个*重复消息 (repeated message)*,在 Go 实现中(很可能)是一个*切片 (slice)*。

通用结构

与经典直方图类似,原生直方图有一个用于观测*计数 (count)*的字段和一个用于观测*总和 (sum)*的字段。虽然观测计数通常是非负的(唯一的例外是 PromQL 中的中间结果),但观测总和可以是任何 float64 值。

此外,原生直方图还包含以下组件,这些组件将在下面的专门章节中详细描述:

  • 一个*方案 (schema)*,用于识别确定任何给定索引为 *i* 的桶边界的方法。
  • 一个用于正观测值和负观测值的镜像索引桶的稀疏表示。
  • 一个*零桶 (zero bucket)*,用于计数接近零的观测值。
  • 一个(可能为空的)*自定义值 (custom values)*列表。
  • Exemplars(样本范例).

风格

任何原生直方图在两个独立的维度上都有特定的风格:

  1. 计数器 vs. 仪表盘:通常,直方图是“类计数器”的,即它的每个桶都充当观测的计数器。然而,也存在“类仪表盘”的直方图,其中每个桶都是一个仪表盘,表示某个时间点的任意分布。仪表盘直方图的概念先前已由 OpenMetrics为经典直方图引入。
  2. 整数 vs. 浮点数(简称:float):直方图的明显用途是计数观测值,导致每个桶内的观测数 ≥ 0,包括*零桶 (zero bucket)*和总*观测计数 (count)*,都表示为无符号 64 位整数(简称:uint64)。然而,有些特定用例会导致“加权”或“缩放”的直方图,其中所有这些值都表示为 64 位浮点数(简称:float64)。请注意,在任何情况下,观测值的*总和 (sum)*都是 float64。

浮点直方图偶尔用于直接仪表化中的“加权”观测,例如,计算观测值落入直方图不同桶中的秒数。然而,浮点直方图更常见的用例是在 PromQL 中。PromQL 通常只对浮点值进行操作,因此 PromQL 引擎会首先将从 TSDB 检索到的每个直方图转换为浮点直方图,并且通过记录规则存回 TSDB 的任何直方图都是浮点直方图。如果这样的直方图实际上是整数直方图(因为所有非*总和 (sum)*字段的值都可以精确地表示为 uint64),TSDB 实现 MAY 将其转换回整数直方图以提高存储效率。(截至 Prometheus v3.00,Prometheus 内部的 TSDB 实现没有利用这个选项。)但请注意,应用于计数器直方图的最常见的 PromQL 函数是 rate,它通常会产生非整数,因此记录规则的结果通常会是非整数值的浮点直方图。

PromQL 表达式甚至可以创建“负”直方图(例如,通过将直方图乘以 -1)。这些负直方图只允许作为中间结果,否则被视为无效。它们不能在任何交换格式(暴露格式、远程写入、OTLP)中表示,也不能存储在 TSDB 中。另请参阅关于负直方图的详细章节

将原生直方图明确地视为整数直方图与浮点直方图,这与传统简单数值样本的处理方式有显著不同,后者为了简单起见,在整个技术栈中始终被视为浮点数。

对直方图进行更复杂处理的主要原因是在基于 protobuf 的暴露格式中容易获得效率提升。Protobuf 对整数使用 varint 编码,这在不需要额外压缩层的情况下减小了小整数值的数据大小。这个好处通过整数桶的增量编码得到放大,通常会产生更小的整数值。相比之下,浮点数在 protobuf 中总是需要 8 个字节。实际上,整数直方图中的许多整数将适合 1 个字节,大多数将适合 2 个字节,因此在 protobuf 暴露格式中明确存在整数直方图,直接导致数据大小减少接近 8 倍,特别是对于具有许多桶的直方图。这一点尤为重要,因为被仪表化目标暴露的绝大多数直方图都是整数直方图。

出于类似的原因,整数直方图在内存和磁盘上的表示通常比浮点直方图更高效。不过,这一点没有在暴露格式中的好处那么重要。一方面,Prometheus 对浮点数使用 Gorilla 风格的 XOR 编码,这减小了它们的大小,尽管不如用于整数的双增量编码那么显著。更重要的是,实现总是可以决定内部对实际上是整数值的直方图字段使用整数表示(见上文)。(历史注记:Prometheus v1 正是使用这种方法来改进浮点样本的压缩,而 Prometheus v3 很可能在未来再次采用这种方法。)

在计数器直方图中,总*观测计数 (count)*和各个桶中的计数行为类似于 Prometheus 计数器,即它们只在计数器重置时才会减少。然而,由于观测到负值,观测的*总和 (sum)*可能会减少。PromQL 实现必须基于整个直方图来检测计数器重置(详见下文的计数器重置注意事项部分)。(请注意,这对于经典直方图和摘要的*总和 (sum)*组件也一直是个问题。到目前为止的做法是接受在这些情况下,计数器重置检测对*总和 (sum)*会静默失效。幸运的是,对于 Prometheus 直方图和摘要,负观测是一个非常罕见的用例。)

方案 (Schema)

*方案 (schema)*是一个大小为 8 位的有符号整数值(简称:int8)。它定义了计算桶边界的方式。当前有效的值是 -53 以及从 -4 到 +8(含两端)的范围(还有一个更大的范围,从 -9 到 +52(含两端)被保留,详见下文)。未来可能会添加更多的方案。-53 是一个用于所谓的*自定义桶边界 (custom bucket boundaries)*或简称*自定义桶 (custom buckets)*的方案,而其他方案编号代表不同的标准指数方案(简称:*标准方案 (standard schemas)*)。

标准方案之间可以相互合并,推荐用于一般用例。较大的方案编号对应较高的分辨率。方案 *n* 的分辨率是方案 *n*+1 的一半,这意味着一个方案为 *n*+1 的直方图可以通过合并相邻的桶转换为一个方案为 *n* 的直方图。

对于任何标准方案 *n*,索引为 *i* 的桶的边界计算如下(使用 Python 语法):

  • 正值桶的上限(包含):(2**2**-n)**i
  • 正值桶的下限(不包含):(2**2**-n)**(i-1)
  • 负值桶的下限(包含):-((2**2**-n)**i)
  • 负值桶的上限(不包含):-((2**2**-n)**(i-1))

*i* 是一个可以为负的整数。

对于可表示为 float64 的最大和最小有限值(下文称为 MaxFloat64MinFloat64)以及正负无穷大值(+Inf-Inf),上述规则存在例外情况:

  • 包含 MaxFloat64 的正值桶(根据上述边界公式)其上限(包含)为 MaxFloat64(而不是按上述公式计算的会溢出 float64 的值)。
  • 下一个正值桶(相对于前一项的索引为 *i*+1)的下限(不包含)为 MaxFloat64,上限(包含)为 +Inf。(可以称之为*正溢出桶 (positive overflow bucket)*。)
  • 包含 MinFloat64 的负值桶(根据上述边界公式)其下限(包含)为 MinFloat64(而不是按上述公式计算的会下溢 float64 的值)。
  • 下一个负值桶(相对于前一项的索引为 *i*+1)的上限(不包含)为 MinFloat64,下限(包含)为 -Inf。(可以称之为*负溢出桶 (negative overflow bucket)*。)
  • 超出上述 +Inf-Inf 桶的桶不得 (MUST NOT) 使用。

对于接近零的值还有更多例外情况,详见下文的零桶部分

当前最低分辨率 -4 和最高分辨率 8 的限制是基于实际用途选择的。如果出现对更低或更高分辨率的实际需求,将考虑扩大范围。然而,大于 52 的方案没有意义,因为从一个桶到下一个桶的增长因子将小于可表示的 float64 数之间的差值。同样,小于 -9 的方案也没有意义,因为增长因子将超过可表示为 float64 的最大浮点数。因此,介于 -9 和 +52(含两端)之间的方案编号被保留用于未来的标准方案(遵循上述桶边界公式),并且不得 (MUST NOT) 用于任何其他方案。

原生直方图的接收方可以 (MAY) 在摄取时通过适当合并桶来降低摄取直方图的方案,从而降低其分辨率。如果接收方在摄取时遵循上述桶边界公式将方案降低到有效数字(即 -4 到 8 之间),则可以 (MAY) 接受 9 到 52 之间的方案。

如果在可选的方案转换后,方案对接收方仍然未知,则有以下选项:

  • 如果一次抓取(包括联邦)包含一个或多个具有未知方案的直方图,则整个抓取必须 (MUST) 失败,遵循 Prometheus 避免不完整抓取的实践。
  • 对于任何其他摄取路径(包括重放 WAL/WBL),接收方可以 (MAY) 忽略具有未知方案的直方图,并应该 (SHOULD) 以适当的方式通知用户此遗漏。

当 TSDB 实现从其永久存储中读取直方图时(不包括重放 WAL/WBL),类似的指导原则适用:9 到 52 之间的方案可以 (MAY) 转换为有效方案。否则,未知方案在检索时必须 (MUST) 返回错误,并且触发检索的 PromQL 查询必须 (MUST) 失败。

对于 schema -53,分桶边界通过自定义值显式设置,具体细节在下文的自定义值部分描述。这将产生一个具有自定义分桶边界的原生直方图(或简称自定义分桶,通常进一步缩写为 NHCB)。这种直方图可用于将经典直方图表示为原生直方图。如果标准 schema 提供的指数分桶不适合表示直方图的分布,也可以使用它。具有不同自定义分桶边界的直方图通常不能相互合并。因此,只有在特定用例中经过深思熟虑后,才应(SHOULD)使用 schema -53。

分桶

对于标准 schema,分桶表示为两个列表,一个用于正值分桶,一个用于负值分桶。对于自定义分桶(schema -53),只使用正值分桶列表,但它被重新用于所有分桶。

任何未填充的分桶都可(MAY)从列表中排除。(这就是为什么这些分桶通常被称为稀疏分桶的原因。)

对于浮点直方图,列表中的元素是 float64,直接表示分桶的计数值。分桶计数值通常为非负数,唯一的例外是 PromQL 中的中间结果

对于整数直方图,列表中的元素是有符号 64 位整数(简称:int64),每个元素表示该分桶计数值相对于列表中前一个分桶的增量。每个列表中的第一个分桶包含绝对计数值(也可以看作是相对于零的增量)。这些增量必须(MUST NOT)导致负的绝对分桶计数值。

为了将列表中的分桶映射到上一节中定义的索引,有两个所谓的跨度(spans)列表,一个用于正值分桶,一个用于负值分桶。

每个跨度由一对数字组成:一个有符号 32 位整数(简称:int32),称为偏移量(offset);一个无符号 32 位整数(简称:uint32),称为长度(length)。每个列表中只有第一个跨度可以有负的偏移量。它定义了其对应分桶列表中第一个分桶的索引。(请注意,对于 NHCB,索引始终为正数,详情请参见下文的自定义值部分。)长度定义了分桶列表开头的连续分桶数量。后续跨度的偏移量定义了被排除(因此未填充)的分桶数量。长度定义了被排除分桶之后列表中的连续分桶数量。

每个跨度列表中所有长度值的总和必须(MUST)等于相应分桶列表的长度。

空跨度(长度为零)是有效的,并且可(MAY)被使用,尽管它们通常没什么用,应(SHOULD)通过将其偏移量加到下一个跨度的偏移量上来消除它们。同样,列表中非第一个跨度的偏移量可(MAY)为零,尽管这些偏移量应(SHOULD)通过将其长度加到前一个跨度上来消除。这两种情况都是允许的,以便原生直方图的生产者可(MAY)在任何时候选择资源权衡最佳的表示方式。例如,如果一个直方图经过多个处理阶段,可能在最后一个处理阶段之后才消除冗余的跨度会最高效。

本着类似的精神,在某些情况下,将每个未填充的分桶从分桶列表中排除是最高效的,但在其他情况下,通过显式表示少量未填充的分桶来减少跨度数量可能更好。

请注意,未来的高分辨率 schema 可能需要大于 int32 所能表示的偏移量。在这种情况下,将需要对数据模型进行扩展。(当前分辨率最高的标准 schema 是 schema 8,其中包含 MaxFloat64 的分桶索引为 262144,因此 +Inf 溢出桶的索引为 262145,而 int32 可表示的最大数字是 2147483647。仍然可以使用 int32 偏移量的最高标准 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 的单个未填充分桶,第二个和第三个跨度可以合并为一个,得到以下结果

  • 正值分桶列表:[3, 2, -4, -1, 3, -1]
  • 正值跨度列表:[[-2, 2], [2,4]]

或者通过显式表示上面所有未填充的分桶来将所有跨度合并为一个

  • 正值分桶列表:[3, 2, -5, 0, 1, -1, 3, -1]
  • 正值跨度列表:[[-2, 8]]

零值桶

根据上述标准 schema 的定义,观测值为精确零的数据不适合放入任何分桶。它们被计入一个专用的分桶,称为零值桶(zero bucket)

零值桶中的观测值数量由一个 uint64(对于整数直方图)或 float64(对于浮点直方图)来跟踪。与常规分桶一样,这个数字通常是非负的。

零值桶有一个额外的参数,称为零值阈值(zero threshold),它是一个 float64 ≥ 0。如果阈值设置为零,则只有精确为零的观测值会进入零值桶,这正是上面描述的情况。如果阈值是一个正数,则所有在闭区间 [-threshold, +threshold] 内的观测值都会进入零值桶,而不是常规分桶。这有两个用例

  • 接近零的噪声观测值往往会填充大量分桶。这些观测值可能是由于数值不精确或观测源是实际物理测量而发生的。带有相对较小阈值的零值桶会将这些观测值重定向到一个单一的分桶中。
  • 如果用户更关心远离零值的分布长尾,那么零值桶相对较大的阈值有助于避免为不感兴趣的范围创建许多高分辨率分桶。

零值桶的阈值应(SHOULD)与常规分桶的边界重合,以避免零值桶与常规分桶部分重叠的复杂情况。但是,如果发生这种重叠,计入与零值桶重叠的常规分桶的观测值必须(MUST)在 [-threshold, +threshold] 区间之外。

要合并具有相同零值阈值的直方图,只需将两个零值桶相加即可。然而,如果源直方图中的零值阈值不同,则选择所有源直方图中最大的阈值。如果该阈值恰好落在其他源直方图的任何已填充分桶内,则增加该阈值,直到对于每个源直方图,以下条件之一为真

  • 新阈值与一个已填充分桶的边界重合。
  • 新阈值不在任何已填充分桶之内。

然后将源零值桶和现在位于新阈值内的任何源分桶相加,得出新零值桶的计数值。

如果 schema 是 -53(自定义分桶),则不使用零值桶。

自定义值

对于标准 schema,自定义值列表是未使用的。它由非标准 schema 以自定义方式使用,以备需要存储额外数据时使用。

目前唯一定义的使用自定义值的 schema 是 -53(自定义分桶)。本节的其余部分将更详细地描述在这种特定情况下自定义值的用法。

自定义值表示自定义分桶的上包含边界。它们按升序排序。自定义分桶本身使用正值分桶列表和正值跨度列表来存储,尽管它们的边界(通过自定义值确定)可以是负数。这些“正值”分桶中每个分桶的索引定义了其上边界在自定义值列表中的从零开始的位置。

下排他边界由上边界之前的自定义值定义。对于第一个自定义值(在列表中的位置零),没有前一个值,这种情况下下边界被认为是 -Inf。因此,索引为零的自定义分桶统计所有在 -Inf 和第一个自定义值之间的观测值。在通常只期望正观测值的情况下,索引为零的自定义分桶的上边界应(SHOULD)为零,以明确标记是否有任何等于或小于零的观测值。(如果确实只有正观测值,索引为零的自定义分桶将保持未填充状态,因此永远不会被显式表示。唯一的代价是在自定义值列表的开头增加一个零元素。)

最后一个自定义值必须不(MUST NOT)是 +Inf。大于最后一个自定义值的观测值会进入一个上边界为 +Inf 的溢出桶。这个溢出桶的索引等于自定义值列表的长度。

Exemplars(范例)

一个原生直方图样本可以有零个、一个或多个典范(exemplar)。它们的工作方式与传统典范相同,但它们被组织在一个列表中(因为可以有多个),并且它们必须(MUST)有时间戳。

作为经典直方图一部分暴露的典范,如果带有时间戳,则可(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 的观测值会增加索引为零的分桶的计数值。

OpenTelemetry 互操作性

具有标准 schema 的 Prometheus(Prom)原生直方图可以轻松地映射到 OpenTelemetry(OTel)指数直方图,反之亦然,具体如下所述。

Prom 的 schema 等同于 OTel 中的 scale,但 OTel 允许比 -4 更低和比 +8 更高的值。如上所述,Prom 保留了更多的 schema 编号,以便在实践中需要时扩展其范围。

索引偏移量为 1,即 Prom 索引为 n 的分桶在 OTel 中的索引为 n-1

OTel 使用密集而非稀疏的分桶表示法。可以认为 OTel 是“只有一个跨度的 Prom”。

Prom 的零值桶(zero bucket)在 OTel 中称为零值计数(zero count)。(Prom 也使用 zero count 来命名存储零值桶中观测计数的字段)。两者的工作方式相同,包括存在零值阈值(zero threshold)。请注意,如果 OTel 未给出阈值,则默认为零。

(待办事项:OTel 规范写道:“当 zero_threshold 未设置或为 0 时,此分桶存储无法使用标准指数公式表示的值以及已四舍五入为零的值。” 需要仔细检查这是否真的会产生相同的行为。如果接近零时存在问题,我们可以使 Prom 的规范更精确。如果 OTel 在零值桶中统计 NaN,我们需要在此处添加说明。)

OTel 指数直方图仅支持标准指数分桶 schema(顾名思义)。因此,NHCBs(或具有其他未来分桶 schema 的原生直方图)不能干净地转换为 OTel 指数直方图。但是,仍然可以转换为具有固定分桶的传统 OTel 直方图。

任何类型的 OTel 直方图都有可选字段,用于记录直方图中观测到的最小值和最大值。这些字段在 Prometheus 中没有等效概念,因为计数器直方图在漫长且不可预测的时间跨度内累积数据,并且可以随时被抓取,因此跟踪最小值和最大值要么不可行,要么用处有限。但请注意,原生直方图能够在任意时间跨度内相当准确地估计最大和最小观测值,请参见 PromQL 部分

暴露格式

在经典的 Prometheus 用例中,指标暴露主要由字符串主导,因为所有的指标名称、标签名称和标签值比 float64 样本值占用更多的空间,即使后者以可能更冗长的文本形式表示。这是过去放弃基于 protobuf 的暴露格式似乎有利的原因之一。

相比之下,一个原生直方图,遵循上述数据模型,包含更多的数值数据。这放大了基于 protobuf 的格式的优势。因此,先前被放弃的基于 protobuf 的暴露格式被重新启用,以高效地暴露和抓取原生直方图。

经典 Prometheus 格式

在原生直方图构思之时,OpenMetrics 的采用仍然不足,特别是 protobuf 版本的 OpenMetrics 没有任何已知的应用。因此,最初的方法是扩展经典的 Prometheus protobuf 格式以支持原生直方图。(一个额外的实际考虑是,Go 插桩库 仍然使用经典的 protobuf 规范作为其内部数据模型,这简化了最初的开发。)

经典的 Prometheus 文本格式没有为原生直方图进行扩展,也没有计划进行这样的扩展。(另请参阅下面的 OpenMetrics 部分。)

protobuf 规范有 proto2 和 proto3 两个版本,它们都创建相同的线路格式

这些文件有全面的注释,应能轻松地将 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.
}

// [...]

请注意以下几点

  • 原生直方图和经典直方图都由同一个 Histogram proto 消息编码,即现有的 Histogram 消息被扩展以包含原生直方图的字段。
  • 观测值总和与总计数的字段以及 created_timestamp 在经典直方图和原生直方图之间共享,并且对两者都以相同的方式工作。
  • 格式最初不支持经典的浮点直方图。在为原生直方图扩展格式时,作为副产品添加了对经典浮点直方图的支持(参见字段 sample_count_float, cumulative_count_float)。
  • Bucket 字段和 Bucket 消息用于经典直方图的分桶。完全可以创建一个同时表示同一 histograms 的经典版本和原生版本的 Histogram 消息。解析器可以自由选择其中一个或两个版本(另请参阅 抓取配置部分)。
  • 在浮点直方图的情况下,分桶计数值被编码为绝对数;在整数直方图的情况下,则编码为与前一个分桶(或第一个分桶的零)的增量。后者产生更小的数字,由于 protobuf 对 sint64 类型使用 varint 编码,因此编码后的消息尺寸更小。
  • 一个尚未收到任何观测值的原生直方图和一个没有配置分桶的经典直方图,作为 protobuf 消息看起来完全相同。因此,一个旨在被解析为原生直方图的 Histogram 消息必须(MUST)在重复的 positive_span 字段中包含一个“空操作跨度”,即一个 offsetlength 都设置为 0 的 BucketSpan
  • 原生直方图的任意数量的典范(exemplar)可(MAY)被添加到 Histogram 消息的重复 Exemplar 字段中,但每一个都必须(MUST)有时间戳。如果没有以这种方式提供典范,解析器可(MAY)使用为经典分桶提供的时间戳典范(即在 Bucket 消息的 Exemplar 字段中每个分桶最多一个典范)。
  • 原生直方图典范的数量和分布应(SHOULD)适合手头的用例。通常,典范的有效负载不应(SHOULD NOT)比 Histogram 消息的其余部分大得多,并且典范应(SHOULD)落入不同的分桶,并大致均匀地覆盖整个分桶范围。(这通常比按比例代表观测值分布的典范分布更可取,因为后者很少能产生分布长尾的典范,而这些往往是最值得关注的典范。)
  • 没有用于表示 NHCB 所需自定义值的字段。NHCB 从不直接暴露,而是作为经典直方图呈现,在采集时转换(回)为 NHCB。这也适用于联邦(federation)。如果未来出现需求,例如未来的 schema 也使用自定义值,我们可能仍会添加自定义值的字段。

OpenMetrics

目前(2024-11-03),OpenMetrics 不支持原生直方图。

由于 OpenMetrics 的 protobuf 版本与经典的 Prometheus protobuf 格式相似,为其添加支持相对直接。一个以 PR 形式提出的提案正在审查中。

为 OpenMetrics 的文本版本添加支持更加困难,但也非常可取,因为在许多情况下生成 protobuf 是不可行的。文本格式必须在人类可读性和机器高效处理(编码、传输、解码)之间做出权衡。相关工作正在进行中。更多详情请参见设计文档

(待办事项:随着进展更新本节。)

插桩库

protobuf 规范允许使用 protobuf 编译器生成的特定语言绑定来低级创建包括原生直方图在内的指标暴露。然而,对于直接的代码插桩,需要一个插桩库。

目前(2024-11-03),有两个官方的 Prometheus 插桩库支持原生直方图

为其他插桩库添加原生直方图支持相对容易,如果该库已经支持 protobuf 暴露。对于纯文本库,完成基于文本的暴露格式是先决条件。(待办事项:根据需要更新此部分。)

本节不涵盖如何使用个别插桩库的细节(请参阅上面链接的文档),而是侧重于常见的使用模式,并提供关于如何在插桩库中实现原生直方图支持的一般性指导。现有的 Go 实现被用作示例。关于数据模型暴露格式的部分对于插桩库的实现非常相关(但本节不再重述!)。

实际的直方图插桩 API 对于原生直方图没有变化。经典直方图和原生直方图都以相同的方式接收观测值(在典范方面有细微差异,见下一段)。插桩库甚至可以维护同一 histograms 的经典版本和原生版本,并并行暴露它们,以便抓取器可以选择要采集哪个版本(详情请参阅关于暴露格式的部分)。用户通过配置设置选择是暴露经典直方图和/或原生直方图。

经典直方图的典范(exemplar)通常通过存储和暴露每个分桶最近的典范来跟踪。只要定义了经典分桶,插桩库可(MAY)为同一 histograms 的原生版本暴露相同的典范,只要每个典范都有时间戳。(事实上,抓取器可(MAY)使用随 histograms 经典版本提供的典范,即使它只采集原生版本,详情请参阅暴露格式部分。)然而,一个原生直方图可(MAY)被分配任意数量的典范,插桩库应(SHOULD)利用这种自由度来满足暴露格式部分描述的典范最佳实践。

插桩库应(SHOULD)为遵循标准 schema 的原生直方图提供以下配置参数。名称是 Go 库中的示例——它们需要调整为其他语言的惯用风格。括号中的值是库应(SHOULD)提供的默认值。

  • NativeHistogramBucketFactor (1.1): 一个大于 1 的浮点数,用于确定初始分辨率。库会选择一个起始 schema,使得分桶宽度从一个分桶到下一个分桶的增长因子不大于所提供的值。示例值见下表。
  • NativeHistogramZeroThreshold (2-128): 一个值为零或更大的浮点数,用于设置零值桶的初始阈值。

分辨率通过增长因子来设置,而不是直接提供 schema,因为大多数用户不了解 schema 数字背后的数学原理。关于分桶间增长因子上限的概念在不知道原生直方图内部工作原理的情况下也是可以理解的。下表列出了每个有效 schema 的示例因子。

NativeHistogramBucketFactor生成的 schema
65536-4
256-3
16-2
4-1
20
1.51
1.22
1.13
1.054
1.035
1.026
1.017
1.0058

限制分桶数量

原生直方图的分桶在首次填充时动态创建。观测值的分布意外地广泛可能导致分桶数量超出预期,需要比预期更多的内存。如果观测值的分布可以从外部操纵,这甚至可能被用作一种 DoS 攻击向量,通过耗尽程序所有可用内存。因此,插桩库应(SHOULD)提供一种分桶限制策略。它可(MAY)默认设置一种,具体取决于库的典型用例。(待办事项:也许我们应该说默认应(SHOULD)设置一种策略。Go 库目前默认不限制分桶,到目前为止还没有报告任何问题。)

以下描述了 Go 插桩库实现的分桶限制策略。其他库可(MAY)遵循此示例,但其他策略也可能可行,具体取决于库的典型使用模式。

该策略由三个参数定义:一个无符号整数 NativeHistogramMaxBucketNumber、一个持续时间 NativeHistogramMinResetDuration 和一个浮点数 NativeHistogramMaxZeroThreshold。如果 NativeHistogramMaxBucketNumber 为零(这是默认值),则完全不限制分桶,其他两个参数将被忽略。如果 NativeHistogramMaxBucketNumber 设置为正值,库会尝试将每个直方图的分桶数保持在所提供的值。一个典型的限制值是 160,这也是 OTel 指数直方图在类似策略中使用的默认值。(请注意,按标签分区会创建多个直方图。该限制分别适用于每个直方图,而不是总和。)如果超出限制,将按顺序应用多种补救措施,直到分桶数量再次在限制范围内

  1. 如果自上次重置直方图(包括创建直方图)以来至少过去了 NativeHistogramMinResetDuration 的时间,则整个直方图将被重置,即所有分桶被删除,观测值的总和和计数以及零值桶都设置为零。Prometheus 将此视为正常的计数器重置,这意味着在两次抓取之间会丢失一些观测值,因此重置的频率应远低于抓取间隔。此外,频繁的计数器重置可能导致 TSDB 中的存储效率降低(详情请参阅 TSDB 部分)。NativeHistogramMinResetDuration 为一小时是在大多数情况下应该能很好工作的值。
  2. 如果自上次重置以来时间不足(或者如果 NativeHistogramMinResetDuration 设置为零,这是默认值),则不执行重置。相反,会增加零值阈值,将接近零的分桶合并到零值桶中,从而减少分桶数量。阈值的增加受 NativeHistogramMaxZeroThreshold 的限制。如果已达到此值(或设置为零,这是默认值),则此步骤不执行任何操作。
  3. 如果分桶数量仍然超过限制,则通过将其转换为下一个较低的 schema 来降低直方图的分辨率,即通过合并相邻分桶,从而使分桶宽度加倍。重复此操作,直到分桶计数在配置的限制内或达到 schema -4。

如果步骤 2 或 3 改变了直方图,一旦自上次重置以来过去了 NativeHistogramMinResetDuration 的时间,就会执行重置,不仅是为了移除分桶,也是为了恢复零值阈值和分桶分辨率的初始值。请注意,这在所有方面都被视为因其他原因而进行的重置,包括更新所谓的创建时间戳

NativeHistogramBucketFactor 设置得非常低(例如 1.005)并配合一个合理的 NativeHistogramMaxBucketNumber(例如 160)是很有诱惑力的。这样,每个直方图总是在给定的分桶数“预算”内拥有尽可能高的分辨率。(这是 OTel 指数直方图使用的默认策略。它甚至从一个更高的 schema (20) 开始,这在 Prometheus 原生直方图中目前甚至不可用。)然而,这种策略通常推荐用于 Prometheus 的用例。在创建后和每次重置后,随着观测值的进入,分辨率会相当频繁地降低。这会在插桩程序和 TSDB 中都产生抖动,后者尤其成问题。所有这些努力大多是徒劳的,因为典型的涉及直方图的查询需要合并许多直方图,在此期间会使用最低的公共分辨率,因此用户最终得到的分辨率 comunque 较低。TSDB 可以通过在采集时限制分辨率来防止抖动(见下文),但如果采集时 comunque 会强制执行一个合理低的分辨率,那么在插桩时就已经设置好这个分辨率会更直接。然而,在某些特定情况下,如果在插桩时无法假设一个合理的分辨率,并且抓取器应该有在抓取时选择所需分辨率的灵活性,这种策略可能值得在插桩程序内部消耗资源。

按标签分区

虽然对一个具有许多分桶的经典直方图按标签进行分区必须谨慎进行,但原生直方图的情况则更为宽松。对原生直方图进行分区仍然会创建多个独立的直方图。然而,由此产生的分区直方图各自填充的分桶数通常会比原始未分区直方图少。(例如,如果一个跟踪 HTTP 请求持续时间的直方图按 HTTP 状态码分区,那么跟踪状态码为 404 的请求的单个直方图可能会在识别未知路径所需典型持续时间周围有一个非常集中的分桶分布,只填充少数几个分桶。)所有分区直方图的总填充分桶数仍会增加,但其增加的因子小于分区直方图的数量。(例如,如果为一个已经相当重的经典直方图添加标签导致了 100 个带标签的直方图,总成本将增加 100 倍。而对于原生直方图,单个直方图的成本可能已经更低,如果经典直方图具有高分辨率的话。分区后,带标签的原生直方图的总填充分桶数将显著小于原始原生直方图分桶数的 100 倍。)

NHCB

目前(2024-11-03),插桩库没有提供直接配置具有自定义分桶边界的原生直方图(NHCBs)的方法。NHCBs 的用例是允许启用原生直方图的抓取器在采集时将经典直方图转换为 NHCBs(见下一节)。然而,在某些情况下,直接在插桩期间使用自定义分桶是合理的。在这些情况下,目前的方法是用经典直方图进行插桩,并配置抓取器在采集时将其转换为 NHCB。不过,未来可能会在插桩库中对 NHCBs 进行更直接的处理。

抓取配置

要使 Prometheus 服务器能够抓取原生直方图,需要使用功能标志 --enable-feature=native-histograms。此标志还会改变内容协商,使其优先选择经典的基于 protobuf 的暴露格式,而不是 OpenMetrics 文本格式。(待办事项:一旦原生直方图成为稳定功能,此行为将会改变。)

微调内容协商

在 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 协商。

(待办事项:一旦原生直方图成为稳定功能或原生直方图被其他格式支持,就更新本节。)

注意在基于文本和基于 protobuf 的暴露格式之间切换有一些不明显的含义。最重要的是,某些实现细节导致了反直觉的效果,即使用基于文本的格式抓取通常比使用基于 protobuf 的格式抓取资源消耗要少得多(详情请参见跟踪问题)。更微妙的是对 quantile 标签(用于摘要)和 le 标签(用于经典直方图)的标签值格式化的影响。这个问题只影响 Prometheus 服务器的 v2 版本(v3 在所有情况下都有一致的格式),并且与原生直方图没有直接关系,但可能会在相同的上下文中出现,因为启用原生直方图需要 protobuf 暴露格式。详情请参见 v2.55 的 native-histograms 功能标志文档

限制分桶数量和分辨率

虽然插桩库应(SHOULD)提供配置选项来限制原生直方图的分辨率和分桶数,但仍然需要在采集时强制执行这些限制。用户可能无法更改给定程序的插桩,或者程序可能故意用高分辨率直方图进行插桩,以便不同的抓取器可以根据需要降低分辨率。

Prometheus 抓取配置提供了两个设置来解决这个需求

  1. native_histogram_bucket_limit 为单个直方图中的分桶数量设置一个上包含限制。如果超过该限制,具有标准 schema 的直方图的分辨率将反复降低(通过将分桶宽度加倍,即减小 schema),直到达到限制。如果 NHCB 超过限制,或者在极少数情况下即使 schema 为 -4 也无法满足限制,则抓取失败。
  2. native_histogram_min_bucket_factor 为分桶间的增长因子设置一个下包含限制。此设置仅与标准 schema 相关,对 NHCB 无效。同样,如果超过该限制,直方图的分辨率将反复降低(通过将分桶宽度加倍,即减小 schema),直到达到限制。然而,一旦达到 schema -4,即使指定了更高的增长因子,抓取仍将成功。

两个设置都接受零作为有效值,表示“无限制”。对于分桶限制,这意味着分桶数量确实完全不被检查。对于分桶因子,Prometheus 仍将确保标准 schema 不会超过所用存储后端的能力。Prometheus 目前存储的标准指数 schema 直方图最高为 8。但是,它接受大于 8 直到保留上限 52 的指数 schema,但在采集时会降低其分辨率,以达到 schema 8(或者如果 native_histogram_bucket_limitnative_histogram_min_bucket_factor 设置要求,则会更低)。

如果两个设置都具有非零值,则 schema 会充分降低以同时满足两个限制。

请注意,在插桩期间设置的分桶因子是上限(暴露的分桶增长因子 ≤ 配置值),而在抓取配置中设置的分桶因子是下限(采集的分桶增长因子 ≥ 配置值)。因此,由某些限制产生的 schema 略有不同。一些示例

native_histogram_min_bucket_factor产生的最大 schema
65536-4
256-3
16-2
4-1
20
1.41
1.12
1.093
1.044
1.025
1.016
1.0057
1.0028

关于设置限制的一般考虑: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 在抓取期间将完全忽略原生直方图部分。(待办事项:一旦该功能标志变为无操作,则更新。)设置该标志后,即使同一 histograms 同时暴露了两者,Prometheus 也将优先选择原生直方图部分而非经典直方图部分。对于没有原生直方图数据的 histograms,Prometheus 仍将抓取经典直方图部分。

在像迁移场景这样的情况下,可能希望为同一 histograms 同时抓取经典和原生两个版本,前提是插桩程序暴露了这两个版本。为了启用此行为,抓取配置中有一个布尔设置 always_scrape_classic_histograms。它默认为 false,但如果设置为 true,只要至少有一个经典分桶和一个原生分桶跨度(可能是一个空操作跨度),每个 histograms 的两个版本都将被抓取和采集。这不会在 TSDB 中引起任何冲突,因为经典直方图作为多个带后缀的系列被采集,而原生直方图仅作为单个系列以其未修改的名称被采集。(例如:名为 rpc_latency_seconds 的 histograms 会产生一个名为 rpc_latency_seconds 的原生直方图系列,以及经典部分的多个系列,即 rpc_latency_seconds_sumrpc_latency_seconds_count,以及多个带有不同 le 标签的 rpc_latency_seconds_bucket 系列。)

将经典直方图抓取为 NHCBs

前述的 NHCB 能够将经典直方图建模为原生直方图。通过布尔抓取配置选项 convert_classic_histograms_to_nhcb,可以配置 Prometheus 将经典直方图作为 NHCBs 采集。

NHCBs 与经典直方图一样存在合并能力有限的问题,但它们的存储成本通常要低得多。

TSDB

注意本节提供了在 TSDB 中存储原生直方图的高级概述,并解释了一些可能容易忽略的重要个别方面。它不旨在解释实现细节、定义磁盘格式或引导您浏览代码库。有关于各种存储格式的详细文档,当然还有通常生成的 GoDoc,以tsdb 包storage 包作为合适的起点。一个有用的资源还有前面提到的Prometheus 原生直方图开发者指南

整数直方图 vs. 浮点直方图

TSDB 以不同的方式存储整数直方图和浮点直方图。通常,整数直方图预计能更好地压缩,因此如果所有分桶计数和观测值计数都在 int64 范围内具有整数值,TSDB 实现可(MAY)将浮点直方图存储为整数直方图,以便转换为整数直方图后能精确表示原始浮点直方图。(请注意,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 遇到一个无法写入当前所用块(chunk)的样本类型时,它会关闭该块,并使用适当的编码启动一个新块。(如果一个时间序列的每个样本都在不同样本类型之间来回切换,将导致每个样本都创建一个新块,这确实非常低效。)

直方图块使用多种自定义编码来处理数值,通过用更少的比特编码常见值,用更多的比特编码不常见值,从而减小数据大小。每种自定义编码的细节在底层块格式文档(以及其中链接的代码)中有详细描述。以下三种编码用于多个不同字段,因此在此命名以备后用:

  • varbit-int 是一种用于有符号整数的可变比特宽度编码。它使用 1 比特到 9 字节不等。接近零的数字需要更少的比特。这类似于浮点样本块中时间戳的编码方式,但使用了不同的比特长度分桶,针对原生直方图中常见的值分布进行了优化。
  • varbit-uint 是一种类似的编码,但用于无符号整数。
  • varbit-xor 是一种用于浮点数序列的可变比特宽度编码。它基于对序列中当前和前一个浮点值进行异或(XOR)操作。每个浮点数使用 1 比特到 77 比特不等。这与 TSDB 已经用于浮点样本的编码完全相同。

直方图块的开头与普通块一样,首先是块中样本数量(uint16),然后是一个字节,描述该直方图是仪表盘直方图还是计数器直方图,并为后者提供计数器重置信息。详情请参见下文的相应章节。接下来是所谓的块布局(chunk layout),它包含了以下由块中所有直方图共享的信息:

  • 零桶(zero bucket)的阈值,使用一种自定义编码,能将常见值(零或二的特定次幂)编码为仅一个字节,但对于任意值则需要 9 个字节。
  • 模式(schema),使用 varbit-int 编码。
  • 正值范围(positive spans),编码为范围数量(varbit-uint),后跟一个重复序列,包含每个范围的长度(varbit-uint)和偏移量(varbit-int)。
  • 负值范围(negative spans),编码方式同上。
  • 仅适用于模式 -53(NHCB)的自定义值,编码为自定义值的数量(varbit-uint),后跟一个重复序列,使用自定义编码表示这些自定义值。

块布局之后是一个重复的样本数据序列。整数直方图和浮点直方图的样本数据有所不同。对于整数直方图,每个样本的数据包含以下内容:

  • 时间戳,使用 varbit-int 编码。第一个样本使用绝对值,第二个样本使用与第一个样本的时间差,后续样本则使用“差值的差值”(即与传统浮点块中时间戳所用的“双重差值”编码相同,只是 varbit-int 编码的比特分桶不同)。
  • 观测计数,第一个样本使用 varbit-uint 编码,后续样本使用 varbit-int 编码,采用与时间戳相同的“双重差值”方法。
  • 零桶的填充数,第一个样本使用 varbit-uint 编码,后续样本使用 varbit-int 编码,采用与时间戳相同的“双重差值”方法。
  • 观测总和,第一个样本使用 float64 编码,后续样本使用 varbit-xor 编码(在当前样本和前一个样本之间进行异或操作)。
  • 正值桶的填充数,每个桶的值是与前一个桶的差值(或者对于第一个桶是绝对填充数),使用 varbit-int 编码,并采用与时间戳相同的“双重差值”方法。(换句话说,“双重差值”编码应用于本身已经是差值的值,因此有时被称为“三重差值”编码。)
  • 负值桶的填充数,编码方式同上。

浮点直方图的样本数据有以下不同之处:

  • 观测计数和零桶填充数现在是浮点数,因此其编码方式与观测总和相同(第一个样本为 float64,后续样本为 varbit-xor)。
  • 桶填充数现在不仅是浮点数,而且是绝对填充计数,而非桶之间的差值。在第一个样本中,所有桶填充数都以普通 float64 表示,而后续样本则使用 varbit-xor 编码,将当前样本和前一个样本的对应桶进行异或操作。

以下事件会触发切割一个新块(原因在括号中说明):

  • 整数直方图和浮点直方图之间的样本类型发生变化(因为两者需要完全不同的块编码)。
  • 仪表盘直方图和计数器直方图之间的样本类型发生变化(因为前导字节必须表示不同的类型)。
  • 计数器直方图发生计数器重置(需作为计数器重置信息存储在前导字节中,详见下文)。
  • 模式发生变化(这意味着我们需要一个新的块布局,而一个块只能有一个块布局)。
  • 零桶阈值发生变化(这会改变块布局,见上文)。
  • 自定义值发生变化(这会改变块布局,见上文)。
  • 一个陈旧标记(staleness marker)后跟一个常规样本(虽然这不严格要求创建新块,但可以假设大多数直方图在消失又回来后变化会很大,因此切割新块是最佳选择)。
  • 超出块大小限制(详见下文)。

范围的差异也会改变块布局,但通过根据需要添加(显式表示的)未填充桶来协调,以使块中的所有直方图共享相同的范围结构。如果一个桶消失了,这很简单,因为在将新直方图追加到块中时,只需将缺失的桶作为一个未填充桶添加到新直方图中即可。然而,一个先前填充的桶的消失构成了一次计数器重置(见下文),所以这种情况只能发生在仪表盘直方图中(它没有计数器重置)。更常见的情况是,新追加的直方图中存在的桶在之前追加的直方图中不存在。在这种情况下,必须将这些桶作为显式未填充桶添加到所有先前追加的直方图中。这需要对整个块进行完全的重新编码。(只重新编码受影响的部分存在一些优化潜力。实现这一点会相当复杂。到目前为止,完全重新编码的性能影响尚未凸显为问题。)

陈旧标记

注意要理解以下部分,回顾 TSDB 中陈旧标记的工作原理非常重要。在浮点序列中,陈旧标记由表示 NaN 值的众多比特模式中的一种特定模式来表示。在下文中,这个非常特定的浮点值被称为“特殊陈旧 NaN 值”。它(几乎可以肯定)绝不会由常规的浮点算术运算返回,因此与“自然出现”的 NaN 值不同,包括在观测值的特殊情况中讨论的那些。实际上,在查询 TSDB 时,特殊陈旧 NaN 值永远不会直接返回,而是在到达调用者之前被内部处理。

要在直方图序列中标记陈旧状态,可以使用通常的特殊陈旧 NaN 值。然而,这将需要切割一个新块,仅仅为了将序列标记为陈旧,因为跟在直方图值后面的浮点值必须存储在不同的块中(见上文)。因此,也存在一个直方图版本的陈旧标记,其中观测总和字段被设置为特殊陈旧 NaN 值。在这种情况下,所有其他字段都会被忽略,这使得可以将它们设置为适合高效存储的值(因为直方图版本的陈旧标记本质上只是一种存储优化)。这对浮点和整数直方图都有效(因为即使在整数直方图中,总和字段也是一个浮点值),并且可以使用适当的版本来避免切割新块。所有版本的陈旧标记(浮点、整数直方图、浮点直方图)都必须被 TSDB 视为等效。

块大小限制

浮点块的大小限制为 1024 字节。同样的大小限制也普遍用于直方图块。然而,如果单个直方图有很多桶,它们可能会变得非常大,因此盲目执行大小限制可能导致块中只有很少的直方图。(在最极端的情况下,单个直方图甚至可能占用超过 1024 字节,导致大小限制根本无法执行。)每个块的直方图数量很少,压缩率就会变差。因此,在 1024 字节的大小限制生效之前,必须达到每个块最少 10 个直方图的门槛。这意味着直方图块可能远大于 1024 字节。

要求每个块最少有 10 个直方图是一个初步的、非常简单的方法,未来可能会改进,以在块大小和压缩率之间找到更好的平衡。

计数器重置考量

通常,Prometheus 认为当一个计数器的值从一个样本下降到下一个样本时,它就发生了重置(但也请参阅下一节关于创建时间戳的处理)。在检测两个直方图样本之间的计数器重置时,情况更为复杂。

首先,仪表盘直方图和计数器直方图是明确不同的(而 Prometheus 通常在摄取后平等对待所有浮点样本,无论它们是作为仪表盘还是计数器指标摄取的)。计数器重置不适用于仪表盘直方图。

如果一个仪表盘直方图后面跟着一个计数器直方图,那么就假定发生了计数器重置,因为从仪表盘到计数器的变化被认为等同于仪表盘被删除,并且计数器从零开始被新创建。

最常见的情况是一个计数器直方图后面跟着另一个计数器直方图。在这种情况下,通过以下程序检测可能发生的计数器重置:

如果两个直方图在模式或零桶宽度上不同,这些变化可能是一次兼容的分辨率降低的一部分(这种情况会定期发生以减少直方图的桶数量)。对于一次兼容的分辨率降低,以下两点都成立:

  • 如果模式已更改,其编号已从一个标准指数模式减少到另一个标准模式。
  • 如果零桶宽度已更改,第一个直方图中的任何已填充常规桶要么完全包含在第二个直方图的零桶中,要么完全不包含(即,旧的常规桶与新的零桶没有部分重叠)。

如果任何条件不满足,则该变化不是兼容的分辨率降低。因为这种变化只有通过重置或新创建直方图才可能发生,所以它被视为一次计数器重置,检测程序至此结束。

如果两个条件都满足,则必须转换第一个直方图,使其模式和零桶宽度与第二个直方图匹配。这与之前描述的方式相同:合并相邻的桶以降低模式,并将常规桶与零桶合并以增加零桶的宽度。

在程序的这个阶段,两个直方图具有相同的模式和零桶宽度,这要么是因为从一开始就是这样,要么是因为第一个直方图经过了相应转换。(请注意,NHCBs 不使用零桶。为了本程序的目的,它们的零桶宽度和填充计数被认为是相等的。)在这种情况下,以下任何一种情况都构成计数器重置:

  • 观测计数下降(但值得注意的是,不是观测总和的下降)。
  • 任何桶(包括零桶)的填充计数下降。这包括一个已填充桶消失的情况,因为一个未表示的桶等同于一个填充数为零的桶。
  • 自定义值的任何更改。这仅适用于使用自定义值的模式(目前是模式 -53,即 NHCB)。(TODO:原则上,NHCBs 中也可能存在兼容桶更改的概念,但这样的概念尚未实现。)

如果以上情况均未发生,则没有计数器重置。

由于整个过程相对复杂,计数器重置检测最好在摄取期间进行一次,并将结果持久化以供后续使用。摄取期间的计数器重置检测无论如何都必须进行,因为计数器重置是切割新块的触发器之一。

在计数器重置后切割新块旨在提高压缩率。计数器重置会将所有桶的填充数设为零,因此需要表示的桶更少。然而,一个块必须表示块中所有直方图的所有桶的超集,因此切割新块可以为新块启用更简单的桶集合。

这反过来意味着在一个块的第一个样本之后永远不会有计数器重置。因此,唯一需要持久化的计数器重置信息是块中第一个直方图的信息。这发生在所谓的*直方图标志*中,它是一个存储在块中样本数量之后的单字节。该字节目前仅用于计数器重置信息,但将来可能用于其他标志。计数器重置信息使用前两位。四种可能的比特模式在 `chunkenc` 包中表示为 `CounterResetHeader` 类型的 Go 常量。它们的名称和含义如下:

  • GaugeType (比特模式 `11`):该块包含仪表盘直方图。计数器重置与仪表盘直方图无关。
  • CounterReset (比特模式 `10`):在前一个块的最后一个直方图和这个块的第一个直方图之间发生了计数器重置。(很可能计数器重置是切割新块的原因。)
  • NotCounterReset (比特模式 `01`):在前一个块的最后一个直方图和这个块的第一个直方图之间没有发生计数器重置。(这通常发生在新块因为前一个块达到大小限制而被切割时。)
  • UnknownCounterReset (比特模式 `00`):在前一个块的最后一个直方图和这个块的第一个直方图之间是否发生计数器重置是未知的。

UnknownCounterReset 始终是一个安全的选择。它不会阻止计数器重置检测,但只是要求在需要计数器重置信息时必须(再次)执行计数器重置检测程序。

在查询 TSDB 时,计数器重置信息会传递给调用者(在 Go 代码中,作为 `Histogram` 和 `FloatHistogram` Go 类型中 `CounterResetHint` 类型的字段,使用与上述比特模式常量同名的枚举常量)。

对于仪表盘直方图,`CounterResetHint` 始终是 `GaugeType`。任何其他的 `CounterResetHint` 值都意味着所讨论的直方图是一个计数器直方图。通过这种方式,查询器(包括 PromQL 引擎,见下文)可以获得一个直方图是仪表盘还是计数器的信息(这与浮点样本有显著不同)。

只要计数器直方图从单个块中按顺序返回,块中第二个及以后直方图的 `CounterResetHint` 就会被设置为 `NotCounterReset`。(重叠块和乱序摄取可能导致直方图序列来自多个块,这需要特殊处理,见下文。)

当从计数器直方图块返回第一个直方图时,`CounterResetHint` 必须设置为 `UnknownCounterReset`,*除非* TSDB 实现可以确保先前返回的直方图确实是摄取时用于检测计数器重置的前一个直方图。只有在后一种情况下,块中的计数器重置信息才可以直接用作返回直方图的 `CounterResetHint`。

需要采取这种预防措施,因为有多种方式可能导致块被移除或插入(例如通过墓碑标记删除或为回填添加块)。虽然计数器重置归因于一个样本,但它实际上发生在被标记的样本和前一个样本*之间*。移除前一个样本或在这两个样本之间插入另一个样本会使先前执行的计数器重置检测失效。

待办事项目前,Prometheus TSDB 无法确保前一个块仍然是摄取时的那个块。因此,Prometheus 目前对来自计数器直方图块的*所有*第一个直方图返回 `UnknownCounterReset`。有关更改此行为的努力,请参阅跟踪问题

如上所述,如果 `CounterResetHint` 设置为 `UnknownCounterReset`,查询器必须(再次)执行计数器重置检测程序。

在处理重叠块或乱序样本时(用于查询或在压缩期间)必须特别小心。在这些情况下,可能会发生计数器重置的过度检测和欠检测,如下例所示:

  • 欠检测示例:一个块包含样本 ABC,没有计数器重置。另一个块包含样本 DEF,同样没有计数器重置。这些块重叠并指向同一个序列。当一起查询它们时,样本的时间顺序结果是 ADBECF。现在,在这些样本中的某些甚至所有样本之间很可能存在计数器重置。如果这两个样本实际上来自不相关的序列,并且被意外合并到同一个序列中,这种情况尤其可能发生。然而,即使是这样的意外合并,TSDB 也必须正确处理。如果重叠的块被压缩成一个新块,则必须进行新的计数器重置检测,以捕获新的计数器重置。如果直接查询重叠的块(未经事先压缩),则必须为每个来自与先前返回的样本不同的块的样本设置 `CounterResetHint` 为 `UnknownCounterReset`,这强制查询器进行计数器重置检测(利用上述的安全回退机制)。
  • 过度检测示例:存在一个样本序列 ABCD,在 B 和 C 之间发生了计数器重置。然而,初始摄取错过了 B 和 C,因此只摄取了 A 和 D,并且在 A 和 D 之间检测到了计数器重置。后来,B 和 C 被摄取(通过乱序摄取或作为单独的块稍后添加到 TSDB 中),并且在 B 和 C 之间检测到了计数器重置。在这种情况下,每个样本都进入了自己的块,因此当组装所有块时,它们甚至不重叠。然而,当根据上述规则返回计数器重置提示时,C 和 D 都将以 `CounterResetHint` 为 `CounterReset` 的形式返回给查询器,尽管现在 C 和 D 之间没有计数器重置。与前一个示例中的情况类似,必须在 A 和 B 之间执行新的计数器重置检测,并在 C 和 D 之间再执行一次。或者,B 和 D 都必须以 `CounterResetHint` 为 `UnknownCounterReset` 的形式返回。

总而言之,每当 TSDB 无法安全地确定两个样本之间的计数器重置检测是在摄取时发生的,它要么必须执行另一次计数器重置检测,要么必须为第二个样本返回一个 `CounterResetHint` 为 `UnknownCounterReset`。

请注意,存在上述程序无法检测到的计数器重置的可能性,即如果重置后的直方图中的计数增长得足够快,以至于计数器重置后的第一个样本的计数与重置前的最后一个样本相比没有减少。(对于浮点计数器来说,这也是一个问题,而且实际上更有可能发生。)通过上述机制,即使在这种情况下,也可以存储计数器重置,前提是该重置是通过其他方式检测到的。然而,由于插入和移除块、乱序样本和重叠块(如上所述)引起的复杂性,如果需要第二轮计数器重置检测,此信息可能会丢失。(待办事项:目前,此信息会可靠地丢失,请参见上面的待办事项。)一种更安全地标记计数器重置的方法是通过创建时间戳(见下一节)。

创建时间戳处理

OpenMetrics 为计数器、摘要和经典计数器直方图引入了所谓的创建时间戳。(这个术语可能是“created-at timestamp”的缩写。更合适的术语可能是“creation timestamp”或“reset timestamp”,但“created timestamp”这个术语现已牢固确立。)

创建时间戳提供了指标最近一次被创建或重置的时间。一份设计文档描述了 Prometheus 如何处理创建时间戳。

创建时间戳对于原生直方图也很有用。与为浮点计数器插入合成零样本的方式相同,为计数器直方图插入一个直方图样本的零值。直方图的零值没有已填充的桶,观测总和、观测计数和零桶填充数都为零。直方图的模式、零桶宽度、自定义值以及浮点与整数类型应当与紧跟在合成零样本之后的样本匹配(以避免触发虚假的计数器重置检测)。

合成零样本的计数器重置信息总是设置为 `CounterReset`。

Exemplar

原生直方图的范例(Exemplar)是附加在整个直方图样本上的,而不是附加到单个桶上。(也请参阅暴露格式部分。)因此,允许(实际上也是常见情况)单个原生直方图样本附带多个范例。

范例可能会也可能不会在两次抓取之间发生变化。抓取器应当检测未变化的范例,以避免存储许多重复的范例。然而,重复检测可能代价高昂,因为单个样本可能有很多范例,其中任何子集都可能是上次抓取的重复范例。TSDB 可以依赖这样一个假设:任何新的范例都比任何先前暴露的范例具有更近的时间戳。(请记住,原生直方图的范例必须有时间戳。)这样,就可以高效地进行重复检测:

  1. 新摄取的原生直方图的范例按以下字段排序:首先是时间戳,然后是值,最后是标签。
  2. 范例按排序顺序追加到范例存储中。
  3. 对于那些排序在最后一个成功追加的范例(可能来自同一指标的上次抓取)之前或与之相等的范例,追加操作会失败。
  4. 对于那些排序在最后一个成功追加的范例之后的范例,追加操作会成功。

只有当一个摄取的直方图的所有范例都排在最后一个成功追加的范例之前时,才算作乱序范例。这不会检测到与较新范例或与最后一个成功追加的范例的重复项混合在一起的乱序范例,这被认为是可接受的。

PromQL

本节描述 PromQL 如何处理原生直方图。它侧重于一般概念,而不是单个操作的每个细节。后者请参考关于运算符函数的 PromQL 文档。

注解

原生直方图的引入会产生一些情况,其中 PromQL 表达式返回意外的结果,最常见的情况是输出向量中的部分或所有元素意外丢失。为了帮助用户检测和理解这些情况,作用于原生直方图的操作通常使用注解。注解可以有警告和信息级别,并描述评估期间可能遇到的问题。警告级别用于标记最可能是用户需要采取行动的实际问题的情况。信息级别用于那些可能是有意为之,但仍然足够不寻常以至于需要标记的情况。

整数直方图 vs. 浮点直方图

PromQL 始终作用于浮点直方图。存储为整数直方图的原生直方图在从 TSDB 检索时会自动转换为浮点直方图。

直方图之间的兼容性

当一个运算符或函数作用于两个或多个原生直方图时,所涉及的直方图需要具有相同的模式和零桶宽度。在一定限制内,可以动态转换直方图以满足这些兼容性标准:

  • 一个 NHCB(模式 -53)只与也必须具有完全相同自定义值的其他 NHCB 兼容。(原则上,自定义值可能存在可以协调的差异,但 PromQL 尚未考虑这些。)
  • 具有标准模式的直方图总是可以通过降低具有较大模式(即更高分辨率)的直方图的分辨率,转换为最小的(即最低分辨率的)公共模式。这通过将相邻的桶合并为较小模式的较大桶的常规方式实现。
  • 不同的零桶宽度通过扩展较小的零桶来处理,将任何已填充的常规桶适当地合并到扩展的零桶中。如果最大的公共宽度恰好落在任何已填充桶的中间,它会进一步扩展以与该桶的桶边界重合。(更多细节请参见上面的零桶部分。)

如果不兼容性阻止了操作,则会在结果中添加一个警告级别的注解。

计数器重置

计数器重置的定义如上文所述。从 TSDB 返回的计数器重置提示可以被考虑,以避免显式的计数器重置检测,并正确处理通过常规程序无法检测到的计数器重置。(这意味着这些计数器重置只在尽力而为的基础上被考虑。然而,对于 TSDB 本身也是如此,见上文。)与经典直方图和摘要的计数器重置处理的一个显著区别是,观测总和的减少本身并*不*构成计数器重置。(例如,即使直方图观测到了负值,计算原生直方图的速率仍然可以正常工作。)

请注意,除非 PromQL 引擎能够安全地检测到从子查询返回的连续计数器直方图在 TSDB 中也是连续的,否则不得考虑子查询返回的计数器直方图的计数器重置提示来避免显式计数器重置检测。

仪表盘直方图 vs. 计数器直方图

通过从 TSDB 返回的计数器重置提示,PromQL 能够知道原生直方图是仪表盘还是计数器直方图。为了模仿 PromQL 对浮点样本的处理方式(它无法可靠地区分浮点计数器和仪表盘),作用于计数器的函数仍然会处理仪表盘直方图,反之亦然,但结果会返回一个警告级别的注解。请注意,在这种情况下,必须对仪表盘直方图执行显式的计数器重置检测,将其视为计数器直方图。

桶内插值

在估算分位数或分数时,PromQL 必须在一个桶内应用插值。在经典直方图中,这种插值是线性的。它基于观测值在桶内均匀分布的假设。实际上,这个假设可能与现实相差甚远。(例如,一个 API 端点可能对几乎所有请求的响应延迟都是 110ms。那么中位数延迟甚至 90 百分位延迟都会接近 110ms。如果一个经典直方图的桶边界在 100ms 和 200ms,它会看到大部分观测值都在这个范围内,并估算中位数为 150ms,90 百分位数为 190ms。)最坏的情况是在一个桶的一端进行估算,而实际值在桶的另一端。因此,可能的最大误差是整个桶的宽度。不进行任何插值,而是使用桶内的某个固定中点(例如算术平均值甚至调和平均值),可以最小化可能的最大误差(对于算术平均值,误差将是桶宽度的一半),但实际上,线性插值在平均情况下产生的误差更低。由于插值在多年的经典直方图使用中效果良好,因此也对原生直方图应用插值。

对于 NHCBs,PromQL 应用与经典直方图相同的插值方法以保持结果一致。(NHCBs 的主要用例是作为经典直方图的直接替代品。)然而,对于标准指数模式,线性插值可以被视为一种不匹配。虽然指数模式主要旨在最小化分位数估算的相对误差,但它们也受益于桶的均衡使用,至少在观测值的某些范围内是如此。基本假设是,对于大多数实际出现的分布,观测值的密度往往在较小的观测值处更高。因此,PromQL 对标准模式使用指数外推法,该模型假设当模式数增加一(即分辨率加倍)时,将一个桶分成两个,平均而言,两个新桶的填充数将相似。更详细的解释可以在实现插值方法的 PR中找到。

一个特殊情况是在零桶内插值。零桶打破了指数分桶模式。因此,在零桶内应用线性插值。此外,如果一个直方图的所有已填充常规桶都是正的,则假定零桶中的所有观测值也都是正的,即在零和零桶的上界之间进行插值。如果一个直方图的所有已填充常规桶都是负的,情况则相反,即在零桶的下界和零之间进行插值。

混合序列

如上所述,样本类型和原生直方图的风格都不是序列身份的一部分。因此,同一个序列可能包含不同样本类型和风格的混合。

计数器直方图和仪表盘直方图的混合不会阻止任何 PromQL 操作,但如果一些输入样本具有不适当的风格,结果会返回一个警告级别的注解(见上文)。

浮点样本和直方图样本的混合更具问题性。许多作用于范围向量的函数会从结果中移除那些输入元素包含浮点数和直方图混合的元素。如果发生这种情况,结果会添加一个警告级别的注解。具体示例可以在下文找到。

一元负号与负直方图

一元负号可以用于原生直方图。它返回一个直方图,其中所有桶的填充数以及观测计数和观测总和的符号都已反转。在任何情况下,计数器重置提示都会设置为 `GaugeType`。其他一切保持不变。强制使用 `GaugeType` 是必要的,因为反转的符号会干扰显式的计数器重置检测。

通常,具有负桶填充数或负观测计数的直方图本身并没有实际意义,它们仅用作其他表达式中的中间结果。在 PromQL 中,它们总是被视为仪表盘直方图。它们不能作为记录规则的结果被持久化。(评估为负直方图的规则会导致错误。)在任何交换格式(暴露格式、remote-write、OTLP)中都无法表示负直方图。

二元运算符

大多数二元运算符在两个直方图之间、一个直方图和一个浮点数之间或一个直方图和一个标量之间不起作用。如果一个运算符处理了这种不可能的组合,相应的元素会从输出向量中移除,并在结果中添加一个信息级别的注解。(这种情况与标签匹配有些相似,其中样本类型扮演着类似标签的角色。因此,这种不匹配可能是已知且故意的,这也是注解级别仅为信息的原因。)

以下描述了所有确实*有效*的操作。

加法(`+`)和减法(`-`)在两个兼容的直方图之间有效。这些运算符会加上或减去所有匹配的桶填充数以及观测计数和观测总和。缺失的桶被假定为空并相应处理。通常,两个操作数都应该是仪表盘。加减计数器直方图需要谨慎,但 PromQL 允许这样做。一个仪表盘直方图和一个计数器直方图相加会得到一个仪表盘直方图。两个计数器直方图相加会得到一个计数器直方图。如果两个操作数共享相同的计数器重置提示,则结果计数器直方图保留该计数器重置提示。否则,结果计数器重置提示会设置为 `UnknownCounterReset`。减法的结果总是被标记为仪表盘直方图,因为它可能导致负直方图,见上面的注释。将两个具有直接矛盾的计数器重置提示(即 `CounterReset` 和 `NotCounterReset`)的计数器直方图相加或相减会触发一个警告级别的注解。(待办事项:如上文所述,TSDB 目前不返回 `NotCounterReset`,因此这种注解只会在涉及 `HistogramStatsIterator` 的特定情况下发生,该迭代器包含额外的计数器重置跟踪。见跟踪问题。)

乘法(`*`)在一侧是浮点样本或标量,另一侧是直方图的情况下有效,顺序不限。它将所有桶的填充数以及观测计数和观测总和乘以该浮点数(样本或标量)。这会导致“缩放的”有时甚至是负的直方图,这通常仅作为其他表达式中的中间结果有用(另见上面的注释)。乘法对计数器直方图和仪表盘直方图都有效,并且它们各自的类型不会因该操作而改变。

除法(`/`)在左侧是直方图,右侧是浮点样本或标量的情况下有效。它等同于与浮点数(样本或标量)的倒数相乘。除以零会导致一个没有常规桶的直方图,并且零桶填充数以及观测计数和总和都设置为 `+Inf`、`-Inf` 或 `NaN`,具体取决于它们在输入直方图中的值(正、负或零/`NaN`)。

相等(`==`)和不相等(`!=`)在两个直方图之间有效,无论是在其过滤版本中还是带有 `bool` 修饰符时。它们比较模式、自定义值、零桶阈值、所有桶的填充数以及观测总和和计数。直方图是计数器还是仪表盘类型与比较无关。(一个计数器直方图可以等于一个仪表盘直方图。)

逻辑/集合二元运算符(`and`, `or`, `unless`)即使涉及直方图样本也能按预期工作。它们只检查向量元素是否存在,而不根据元素的样本类型或风格(浮点或直方图,计数器或仪表盘)改变其行为。

“修剪”运算符 `>/` 和 `</` 是专门为原生直方图引入的。它们只在左侧是直方图,右侧是浮点样本或标量的情况下有效。(它们在*两侧*都是浮点样本或标量时无效。在这种情况下会返回一个信息级别的注解。)这些运算符分别从直方图中移除大于或小于右侧浮点值的观测值,并返回结果直方图。只有当阈值与桶边界重合时,移除才是精确的。否则,必须使用受影响桶内的插值,如上文所述。直方图的计数器与仪表盘类型被保留。(待办事项:这些运算符尚未实现,细节也可能改变,见跟踪问题。)

聚合运算符

以下聚合运算符对浮点和直方图样本以相同方式工作(原因在括号中说明):

  • group (该聚合的结果不依赖于样本值。)
  • count (该聚合的结果不依赖于样本值。)
  • count_values (使用 Go `FloatHistogram.String` 方法生成的文本表示作为直方图的值。)
  • limitk (被采样的元素被原样返回。)
  • limit_ratio (被采样的元素被原样返回。)

sum 聚合运算符对原生直方图有效,通过与上述 `+` 运算符相同的方式对要聚合的直方图求和,包括关于计数器与仪表盘直方图的影响。(通常,只有仪表盘直方图应该以这种方式聚合。)`avg` 聚合运算符以相同方式工作,但将总和除以聚合的直方图数量(与上述 `/` 运算符相同的方式)。这两个聚合运算符都会从输出向量中移除那些需要将浮点样本与直方图样本聚合的元素。这种移除会通过一个警告级别的注解来标记。

所有其他聚合运算符都*不*对原生直方图起作用。输入向量中的直方图会被简单地忽略,并且会为每个被忽略的直方图添加一个信息级别的注解。

函数

以下函数作用于原生直方图的范围向量,通过将通常的浮点操作分别应用于匹配的桶(包括零桶)以及观测总和和计数,从而产生一个新的原生直方图:

  • delta() (用于仪表盘直方图。)
  • increase() (用于计数器直方图。)
  • rate() (用于计数器直方图。)
  • idelta() (用于仪表盘直方图。)
  • irate() (用于计数器直方图。)

如上所述,这些函数应当应用于仪表盘直方图或计数器直方图。然而,它们都对两种类型都有效,但如果范围向量中包含至少一个不合适类型的直方图,则会在结果中添加一个警告级别的注解。

对于在范围内包含浮点样本和直方图样本混合的序列,`delta()`、`increase()` 和 `rate()` 不返回结果。对于范围内最后两个样本是浮点样本和直方图样本混合的序列,`idelta()` 和 `irate()` 不返回结果。在任何一种情况下,都会为每个因此原因而缺失的输出元素添加一个警告级别的注解。

所有这些函数都返回仪表盘直方图作为结果。

与往常一样,这些函数尝试通过尽可能将模式转换为公共模式来协调不同的模式。然而,应用于计数器的函数(`increase()`、`rate()`、`irate()`)在第一个和第二个样本之间发生计数器重置时,不会对第一个样本执行此转换。在这种情况下,第一个样本不包括在计算中,因此第一个样本与其他样本之间不兼容的桶布局会被静默忽略。

为了防止外推到零以下,应用了与浮点计数器相同的启发式方法,但仅基于观测计数。因此,在某些情况下,个别桶仍可能被外推到零以下。另一种方法可能是找到最小的外推,使得计数和任何桶都不会被外推到零以下。然而,这不一定会带来更好的启发式方法,同时会带来显著的复杂性成本。在常见且重要的情况下,即范围内的第一个样本是来自创建时间戳的合成零样本,有限的外推实际上会非常精确地工作,因为计数和所有桶在合成样本的时间戳处恰好为零,这也是外推被限制到的时间点。请注意,经典直方图将启发式方法独立应用于每个桶、计数和总和(因为它们都是独立的序列)。这已知会导致不一致。NHCBs 不会重现这个问题,其工作方式与其他原生直方图相同,这意味着在比较经典直方图和等效 NHCBs 时,`rate()` 和 `increase()` 的结果可能会略有不同。

avg_over_time() 和 `sum_over_time()` 对原生直方图的作用方式与相应的聚合运算符相对应。特别是,如果一个序列在范围内包含浮点样本和直方图样本的混合,相应的结果会从输出向量中完全移除。这种移除会通过一个警告级别的注解来标记。

changes() 和 `resets()` 函数对原生直方图样本的作用方式与对浮点样本相同。它们甚至可以在同一序列内处理浮点样本和直方图样本的混合。在这种情况下,从浮点样本到直方图样本的更改(反之亦然)对 `changes()` 计为一次更改,对 `resets()` 计为一次重置。从计数器直方图到仪表盘直方图的类型更改(反之亦然)不计为 `changes()` 的一次更改。`resets()` 应当只应用于计数器浮点数和计数器直方图,但该函数仍然可以处理仪表盘直方图,在这种情况下应用显式的计数器重置检测。此外,从计数器直方图到仪表盘直方图的更改(反之亦然)被计为一次重置。

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` 的第一个参数来估算直方图中的最大和最小观测值。然而,具有标准模式的原生直方图能够产生更有用的结果,这不仅是因为原生直方图通常具有更高的分辨率,更重要的是因为具有标准模式的原生直方图在整个 float64 数字范围内保持相同的分辨率。对于经典直方图,最大观测值很有可能在 +Inf 桶中,因此估算结果只是返回 +Inf 桶之前的最后一个桶的上限。类似地,最小观测值通常会在最低的桶中。

histogram_quantile 将值为 NaN 的观测值(这不应该发生,请参见上文)视为 +Inf 的观测值。其原理是 NaN 永远不小于 histogram_quantile 返回的任何值。只要结果落入现有的桶中,我们就会返回如同 NaN 观测值为 +Inf 时计算出的结果,并发出一个信息级别的注解,告知用户结果因 NaN 而有所偏差。这与传统直方图通常处理 NaN 观测值的方式一致(在大多数实现中,NaN 最终会进入 +Inf 桶)。如果结果超出现有所有桶的范围,我们返回 NaN。详细解释请参见下文的 histogram_fraction;直观地说,这种情况意味着我们没有一个数大于分位数中的所有观测值,因为 NaN 无法与任何数字比较。我们也会针对这种情况返回一个特定的信息级别注解。

以下函数是专门为原生直方图引入的:

  • histogram_avg()
  • histogram_count()
  • histogram_fraction()
  • histogram_sum()
  • histogram_stddev()
  • histogram_stdvar()

所有这些函数都会静默忽略作为输入的浮点数样本。每个函数返回一个浮点数样本向量。

histogram_count()histogram_sum() 分别返回原生直方图中包含的观测值数量或观测值总和。由于它们是普通函数,其结果不能在范围选择器中使用。计算观测值数量或总和的速率时,推荐的方法是首先对直方图进行速率计算,然后对结果应用 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 中,介于提供的边界(标量值 lowerupper)之间的观测值的估算分数。估算的误差取决于底层原生直方图的分辨率以及提供的边界与直方图中桶边界的对齐程度。+Inf-Inf 是有效的边界值,可用于估算所有高于或低于某个值的观测值的分数。然而,值为 NaN 的观测值总是被认为在指定的边界之外(即使是 +Inf-Inf)。提供的边界是包含性还是排他性仅在提供的边界与底层原生直方图中的桶边界精确对齐时才相关。在这种情况下,其行为取决于直方图模式的精确定义。

q = histogram_fraction(-Inf, x, histogram) 的值意味着小于或等于 x 的观测值的分数是 q。另一方面,y = histogram_quantile(q, histogram) 意味着 q 分数的观测值小于或等于 y。由于 histogram_quantile 计算的是 y 的近似最小值,因此通常情况下 y<=x。考虑这样一种情况:90% 的观测值为 NaN。那么 histogram_fraction 的最大值为 0.1,因为 histogram_fraction 认为 NaN 观测值在任何桶之外。如果例如 histogram_quantile(0.5, histogram) 返回了任何实数 y,那么根据上述论证,我们应该能找到某个数 x,使得 y<=xhistogram_fraction(-Inf, x, histogram) 等于 0.5,然而对于任何 y 都不会发生这种情况,这就是为什么如果 histogram_quantile 的结果会超出所有桶的范围,我们就返回 NaN 的原因。

以下函数不直接与样本值交互,因此它们处理原生直方图样本的方式与处理浮点数样本的方式相同:

  • 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() 函数,原生直方图样本会从输入范围向量中移除。如果任何时间序列在范围内混合了浮点数样本和直方图样本,移除直方图的操作会通过一个信息级别的注解来标记。

记录规则

记录规则可能会产生原生直方图值。它们会像正常采集一样被存回时序数据库(TSDB),包括直方图是仪表盘直方图(gauge histogram)还是计数器直方图(counter histogram)。在后一种情况下,由计数器重置提示明确标记的计数器重置也会被存储,否则在采集期间会启动新的计数器重置检测。

如果转换能够精确表示原始直方图中的所有浮点数值,TSDB 实现可以将记录规则创建的浮点数直方图转换为整数直方图。

告警规则

告警规则对原生直方图的处理方式与平常一样。但是,建议避免将原生直方图作为告警的输出值。如果在模板中使用原生直方图样本,它们会以简单的文本形式呈现(由 Go 的 FloatHistogram.String 方法生成),这对人类来说难以阅读。

测试框架

PromQL 测试框架已扩展,使得 PromQL 单元测试和通过 promtool 进行的规则单元测试都可以包含原生直方图。直方图样本的表示法很复杂,在规则单元测试文档中有解释。

在单元测试框架中,有一个替代的 load 命令叫做 load_with_nhcb,它将传统直方图转换为 NHCBs,并加载传统直方图的浮点数时间序列以及转换后产生的 NHCB 时间序列。

expect 关键字并非原生直方图特有,但在其上下文中非常有用,它可以在单元测试框架中定义对信息级别和警告级别注解的期望。

优化

和往常一样,只要行为保持不变,PromQL 实现可以应用任何它们认为合适的优化。解码原生直方图可能因为桶数量众多而相当昂贵。同样,在 PromQL 引擎内深拷贝一个直方图样本比拷贝一个简单的浮点数样本要昂贵得多。与总是解码所有内容并总是拷贝所有内容的朴素方法相比,这带来了巨大的优化潜力。

Prometheus 目前试图避免不必要的拷贝(待办事项:但仍需实现一个合适的写时复制(CoW)类方法,因为它会更清晰且不易出错),并在只需要观测值总和与计数的情况下跳过对桶的解码。

Prometheus 查询 API

查询 API 文档包含了对原生直方图的支持。本节重点介绍与原生直方图相关的部分,并提供一些 API 文档中未包含的背景信息。

即时查询和范围查询

为了在即时查询(query 端点)和范围查询(query_range 端点)的 JSON 响应中返回原生直方图,vectormatrix 结果类型都需要通过一个新的键进行扩展。

vector 结果类型在与现有 value 键相同的层级上增加了一个新的键 histogram。这两个键是互斥的,即 vector 中的每个元素要么有一个 value 键(用于浮点数结果),要么有一个 histogram 键(用于直方图结果)。histogram 键的值结构与 value 键的值类似(一个双元素数组),不同之处在于代表浮点数样本值的字符串被一个下文描述的特定直方图对象所取代。

matrix 结果类型在与现有 values 键相同的层级上增加了一个新的键 histograms。这些键不是互斥的。一个时间序列可能同时包含浮点数值和直方图值,但对于给定的时间戳,必须只有一个样本,要么是浮点数,要么是直方图。histograms 键的值结构与 values 键的值类似(一个包含 n 个双元素数组的数组),不同之处在于代表浮点数样本值的字符串被下文描述的特定直方图对象所取代。

注意,更好的键命名应该是 float/histogramfloats/histograms,因为浮点数值和直方图值都是值。当前的命名有历史原因。(过去,只有一种值类型,即浮点数,所以将键简单地称为 valuevalues 是显而易见的选择。)此处的意图是不破坏不了解原生直方图的现有消费者。

上述的直方图对象具有以下结构:

{
  "count": "<count_of_observations>",
  "sum": "<sum_of_observations>",
  "buckets": [ [ <boundary_rule>, "<left_boundary>", "<right_boundary>", "<count_in_bucket>" ], ... ]
}

countsum 直接对应于同名的直方图字段。每个桶都明确地用其边界和计数表示,包括零桶。因此,跨度和模式不属于响应的一部分,直方图对象的结构不依赖于所使用的模式。

<boundary_rule> 占位符是一个 0 到 3 之间的整数,含义如下:

  • 0:“左开”(左边界不包含,右边界包含)
  • 1:“右开”(左边界包含,右边界不包含)
  • 2:“双开”(两个边界都不包含)
  • 3:“双闭”(两个边界都包含)

对于标准模式,正数桶是“左开”,负数桶是“右开”,零桶(左边界为负,右边界为正)是“双闭”。对于 NHCBs,所有桶都是“左开”(镜像了传统直方图的行为)。未来的模式可能会使用不同的边界规则。

元数据

对于 series 端点,包含原生直方图的时间序列与仅包含浮点数的常规时间序列以相同方式包含。该端点不提供任何关于包含了哪些样本类型的信息(实际上,任何时间序列都可能包含任一或两种样本类型)。特别注意,一个由目标以 request_duration_seconds 名称暴露的直方图,如果作为原生直方图被暴露和采集,将产生一个名为 request_duration_seconds 的时间序列;但如果作为传统直方图被暴露和采集,将产生一组名为 request_duration_seconds_sumrequest_duration_seconds_countrequest_duration_seconds_bucket 的时间序列。如果该直方图被同时作为原生直方图和传统直方图采集series 端点将返回上述所有的系列名称。

目标和指标元数据(端点 targets/metadatametadata)的工作方式略有不同,因为它们作用于目标暴露的原始名称。这意味着一个名为 request_duration_seconds 的传统直方图将仅由这些元数据端点表示为 request_duration_seconds(而不是 request_duration_seconds_sumrequest_duration_seconds_countrequest_duration_seconds_bucket)。一个原生直方图 request_duration_seconds 也将以此名称表示。即使在 request_duration_seconds 同时作为传统和原生直方图被采集的情况下,也不会发生冲突,因为返回的元数据实际上是相同的(最值得注意的是返回的 type 将是 histogram)。换句话说,目前无法仅通过元数据端点区分原生直方图和传统直方图。需要通过 series 端点进行额外的查找。目前没有计划改变这一点,因为现有的元数据端点本身就存在严重限制(没有历史信息,没有规则创建的指标的元数据,处理不同目标之间冲突元数据的能力有限)。不过,有计划全面改进 Prometheus 中的元数据处理。这些努力也将考虑如何恰当地支持原生直方图。(待办事项:随着进展更新。)

Prometheus UI

本节描述 Prometheus 自身 UI 对直方图的渲染。这可以作为第三方图形前端的指南。

表格视图中,一个直方图数据点以条形图的形式图形化呈现,并附有所有桶的文本表示,包括其上下限、观测计数和总和。条形图中的每个条形代表一个桶。每个条形在 x 轴上的位置由相应桶的上下限决定。每个条形的面积与相应桶的总体成正比(这是渲染直方图的一个核心原则)。

图形化直方图允许在指数和线性 x 轴之间进行选择。前者是默认设置。它非常适合标准模式。(待办事项:考虑将线性作为非指数模式的默认设置。)方便的是,指数模式的所有常规桶在指数 x 轴上具有相同的宽度。这意味着 y 轴可以显示实际的桶总体,而不会违反上述条形面积(而非高度)代表桶总体的原则。零桶是个例外。技术上讲,它的宽度是无限的。Prometheus 只是将其渲染为与常规指数桶相同的宽度(这反过来意味着 x 轴在零点附近并非严格指数的)。(待办事项:如何为非指数模式进行渲染。)

使用线性 x 轴时,桶的宽度通常各不相同。因此,y 轴显示的是桶总体除以其宽度。Prometheus UI 不在 y 轴上渲染数值,因为它们对人类来说 comunque 难以解读。总体仍然可以在文本表示中查看。

图表视图中,Prometheus 显示一个热力图(待办事项:尚未实现,见下文),可以看作是一系列随时间变化的直方图,旋转了90度,并用颜色而不是条形高度来编码桶的总体。将计数器类直方图渲染为热力图的典型查询是 rate 查询。热力图是一种极其强大的表示方法,能让人类轻松发现分布特征随时间的变化。

待办事项热力图尚未实现。目前,UI 仅将观测值的总和绘制成传统图表。请参见跟踪问题。该问题也讨论了如何在表格视图中处理范围向量的渲染。

模板展开

原生直方图支持模板展开。它们以一种受开闭区间数学表示法启发的文本形式呈现。(这是由 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}

远程写入与远程读取

远程写入与远程读取的 protobuf 规范已为原生直方图作为实验性功能进行了扩展。无法处理原生直方图的接收端将简单地忽略新增的字段。尽管如此,Prometheus 必须配置为通过远程写入发送原生直方图(通过将 send_native_histograms 远程写入配置设置为 true)。

远程写入 v2中,原生直方图是一个稳定功能。

在发送或接收时将传统直方图转换为 NHCBs 可能看起来很有吸引力。然而,这并不能克服传统直方图在通过远程写入传输时已知的一致性问题。相反,传统直方图应该在抓取期间转换为 NHCBs。同样,显式的 OTel 直方图应该在 OTLP 采集期间就转换为 NHCBs。

待办事项远程写入中一个仍可能存在的问题是,如果最初为同一个原生直方图采集的多个样本数据在不同的远程写入请求中发送,该如何处理。

联邦

只要联邦抓取使用 protobuf 格式,原生直方图的联邦功能就能按预期工作。一旦 OpenMetrics 文本格式支持原生直方图,原则上就可以通过该格式进行联邦,但出于效率考虑,无论如何都首选通过 protobuf 进行联邦。

当通过联邦端点暴露时,NHCBs 会被渲染为传统直方图。抓取器可以选择将它们转换回 NHCBs 或作为传统直方图进行采集。不过,后者可能导致命名冲突。

待办事项一旦 OM 支持 NH,将进行更新。

OTLP

Prometheus 内置的 OTLP 接收器利用上文描述的兼容性,将传入的 OTel 指数直方图转换为 Prometheus 原生直方图。使用大于 8 的模式(在 OTel 术语中为“scale”)的直方图的分辨率将被降低以匹配模式 8。(在不太可能的情况下,如果使用小于 -4 的模式,采集将失败。)

显式的 OTel 直方图相当于 Prometheus 的传统直方图。因此,Prometheus 默认将它们转换为传统直方图,但可选地提供直接转换为 NHCBs 的功能。

Pushgateway

原生直方图支持已逐渐添加到 Pushgateway。在 v1.9 中实现了完全支持。Pushgateway 一直以来都基于传统的 protobuf 格式作为其内部数据模型,这使得必要的更改变得容易(主要是 UI 方面的考虑)。可以推送组合直方图(包含传统和原生桶),并通过 /metrics 端点以此形式暴露。(但是,可用于以 JSON 格式查询推送指标的查询 API 将只能返回一种桶,并且如果存在原生桶,则会优先选择原生桶。)

promtool

本节描述为支持原生直方图而添加或更改的 promtool 命令。未明确提及的命令不直接与原生直方图交互,无需更改。

promtool query ... 命令支持原生直方图。请参阅查询 API 文档了解输出格式。新增了一个命令 promtool query analyze,专门用于分析查询 API 返回的传统和原生直方图使用模式。

通过 promtool test rules 进行的规则单元测试支持原生直方图,使用上文描述的格式。

promtool tsdb analyzepromtool 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 添加了对原生直方图的支持。如果暴露的直方图包含至少一个桶跨度,prom2json 将在 JSON 输出中用原生直方图的桶替换通常的传统桶,其格式借鉴了 Prometheus 查询 API

迁移注意事项

从传统直方图迁移到原生直方图时,需要考虑三个重要的问题来源:

  1. 查询原生直方图的方式与查询传统直方图不同。在大多数情况下,更改很小且直接,但存在棘手的边缘情况,这使得可靠的自动转换变得困难。
  2. 传统直方图和原生直方图不能相互聚合。在某个时间点从传统直方图变为原生直方图,使得创建跨越该转换点的仪表盘变得困难,并且包含该转换点的范围向量将不可避免地不完整(即,选择传统直方图的范围向量将只包含范围较早部分的数据点,而选择原生直方图的范围向量将只包含范围较晚部分的数据点)。
  3. 传统直方图可能被定制为在感兴趣的点上精确设置桶边界。具有标准模式的原生直方图可以有很高的分辨率,但不允许在任意值上设置桶边界。在这些情况下,使用原生直方图的用户体验实际上可能会更差。

为解决(3),当然可以不迁移有问题的传统直方图,保持原样。另一种选择是保持检测代码不变,但在采集时将传统直方图转换为 NHCBs。这利用了原生直方图更高的存储性能,但仍然需要以与完全迁移到原生直方图相同的方式解决(1)和(2)(见下文)。

解决(1)和(2)的保守方法是允许一个较长的过渡期,代价是并行收集和存储传统与原生直方图一段时间。

第一步是更新检测代码,以并行暴露传统和原生直方图。(如果计划在检测代码中继续使用传统直方图,并在抓取时将其转换为 NHCBs,则可以跳过此步骤。)

然后配置 Prometheus 同时抓取传统和原生直方图,请参见上文关于同时抓取传统和原生直方图的部分。(如果需要,也激活将传统直方图转换为 NHCB。)

涉及传统直方图的现有查询将继续工作,但从现在开始,用户可以开始使用原生直方图,并开始更改仪表盘、告警、记录规则中的查询。如上所述,重要的是要注意具有较长范围向量的查询,例如 histogram_quantile(0.9, rate(rpc_duration_seconds[1d]))。此查询计算过去一天的第 90 百分位延迟。但是,如果原生直方图的收集时间不足一天,查询将只覆盖那段较短的时间。因此,只有在原生直方图已收集至少 1 天后才应使用该查询。对于一个显示过去一个月每日第 90 百分位延迟的仪表盘,人们很想编写一个能在正确时刻从传统直方图切换到原生直方图的查询。虽然这在原则上是可能的,但很棘手。如果可行,并行收集传统和原生直方图的过渡期可以很长,以最大限度地减少实现棘手切换的必要性。例如,一旦传统和原生直方图已并行收集一个月,任何回顾不超过一个月的仪表盘都可以简单地从传统直方图查询切换到原生直方图查询,而无需考虑正确的切换时机。

一旦确信所有查询都已正确迁移,就配置 Prometheus 只抓取原生直方图(这是“正常”设置)。(也可以在抓取配置中使用重标签规则逐步移除传统直方图。)如果一切仍然正常,就该从检测代码中移除传统直方图了。

Grafana Mimir 文档包含一份详细的迁移指南,其理念与本节描述的相同。

本页内容