原生直方图

原生直方图于 2022 年 11 月作为实验性功能引入。它们是一个概念,几乎触及 Prometheus 堆栈的每个部分。第一个支持原生直方图的 Prometheus 服务器版本是 v2.40.0。必须通过功能标志 --enable-feature=native-histograms 启用支持。(待办事项:当前版本 v2.55 和 v3.00 仍然如此。一旦稳定版本发布,请更新此部分。)

由于与原生直方图相关的更改具有普遍性,因此这些更改的文档和底层概念的解释广泛分布在各种渠道(例如,受影响的 Prometheus 组件的文档、源代码中的文档注释、有时是源代码本身、设计文档、会议演讲等)。本文档旨在收集所有这些信息,并在统一的上下文中简洁地呈现它们。本文档倾向于链接现有的详细文档,而不是重复说明,但它包含足够的信息,以便在不参考其他来源的情况下也能理解。尽管如此,应该注意的是,本文档既不适合作为初学者的入门指南,也不侧重于开发人员的需求。对于前者,计划提供 关于直方图和摘要的最佳实践文章 的更新版本。(待办事项:以及一篇博客文章,甚至可能是一系列文章。)对于后者,有 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)。这些浮点数可以直接表示 gaugecounter。Prometheus 指标类型 summary 和(经典版本的)histogram,正如它们在暴露格式中存在的那样,在摄取时被分解为浮点组件:两种类型都有一个 sum 和一个 count 组件,summary 有一些 quantile 样本,而(经典)histogram 有一些 bucket 样本。

使用原生直方图,引入了一种新的结构化样本类型。单个样本表示先前已知的 sumcount,以及一组动态的桶。这不仅限于摄取,PromQL 表达式也可能返回新的样本类型,而以前只能返回浮点样本。

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

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

这些关键属性通过标准桶化 Schema 完全实现。还有其他具有不同权衡的 Schema,可能只具有这些属性的子集。有关详细信息,请参阅下面的 Schema 部分

与以前存在的“经典”直方图相比,原生直方图(使用标准桶化 Schema)允许在任意观测值范围内实现更高的桶分辨率,同时存储和查询成本更低,并且几乎不需要配置。甚至按标签分区直方图现在也更加经济实惠。

由于稀疏表示(上面列表中的属性 1)对于原生直方图的许多其他优点至关重要,因此在设计过程的早期,稀疏直方图原生直方图的常用名称。然而,其他关键属性(如指数桶化 Schema 或桶的动态性质)也非常重要,但在术语 稀疏直方图中根本没有体现。

设计文档

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

会议演讲

学习原生直方图的一种更平易近人的方法是观看会议演讲,下面列出了一些精选的演讲。作为入门,观看这些演讲,然后再回到本文档学习所有细节和技术性内容可能更有意义。

术语表

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

数据模型

本节概述了原生直方图的数据模型。它尽可能避免了实现细节。这包括术语。例如,本节中描述的 list 在 protobuf 实现中将成为 repeated message,并且(很可能)在 Go 实现中成为 slice

通用结构

与经典直方图类似,原生直方图具有用于观测计数的字段和用于观测总和的字段。此外,它还包含以下组件,这些组件将在下面的专用部分中详细描述

  • 一个 Schema,用于标识确定具有索引 i 的任何给定桶的边界的方法。
  • 索引桶的稀疏表示,镜像用于正向和负向观测。
  • 一个 零值桶,用于计算接近零的观测值。
  • 一个(可能为空的)自定义值列表。
  • Exemplar.

类型

任何原生直方图在两个独立维度中的每一个维度上都有特定的类型

  1. Counter 与 gauge:通常,直方图是“counter 类型的”,即,它的每个桶都充当观测值的计数器。但是,也有“gauge 类型的”直方图,其中每个桶都是一个 gauge,表示某个时间点的任意分布。gauge 直方图的概念之前由 OpenMetrics 为经典直方图引入。
  2. 整数与浮点数(简称:float):直方图的明显用例是计算观测值,从而在每个桶(包括零值桶)和观测值的总计数中产生 ≥ 0 的整数观测值,表示为无符号 64 位整数(简称:uint64)。但是,有一些特定的用例导致“加权”或“缩放”直方图,其中所有这些值都表示为 64 位浮点数(简称:float64)。请注意,在任何情况下,观测值的总和都是 float64。

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

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

更复杂地处理直方图的主要原因是基于 protobuf 的暴露格式中易于获得的效率提升。Protobuf 对整数使用 varint 编码,这减少了小整数值的数据大小,而无需额外的压缩层。整数桶的 delta 编码放大了这种优势,这通常会导致更小的整数值。相比之下,浮点数在 protobuf 中始终需要 8 个字节。在实践中,整数直方图中的许多整数将适合 1 个字节,而大多数将适合 2 个字节,因此整数直方图在 protobuf 暴露格式中的显式存在直接导致数据大小减少接近 8 倍,对于具有许多桶的直方图而言。这一点尤其重要,因为指标化目标暴露的绝大多数直方图都是整数直方图。

出于类似的原因,整数直方图在 RAM 和磁盘上的表示通常比浮点直方图更有效。但这不如暴露格式中的优势那么重要。首先,Prometheus 对浮点数使用 Gorilla 风格的 XOR 编码,这减小了它们的大小,尽管不如用于整数的双 delta 编码那么多。更重要的是,实现始终可以决定在内部对实际上是整数值的直方图字段使用整数表示(见上文)。(历史记录:Prometheus v1 正是使用这种方法来提高浮点样本的压缩率,而 Prometheus v3 很可能在未来再次采用这种方法。)

在 counter 直方图中,观测值的总计数和桶中的计数分别表现得像 Prometheus counter,即,它们仅在 counter 重置时才减少。但是,观测值的 sum 可能会由于负值的观测而减少。PromQL 实现 MUST 基于整个直方图检测 counter 重置(有关详细信息,请参阅下面的 计数器重置注意事项 部分)。(请注意,这始终是经典直方图和摘要的 sum 组件的问题。到目前为止,该方法是接受在这些情况下 counter 重置检测会为 sum 静默中断。幸运的是,负观测对于 Prometheus 直方图和摘要来说是一个非常罕见的用例。)

Schema

Schema 是一个有符号整数值,大小为 8 位(简称:int8)。它定义了计算桶边界的方式。当前有效值为 -53 以及 -4 到 +8(包括 -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 的最大和最小有限值(在下文中称为 MaxFloat64MinFloat64)以及正负无穷大值(+Inf-Inf

  • 包含 MaxFloat64 的正桶(根据上面的边界公式)的上边界(包含)为 MaxFloat64(而不是上面公式计算的边界,该边界会使 float64 溢出)。
  • 下一个正桶(相对于前一项的桶,索引为 i+1)的下边界(不包含)为 MaxFloat64,上边界(包含)为 +Inf。(它可以称为正溢出桶。)
  • 包含 MinFloat64 的负桶(根据上面的边界公式)的下边界(包含)为 MinFloat64(而不是上面公式计算的边界,该边界会使 float64 下溢)。
  • 下一个负桶(相对于前一项的桶,索引为 i+1)的上边界(不包含)为 MinFloat64,下边界(包含)为 -Inf。(它可以称为负溢出桶。)
  • MUST NOT 使用超出上述 +Inf-Inf 桶的桶。

对于接近零的值,还有更多例外情况,请参阅下面的 零值桶部分

当前最低分辨率为 -4,最高分辨率为 8 的限制是根据实际用途选择的。如果实际需要更低或更高的分辨率,将考虑扩展范围。但是,大于 52 的 Schema 没有意义,因为从一个桶到下一个桶的增长因子将小于可表示 float64 数字之间的差异。同样,小于 -9 的 Schema 也没有意义,因为增长因子将超过可表示为 float64 的最大浮点数。因此,-9 到 +52(包括 -9 和 +52)之间的 Schema 编号保留供将来的标准 Schema 使用(遵循上述桶边界公式),并且 MUST NOT 用于任何其他 Schema。

对于 Schema -53,桶边界通过自定义值显式设置,这在下面的 自定义值部分 中详细描述。这导致生成具有自定义桶边界(或简称自定义桶,通常进一步缩写为 NHCB)的原生直方图。这种直方图可用于将经典直方图表示为原生直方图。如果标准 Schema 的指数桶化与要由直方图表示的分布不匹配,也可以使用它。具有不同自定义桶边界的直方图通常彼此不可合并。因此,Schema -53 SHOULD 仅在特定用例中作为明智的决策使用。

对于标准 Schema,桶表示为两个列表,一个用于正桶,一个用于负桶。对于自定义桶(Schema -53),仅使用正桶列表,但将其重新用于所有桶。

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

对于浮点直方图,列表的元素是 float64,直接表示桶的填充量。

对于整数直方图,列表的元素是有符号 64 位整数(简称:int64),每个元素表示桶的填充量,作为列表中前一个桶的增量。每个列表中的第一个桶包含绝对填充量(也可以看作是相对于零的增量)。

为了将列表中的桶映射到上一节中定义的索引,有两张所谓的 span 列表,一张用于正桶,一张用于负桶。

每个 span 由一对数字组成,一个有符号 32 位整数(简称:int32),称为 offset,和一个无符号 32 位整数(简称:uint32),称为 length。每个列表中的第一个 span 只能具有负 offset。它定义了其对应桶列表中的第一个桶的索引。(请注意,对于 NHCB,索引始终为正数,有关详细信息,请参阅下面的 自定义值部分。)Length 定义了桶列表开始的连续桶的数量。以下 span 的 offset 定义了排除的(以及因此未填充的桶)的数量。Length 定义了列表中排除的桶之后的连续桶的数量。

每个 span 列表中所有 length 值的总和 MUST 等于相应桶列表的长度。

空 span(length 为零)是有效的,并且 MAY 可以使用,尽管它们通常没有用,并且 SHOULD 通过将其 offset 添加到以下 span 的 offset 来消除它们。类似地,列表中不是第一个 span 的 span MAY 具有零 offset,尽管这些 offset SHOULD 通过将其 length 添加到前一个 span 来消除。允许这两种情况是为了使原生直方图的生产者 MAY 选择在当时具有最佳资源权衡的任何表示形式。例如,如果直方图经过各个阶段处理,则可能最有效的方法是在最后一个处理阶段之后才消除冗余 span。

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

请注意,未来高分辨率 Schema 可能需要 offset,这些 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]
  • 正 span 列表:[[-2, 2], [2,1], [1,2]]

如果显式表示索引为 3 的单个未填充桶,则第二个和第三个 span 可以合并为一个,从而导致以下结果

  • 正桶列表:[3, 2, -4, -1, 3, -1]
  • 正 span 列表:[[-2, 2], [2,4]]

或者通过显式表示上面的所有未填充桶,将所有 span 合并为一个

  • 正桶列表:[3, 2, -5, 0, 1, -1, 3, -1]
  • 正 span 列表:[[-2, 8]]

零值桶

精确为零的观测值不适合上述标准 Schema 定义的任何桶。它们在称为零值桶的专用桶中计数。

零值桶中的观测值数量由单个 uint64(对于整数直方图)或 float64(对于浮点直方图)跟踪。

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

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

零值桶的阈值 SHOULD 与常规桶的边界重合,这避免了零值桶与常规桶的部分重叠的复杂性。但是,如果发生这种重叠,则在与零值桶重叠的常规桶中计数的观测值 MUST 在 [-threshold, +threshold] 区间之外。

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

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

然后将源零值桶和现在位于新阈值内的任何源桶相加,以产生新零值桶的填充量。

如果 Schema 为 -53(自定义桶),则不使用零值桶。

自定义值

自定义值列表不用于标准 Schema。如果需要存储其他数据,则非标准 Schema 以自定义方式使用它。

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

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

下边界(不包含)由上边界之前的自定义值定义。对于第一个自定义值(在列表中的位置零处),没有前一个值,在这种情况下,下边界被认为是 -Inf。因此,索引为零的自定义桶计算 -Inf 和第一个自定义值之间的所有观测值。在仅预期正观测值的常见情况下,索引为零的自定义桶 SHOULD 具有零的上边界,以清楚地标记是否在零或以下有任何观测值。(如果确实只有正观测值,则索引为零的自定义桶将保持未填充状态,因此永远不会显式表示。唯一的成本是自定义值列表开头的额外零元素。)

最后一个自定义值 MUST NOT 为 +Inf。大于最后一个自定义值的观测值进入上边界为 +Inf 的溢出桶。此溢出桶以等于自定义值列表长度的索引添加。

Exemplar

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

作为经典直方图一部分公开的范例(Exemplars)如果带有时间戳,则可以被原生直方图使用。

观测值的特殊情况

仪表化代码 SHOULD 避免观测 NaN±Inf 值,因为在直方图上下文中它们意义有限。但是,仍然 MUST 正确处理这些值,如下所述。

观测值总和的计算方式与往常一样,将观测值加到观测值总和中,遵循正常的浮点运算。(例如,观测到 NaN 会将总和设置为 NaN。观测到 +Inf 会将总和设置为 +Inf,除非它已经是 NaN-Inf,在这种情况下,总和将设置为 NaN。)

观测到 NaN 不会进入任何存储桶,但会增加观测计数。这意味着观测计数可能大于所有存储桶(负存储桶、正存储桶和零存储桶)的总和,而差值是 NaN 观测的数量。(对于没有任何 NaN 观测的整数直方图,所有存储桶的总和等于观测计数。在通常的浮点精度限制内,对于没有任何 NaN 观测的浮点直方图也是如此。)

观测到 +Inf-Inf 会增加观测计数,并按以下方式增加选定的存储桶:- 对于标准模式,观测到 +Inf 会增加如上所述的正溢出存储桶。- 对于标准模式,观测到 -Inf 会增加如上所述的负溢出存储桶。- 对于模式 -53(自定义存储桶),观测到 +Inf 会增加索引等于自定义值列表长度的存储桶。- 对于模式 -53(自定义存储桶),观测到 -Inf 会增加索引为零的存储桶。

OpenTelemetry 互操作性

具有标准模式的 Prometheus (Prom) 原生直方图可以轻松映射到 OpenTelemetry (OTel) 指数直方图,反之亦然,详情如下。

Prom 的模式等于 OTel 中的 scale(比例),但 OTel 允许低于 -4 和高于 +8 的值。如上所述,Prom 保留了更多的模式编号以扩展其范围,以备将来在实践中需要。

索引偏移一,即 Prom 索引为 n 的存储桶在 OTel 中的索引为 n-1

OTel 具有存储桶的密集表示而不是稀疏表示。可以将 OTel 视为“仅具有一个 span 的 Prom”。

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

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

OTel 指数直方图仅支持标准指数分桶模式(顾名思义)。因此,NHCB(或具有其他未来分桶模式的原生直方图)无法干净地转换为 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.
}

// [...]

(待办事项:以上内容尚未包含 NHCB 所需的自定义值。我们现在不需要它,因为 NHCB 可以通过抓取经典直方图来摄取。但是,最终在公开格式中包含自定义存储桶可能仍然有用,例如用于联邦,以及用于将来也可能使用自定义值的模式。)

请注意以下几点

  • 原生直方图和经典直方图都由相同的 Histogram proto 消息编码,即现有的 Histogram 消息已扩展了用于原生直方图的字段。
  • 观测总和、观测计数和 created_timestamp 的字段在经典直方图和原生直方图之间共享,并且对于两者都以相同的方式工作。
  • 该格式最初不支持经典浮点直方图。在扩展格式以支持原生直方图时,作为副产品添加了对经典浮点直方图的支持(请参阅字段 sample_count_floatcumulative_count_float)。
  • Bucket 字段和 Bucket 消息用于经典直方图的存储桶。完全可以创建一个 Histogram 消息,该消息同时表示同一直方图的经典版本和原生版本。解析器可以自由选择其中一个或两个版本(另请参见 抓取配置部分)。
  • 在浮点直方图的情况下,存储桶填充被编码为绝对数字,而在整数直方图的情况下,被编码为相对于前一个存储桶(或对于第一个存储桶为零)的增量。后者导致数字更小,从而编码为更小的消息大小,因为 protobuf 对 sint64 类型使用 varint 编码。
  • 尚未接收到任何观测值的原生直方图和未配置任何存储桶的经典直方图在 protobuf 消息中看起来完全相同。因此,旨在解析为原生直方图的 Histogram 消息 MUST 包含一个“no-op span”,即在重复的 positive_span 字段中,offsetlength 设置为 0 的 BucketSpan
  • 原生直方图的任意数量的范例(Exemplars) MAY 添加到 Histogram 消息的重复 Exemplar 字段中,但每个范例 MUST 具有时间戳。如果没有以这种方式提供范例,则解析器 MAY 使用为经典存储桶提供的带时间戳的范例(在 Bucket 消息的 Exemplar 字段中,每个存储桶最多一个范例)。
  • 原生直方图范例的数量和分布 SHOULD 适合手头的用例。通常,范例有效负载 SHOULD 不应比 Histogram 消息的其余部分大很多,并且范例 SHOULD 落入不同的存储桶,并大致均匀地覆盖整个存储桶范围。(通常,这比按比例表示观测分布的范例分布更可取,因为后者很少会产生来自分布长尾的范例,而这些长尾通常是最有趣的范例。)

OpenMetrics

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

由于 OpenMetrics 的 protobuf 版本与经典的 Prometheus protobuf 格式相似,因此向其添加原生直方图支持相对简单。一个 PR 形式的提案 正在审核中。

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

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

仪表化库

protobuf 规范 支持使用 protobuf 编译器创建的特定于语言的绑定来低级别创建包括原生直方图在内的指标公开。但是,对于直接代码仪表化,需要一个仪表化库。

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

如果仪表化库已经支持 protobuf 公开,则向其他仪表化库添加原生直方图支持相对容易。对于纯文本的库,基于文本的公开格式的完成是先决条件。(待办事项:根据需要更新此内容。)

本节不涵盖如何使用各个仪表化库的详细信息(请参阅上面链接的文档),而是侧重于常见的用法模式,并提供有关如何作为仪表化库的一部分实现原生直方图支持的通用指南。已有的 Go 实现 用作示例。数据模型公开格式 部分对于仪表化库的实现高度相关(但本节中未重述!)。

直方图的实际仪表化 API 对于原生直方图没有改变。经典直方图和原生直方图都以相同的方式接收观测值(在范例方面存在细微差异,请参阅下一段)。仪表化库甚至可以维护同一直方图的经典版本和原生版本,并将它们并行公开,以便抓取器可以选择要摄取的版本(有关详细信息,请参阅关于 公开格式 的部分)。用户通过配置设置选择是否公开经典直方图和/或原生直方图。

经典直方图的范例通常通过存储和公开每个存储桶的最新范例来跟踪。只要定义了经典存储桶,仪表化库 MAY 为同一直方图的原生版本公开相同的范例,只要每个范例都具有时间戳。(实际上,即使抓取器仅摄取原生版本,它也 MAY 使用经典直方图版本提供的范例,有关详细信息,请参阅 公开格式 部分。)但是,可以为原生直方图分配任意数量的范例,并且仪表化库 SHOULD 利用此自由度来满足 公开格式 部分中描述的范例最佳实践。

对于遵循标准模式的原生直方图,仪表化库 SHOULD 提供以下配置参数。名称是来自 Go 库的示例 - 它们必须调整为其他语言的惯用风格。括号中的值是库 SHOULD 提供的默认值。

  • NativeHistogramBucketFactor (1.1):一个大于 1 的浮点数,用于确定初始分辨率。库选择一个起始模式,该模式导致存储桶宽度从一个存储桶到下一个存储桶的增长因子不大于提供的值。有关示例值,请参见下表。
  • NativeHistogramZeroThreshold (2-128):一个值大于等于零的浮点数,用于设置零存储桶的初始阈值。

分辨率是通过增长因子而不是直接提供模式来设置的,因为大多数用户将不了解模式编号背后的数学原理。从一个存储桶到下一个存储桶的增长因子的上限的概念是可以理解的,而无需了解原生直方图的内部工作原理。下表列出了每个有效模式的示例因子。

NativeHistogramBucketFactor 结果模式
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 默认设置一个,具体取决于库的典型用例。(待办事项:也许我们应该说默认情况下 SHOULD 设置策略。Go 库目前默认不限制存储桶,到目前为止尚未报告任何问题。)

以下描述了 Go 仪表化库实现的存储桶限制策略。其他库 MAY 遵循此示例,但是,根据库的典型使用模式,其他策略也可能是可行的。

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

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

如果步骤 2 或 3 更改了直方图,则一旦自上次重置以来过去了 NativeHistogramMinResetDuration,将执行重置,不仅要删除存储桶,还要返回零阈值和存储桶分辨率的初始值。请注意,这在所有方面都被视为因其他原因进行的重置,包括更新所谓的 创建时间戳

将非常低的 NativeHistogramBucketFactor(例如 1.005)与合理的 NativeHistogramMaxBucketNumber(例如 160)一起设置是很诱人的。通过这种方式,每个直方图始终具有在给定的存储桶计数“预算”内可承受的最高分辨率。(这是 OTel 指数直方图使用的默认策略。它从更高的模式 (20) 开始,这在 Prometheus 原生直方图中甚至尚不可用。)但是,对于 Prometheus 用例,通常建议使用此策略。分辨率将在创建后以及每次重置后随着观测值的传入而频繁降低。这会在仪表化程序以及 TSDB 中产生动荡,这对后者尤其成问题。所有这些努力大多是徒劳的,因为涉及直方图的典型查询需要合并许多直方图,在此期间使用最低的公共分辨率,因此用户最终仍将获得较低的分辨率。可以通过在摄取时限制分辨率来保护 TSDB 免受动荡的影响(请参阅下面的 限制存储桶计数和分辨率),但是如果无论如何在摄取时强制执行合理低的分辨率,则更直接的方法是在仪表化期间已经设置此分辨率。但是,在无法在仪表化时假定合理分辨率的特定情况下,并且抓取器应具有在抓取时选择所需分辨率的灵活性,则此策略可能值得仪表化程序中的资源开销。

按标签分区

虽然对于具有许多存储桶的经典直方图,按标签进行分区必须谨慎进行,但对于原生直方图,情况要宽松得多。分区原生直方图仍然会创建多个单独的直方图。但是,与原始的未分区直方图相比,生成的分区直方图通常每个填充的存储桶更少。(例如,如果按 HTTP 状态代码对跟踪 HTTP 请求持续时间的直方图进行分区,则跟踪状态代码为 404 响应的请求的单个直方图可能具有非常集中的存储桶分布,围绕识别未知路径所需的典型持续时间,仅填充几个存储桶。)所有分区直方图的总填充存储桶数仍将上升,但比分区直方图的数量小一个数量级。(例如,如果向已经相当重的经典直方图添加标签会导致 100 个带标签的直方图,则总成本将增加 100 倍。在原生直方图的情况下,如果经典直方图具有高分辨率,则单个直方图的成本可能已经较低。分区后,带标签的原生直方图中的总填充存储桶数将显着小于原始原生直方图中存储桶数的 100 倍。)

NHCB

目前(2024-11-03),仪表化库没有提供直接配置具有自定义存储桶边界(NHCB)的原生直方图的方法。NHCB 的用例是允许启用原生直方图的抓取器在摄取时将经典直方图转换为 NHCB(请参阅 下一节)。但是,存在有效的用例,其中直接在仪表化期间需要自定义存储桶。在这些情况下,当前的方法是使用经典直方图进行仪表化,并配置抓取器以在摄取时将其转换为 NHCB。但是,将来可能会在仪表化库中更直接地处理 NHCB。

抓取配置

要使 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 抓取而不摄取原生直方图,或者为某些目标强制使用非 protobuf 格式。只要经典的 Prometheus protobuf 格式(配置列表中的 PrometheusProto)是唯一支持原生直方图的格式,就需要功能标志和 protobuf 协商才能实际摄取原生直方图。

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

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

限制存储桶计数和分辨率

虽然 仪表化库 SHOULD 提供配置选项来限制原生直方图的分辨率和存储桶计数,但在摄取时仍然需要强制执行这些限制。用户可能无法更改给定程序的仪表化,或者程序可能有目的地使用高分辨率直方图进行仪表化,以便为不同的抓取器提供选择降低分辨率的选项。

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

  1. native_histogram_bucket_limit 设置单个直方图中存储桶数量的上限(包含上限)。如果超出限制,则会重复降低具有标准模式的直方图的分辨率(通过将存储桶的宽度加倍,即降低模式),直到达到限制。如果 NHCB 超出限制,或者在即使使用模式 -4 也无法满足限制的罕见情况下,抓取将失败。
  2. 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,则将抓取并摄取每个直方图的两个版本,前提是至少有一个经典存储桶和至少一个原生存储桶 span(可能是 no-op span)。这不会在 TSDB 中引起任何冲突,因为经典直方图被摄取为许多带有后缀的序列,而原生直方图仅被摄取为一个带有其未修改名称的序列。(示例:名为 rpc_latency_seconds 的直方图会生成名为 rpc_latency_seconds 的原生直方图序列,以及经典部分的许多序列,即 rpc_latency_seconds_sumrpc_latency_seconds_count 和许多带有不同 le 标签的 rpc_latency_seconds_bucket 序列。)

将经典直方图作为 NHCB 抓取

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

NHCB 与经典直方图一样,在合并性方面存在相同的问题,但是它们通常存储成本更低。

TSDB

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

整数直方图与浮点直方图

TSDB 以不同的方式存储整数直方图和浮点直方图。通常,整数直方图有望更好地压缩,因此,如果所有存储桶计数和观测计数在 int64 范围内都具有整数值,则 TSDB 实现 MAY 将浮点直方图存储为整数直方图,以便转换为整数直方图可以创建原始浮点直方图的数值精确表示。(请注意,Prometheus TSDB 尚未利用此选项。)

编码

原生直方图需要在 TSDB 中使用两种新的 chunk 编码(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.HistogramSamplesLegacyhistogram_samples_legacy,7)和 record.FloatHistogramSamplesLegacyfloat_histogram_samples_legacy,8)。它们在引入 NHCB 所需的自定义值之前使用。支持它们是为了仍然可以读取旧的 WAL。

Prometheus 仅通过标签来识别时间序列。一个序列中的样本是浮点数(因此是计数器或仪表)还是直方图(无论何种类型)并不影响该序列的身份。因此,一个序列可能包含不同类型和风格的样本混合。时间序列中样本类型的更改在实践中预计非常罕见。它们通常发生在目标 instrumentation 发生更改之后(在极少数情况下,相同的指标名称在更改之前用于仪表浮点数,而在更改之后用于计数器直方图),或者在记录规则更改之后(例如,旧版本的规则创建了一个仪表浮点数,而新版本的规则现在创建了一个仪表直方图,同时保留了其名称)。频繁更改样本类型通常是配置错误的结果(例如,两个不同的记录规则创建了不同的样本类型并馈送到同一个序列)。因此,TSDB 实现必须处理样本类型更改,但可以以相对低效的方式进行处理。当 Prometheus TSDB 遇到无法写入当前使用的数据块(chunk)的样本类型时,它会关闭该数据块并使用适当的编码启动一个新的数据块。(对于每个样本都来回切换样本类型的时间序列,将导致每个样本都有一个新的数据块,这确实非常低效。)

直方图数据块对数值使用多种自定义编码,以便通过使用比不常见的值更少的位来编码常见值,从而减小数据大小。每个自定义编码的详细信息在底层数据块格式文档(以及最终链接到那里的代码)中进行了描述。以下三种编码用于许多不同的字段,因此在此处命名以供后续参考

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

直方图数据块通常以数据块中样本的数量(作为 uint16)开始,后跟一个字节,描述直方图是仪表直方图还是计数器直方图,并为后者提供计数器重置信息。有关详细信息,请参阅下面的相应章节。接下来是所谓的“数据块布局”,其中包含以下信息,由数据块中的所有直方图共享

  • 零 bucket 的阈值,使用自定义编码,该编码使用仅一个字节编码常用值(零或某些 2 的幂),但对于任意值需要 9 字节。
  • schema,编码为 varbit-int。
  • 正跨度(positive spans),编码为跨度数量(varbit-uint),后跟长度(varbit-uint)和每个跨度在重复序列中的偏移量(varbit-int)。
  • 负跨度(negative spans),以相同的方式编码。
  • 仅对于 schema -53 (NHCB),自定义值,编码为自定义值的数量(varbit-uint),后跟重复序列中的自定义值,使用自定义编码。

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

  • 时间戳,编码为 varbit-int,第一个样本使用绝对值,第二个样本使用第一个和第二个样本之间的增量,以及任何后续样本使用“增量的增量”(即,与传统浮点数据块中的时间戳相同的“双增量”编码,只是 varbit-int 编码的位 bucket 划分不同)。
  • 观测计数,第一个样本编码为 varbit-uint,后续样本编码为 varbit-int,使用与时间戳相同的“增量的增量”方法。
  • 零 bucket 填充数,第一个样本编码为 varbit-uint,后续样本编码为 varbit-int,使用与时间戳相同的“增量的增量”方法。
  • 观测总和,第一个样本编码为 float64,后续样本编码为 varbit-xor(当前样本和前一个样本之间进行异或运算)。
  • 正 bucket 的 bucket 填充数,每个 bucket 的填充数都是相对于前一个 bucket 的增量(或第一个 bucket 使用绝对填充数),编码为 varbit-int,使用与时间戳相同的“增量的增量”方法。(换句话说,“双增量”编码应用于已经是自身增量的值,这就是为什么有时将其称为“三增量”编码的原因。)
  • 负 bucket 的 bucket 填充数,以相同的方式编码。

浮点直方图的样本数据具有以下差异

  • 观测计数和零 bucket 填充数现在是浮点数,因此以与观测总和相同的方式编码(第一个样本为 float64,后续样本为 varbit-xor)。
  • bucket 填充数现在不仅是浮点数,而且是绝对填充计数,而不是 bucket 之间的增量。在第一个样本中,所有 bucket 填充数都表示为普通的 float64,而对于所有后续样本,它们都编码为 varbit-xor,对当前样本和前一个样本中相应的 bucket 进行异或运算。

以下事件会触发切割新数据块(括号中描述了原因)

  • 整数直方图和浮点直方图之间样本类型的更改(因为两者完全需要不同的数据块编码)。
  • 仪表直方图和计数器直方图之间样本类型的更改(因为前导字节必须表示不同的类型)。
  • 计数器直方图的计数器重置(要存储在前导字节中作为计数器重置信息,请参阅下面的详细信息)。
  • schema 更改(这意味着我们需要一个新的数据块布局,而一个数据块只能有一个数据块布局)。
  • 零阈值的更改(这会更改数据块布局,请参阅上文)。
  • 自定义值的更改(这会更改数据块布局,请参阅上文)。
  • 停滞标记后跟一个常规样本(这严格来说不需要新的数据块,但可以假设大多数直方图在消失和重新出现时会发生很大变化,因此切割新的数据块是最佳选择)。
  • 超过数据块大小限制(请参阅下面的详细信息)。

跨度的差异也会更改数据块布局,但通过根据需要添加(显式表示的)未填充的 bucket 来协调这些差异,以便数据块中的所有直方图共享相同的跨度结构。如果一个 bucket 消失了,这很简单,因为在将直方图附加到数据块时,只需将缺失的 bucket 作为未填充的 bucket 添加到新直方图中即可。但是,先前填充的 bucket 的消失构成计数器重置(请参阅下面),因此这种情况只能发生在仪表直方图(不具有计数器重置功能)中。更常见的情况是,新附加的直方图中存在 bucket,而先前附加的直方图中不存在这些 bucket。在这种情况下,必须将这些 bucket 作为显式未填充的 bucket 添加到所有先前附加的直方图中。这需要对整个数据块进行完全重新编码。(在仅重新编码受影响的部分方面存在一些优化潜力。实现这一点将非常复杂。到目前为止,完全重新编码的性能影响并不突出为有问题。)

停滞标记

注意: 要理解以下部分,重要的是要回顾停滞标记在 TSDB 中是如何工作的。浮点序列中的停滞标记由可用于表示 NaN 值的许多特定位模式中的一种表示。以下部分将此非常特定的浮点值称为“特殊停滞 NaN 值”。它(几乎可以肯定)永远不会通过通常的算术浮点运算返回,因此与“自然发生的” NaN 值不同,包括观察值的特殊情况中讨论的那些。实际上,特殊停滞 NaN 值在查询 TSDB 时永远不会直接返回,而是在到达调用者之前在内部处理。

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

数据块大小限制

浮点数据块的大小限制为 1024 字节。相同的尺寸限制通常也用于直方图数据块。但是,如果单个直方图有很多 bucket,它们可能会变得非常大,因此盲目地强制执行尺寸限制可能会导致数据块中只有很少的直方图。(在最极端的情况下,单个直方图甚至可能占用超过 1024 字节,因此根本无法强制执行尺寸限制。)每个数据块的直方图数量非常少时,压缩率会变差。因此,在 1024 字节的尺寸限制生效之前,必须达到每个数据块至少 10 个直方图的最小数量。这意味着直方图数据块可能比 1024 字节大得多。

每个数据块至少需要 10 个直方图是一种初步的、非常简单的方法,未来可能会对其进行改进,以找到数据块大小和压缩率之间更好的权衡。

计数器重置注意事项

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

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

如果时间序列中仪表直方图之后是计数器直方图,则假定发生了计数器重置,因为从仪表更改为计数器被认为等同于仪表被删除,计数器从零开始重新创建。

最常见的情况是计数器直方图之后是另一个计数器直方图。在这种情况下,可能的计数器重置通过以下步骤检测

如果两个直方图在 schema 或零 bucket 宽度上有所不同,这些更改可能是兼容分辨率降低的一部分(为了减少直方图的 bucket 计数而定期发生)。对于兼容分辨率降低,以下两点都是正确的

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

如果任何条件不满足,则更改不是兼容分辨率降低。由于这种更改只能通过重置或重新创建直方图来实现,因此它被视为计数器重置,并且检测过程结束。

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

在该过程的这一点上,两个直方图都具有相同的 schema 和零 bucket 宽度,要么是因为从一开始就是这种情况,要么是因为第一个直方图已相应转换。(请注意,NHCB 不使用零 bucket。为了此过程的目的,它们的零 bucket 宽度和填充计数被认为是相等的。)在这种情况下,以下任何一种情况都构成计数器重置

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

如果以上情况均未发生,则不存在计数器重置。

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

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

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

  • GaugeType(位模式 11):数据块包含仪表直方图。计数器重置与仪表直方图无关。
  • CounterReset(位模式 10):在前一个数据块的最后一个直方图和当前数据块的第一个直方图之间发生了计数器重置。(很可能计数器重置实际上是切割新数据块的原因。)
  • NotCounterReset(位模式 01):在前一个数据块的最后一个直方图和当前数据块的第一个直方图之间没有发生计数器重置。(如果切割新数据块是因为前一个数据块达到尺寸限制,则通常会发生这种情况。)
  • UnknownCounterReset(位模式 00):在前一个数据块的最后一个直方图和当前数据块的第一个直方图之间是否发生计数器重置未知。

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

计数器重置信息在查询 TSDB 时会传播给调用者(在 Go 代码中,作为 Go 类型 HistogramFloatHistogramCounterResetHint 类型的字段,使用与上述位模式常量名称相同的枚举常量)。

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

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

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

需要此预防措施是因为有多种方法可以删除或插入数据块(例如,通过 tombstone 删除或为回填添加数据块)。计数器重置虽然归因于一个样本,但实际上发生在标记的样本前一个样本之间。删除前一个样本或在两个样本之间插入另一个样本会使先前执行的计数器重置检测无效。

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

如上文已经暗示的那样,如果 CounterResetHint 设置为 UnknownCounterReset,则查询器必须(再次)执行计数器重置检测过程。

在处理重叠数据块或乱序样本(用于查询或在压缩期间)时,必须格外小心。在这些情况下,可能会发生计数器重置的过度检测和检测不足,以下示例说明了这一点

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

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

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

创建时间戳处理

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

设计文档描述了 Prometheus 如何处理创建时间戳。

创建时间戳对于原生直方图也很有用。与为浮点计数器插入合成零样本的方式相同,为计数器直方图插入直方图样本的零值。直方图的零值没有填充的 bucket,并且观测总和、观测计数和零 bucket 填充数都为零。直方图的 Schema、零 bucket 宽度、自定义值以及浮点与整数风格应该与紧随合成零样本之后的样本匹配(以避免触发虚假的计数器重置检测)。

合成零样本的计数器重置信息始终设置为 CounterReset。(待办事项:目前,Prometheus 可能会为序列的第一个样本将其设置为 UnknownCounterReset,这并没有错,但我认为将其设置为 CounterReset 更有意义。)

Exemplar

原生直方图的 Exemplar 附加到整个直方图样本,而不是附加到单个 bucket。(另请参阅展示格式章节。)因此,允许(事实上是常见情况)单个原生直方图样本附带多个 Exemplar。

Exemplar 可能会或可能不会从一次抓取更改到下一次抓取。抓取器应该检测未更改的 Exemplar,以避免存储许多重复的 Exemplar。但是,考虑到单个样本可能具有许多 Exemplar,其中任何子集都可能是上次抓取的重复 Exemplar,因此重复检测可能很昂贵。TSDB 可以依赖于以下假设:任何新的 Exemplar 都具有比任何先前公开的 Exemplar 更新的时间戳。(请记住,原生直方图的 Exemplar 必须具有时间戳。)然后可以以有效的方式进行重复检测

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

仅当摄取的直方图的所有 Exemplar 都将排在上次成功附加的 Exemplar 之前时,Exemplar 才会被计为乱序。这不会检测与较新的 Exemplar 或上次成功附加的 Exemplar 的重复项混合在一起的乱序 Exemplar,这被认为是可接受的。

PromQL

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

注解

原生直方图的引入创建了某些情况,其中 PromQL 表达式返回意外结果,最常见的情况是输出向量中的某些或所有元素意外丢失。为了帮助用户检测和理解这些情况,作用于原生直方图的操作通常使用注解。注解可以具有 warn 和 info 级别,并描述评估期间可能遇到的问题。Warn 级别用于标记最有可能用户必须采取行动的实际问题的情况。Info 级别用于也可能是故意的,但仍然足够不寻常以标记它们的情况。

整数直方图与浮点直方图

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

直方图之间的兼容性

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

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

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

计数器重置

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

请注意,从子查询返回的计数器直方图的计数器重置提示不得被考虑在内以避免显式的计数器重置检测,除非 PromQL 引擎可以安全地检测到从子查询返回的连续计数器直方图在 TSDB 中也是连续的。(待办事项:尚未实现。)

仪表直方图与计数器直方图

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

bucket 内插值

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

对于 NHCB,PromQL 应用与经典直方图相同的插值方法,以保持结果的一致性。(NHCB 的主要用例是作为经典直方图的直接替代品。)然而,对于标准指数模式,线性插值可能被视为不合适。虽然指数模式主要旨在最小化分位数估计的相对误差,但它们也受益于存储桶的均衡使用,至少在某些观测值范围内是这样。基本假设是,对于大多数实际发生的分布,观测密度在较小的观测值下往往更高。因此,PromQL 对标准模式使用指数外推法,这种方法模拟了以下假设:当模式编号增加一(即分辨率加倍)时,将一个存储桶分成两个平均会在两个新存储桶中看到相似的总体。更详细的解释可以在实现插值方法的 PR 中找到。

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

混合序列

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

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

浮点样本和直方图样本的混合更成问题。许多对范围向量进行操作的函数将从结果中删除输入元素包含浮点数和直方图混合的元素。如果发生这种情况,则会将警告级别的注解添加到结果中。具体示例可以在下文中找到。

一元减号和负直方图

一元减号可以用于原生直方图。它返回一个直方图,其中所有存储桶的 population 以及观测值的计数和总和都将其符号反转。其他一切保持不变,包括计数器重置提示。但是请注意,显式计数器重置检测将被反转的符号抛出。(待办事项:也许我们应该将所有负直方图标记为仪表?)负直方图本身并没有真正的意义,仅应作为其他表达式内部的中间结果。

二元运算符

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

以下描述了实际起作用的所有操作。

加法 (+) 和减法 (-) 在两个兼容的直方图之间起作用。这些运算符添加或减去所有匹配的存储桶 population 以及观测值的计数和总和。缺失的存储桶被假定为空并进行相应的处理。减法可能会导致负直方图,请参阅以上说明。通常,两个操作数都应该是仪表。添加和减去计数器直方图需要谨慎,但 PromQL 允许这样做。添加仪表直方图和计数器直方图会生成仪表直方图。添加两个具有冲突计数器重置提示的计数器直方图会触发警告级别的注解。(待办事项:后者尚未实现。此外,减法尚未检查/修改计数器重置提示。这应在 PromQL 文档中详细记录。)

乘法 (*) 在浮点样本或标量在一侧,直方图在另一侧之间起作用,顺序任意。它将所有存储桶 population 以及观测值的计数和总和乘以浮点数(样本或标量)。这将导致“缩放”甚至有时是负直方图,这通常仅作为其他表达式内部的中间结果有用(另请参阅以上说明)。乘法适用于计数器直方图和仪表直方图,并且它们的功能不受操作的影响。

除法 (/) 在左侧为直方图,右侧为浮点样本或标量之间起作用。它等效于乘以浮点数(样本或标量)的倒数。除以零会导致直方图没有常规存储桶,并且零存储桶 population 以及观测值的计数和总和都设置为 +Inf-InfNaN,具体取决于它们在输入直方图中的值(正数、负数或零/NaN,分别)。

相等 (==) 和不等 (!=) 在两个直方图之间起作用,无论是在其过滤版本中还是在使用 bool 修饰符的情况下。它们比较模式、自定义值、零阈值、所有存储桶 population 以及观测值的总和和计数。直方图是否具有计数器或仪表功能与比较无关。(计数器直方图可能等于仪表直方图。)

逻辑/集合二元运算符 (andorunless) 即使涉及直方图样本也能按预期工作。它们仅检查向量元素的存在,并且不会根据元素的样本类型或功能(浮点数或直方图,计数器或仪表)来更改其行为。

“修剪”运算符 >/</ 是专门为原生直方图引入的。它们仅适用于左侧为直方图,右侧为浮点样本或标量的情况。(它们不适用于两侧都是浮点样本或标量的情况。在这种情况下,会返回信息级别的注解。)这些运算符从直方图中删除大于或小于右侧浮点值的观测值,并返回结果直方图。仅当阈值与存储桶边界重合时,删除才是精确的。否则,必须使用受影响存储桶内的插值,如上文所述。直方图的计数器与仪表功能得以保留。(待办事项:这些运算符尚未实现,并且细节也可能更改,请参阅跟踪问题。)

聚合运算符

以下聚合运算符以相同的方式处理浮点数和直方图样本(原因在括号中说明)

  • 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()) 如果在第一个和第二个样本之间存在计数器重置,则不会对第一个样本执行此转换。在这种情况下,第一个样本不包含在计算中,因此第一个样本与其他样本之间不兼容的存储桶布局将被静默忽略。

待办事项: 防止 低于零的外推 目前尚未针对原生直方图实现(并且实际上可能没有意义)。当将经典直方图与等效的 NHCB 进行比较时,这可能会导致略有不同的结果。

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 存储桶中)相一致。(待办事项:此行为的正确实现仍需要通过测试来验证。)

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

  • 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)。(待办事项:通过测试验证此行为的正确实现。)提供的边界是包含还是排除仅在提供的边界与底层原生直方图中的存储桶边界精确对齐时才相关。在这种情况下,行为取决于直方图模式的精确定义。

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

  • 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 中,包括直方图是仪表直方图还是计数器直方图。在后一种情况下,显式标记计数器重置提示的计数器重置也会存储,而在其他情况下,在采集期间会启动新的计数器重置检测。

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

警报规则

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

测试框架

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

有一个名为 load_with_nhcb 的替代 load 命令,它将经典直方图转换为 NHCB,并加载经典直方图的浮点数系列以及转换产生的 NHCB 系列。

与原生直方图无关,但在其上下文中非常有用的 eval_infoeval_warn 关键字,它们分别期望评估结果中至少包含一个信息级别的注解或至少一个警告级别的注解。目前既无法测试是否存在两种级别的注解,也无法测试特定注解。

优化

与往常一样,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:“两端闭”(两个边界都是包含性的)

对于标准模式,正存储桶是“左开”,负存储桶是“右开”,零存储桶(具有负左边界和正右边界)是“两端闭”。对于 NHCB,所有存储桶都是“左开”(反映了经典直方图的行为)。未来的模式可能会使用不同的边界规则。

元数据

对于 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 轴上的位置由相应存储桶的下限和上限确定。每个条形的面积与相应存储桶的 population 成正比(这是一般呈现直方图的核心原则)。

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

在线性 x 轴上,存储桶通常具有变化的宽度。因此,y 轴显示存储桶 population 除以其宽度。Prometheus UI 不会在 y 轴上呈现值,因为无论如何它们对于人类来说都难以解释。population 仍然可以在文本表示形式中检查。

图形视图中,Prometheus 显示热图(待办事项:尚未实现,请参见下文),这可以看作是随时间推移的一系列直方图,旋转 90 度并将存储桶 population 编码为颜色而不是条形的高度。将类似计数器的直方图呈现为热图的典型查询将是 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 中,原生直方图是一项稳定功能。

将经典直方图转换为 NHCB 并在发送或接收时使用似乎很诱人。然而,这并不能克服经典直方图在通过远程写入传输时已知的一致性问题。相反,经典直方图应该在抓取期间转换为 NHCB。 同样,显式的 OTel 直方图应该OTLP 摄取期间就转换为 NHCB。(TODO:参见 跟踪问题。)

TODO: 远程写入仍然可能存在一个问题:如果最初为同一原生直方图摄取的多个 exemplars 在不同的远程写入请求中发送,该怎么办。

联邦

原生直方图的联邦按预期工作,前提是联邦抓取使用 protobuf 格式。通过 OpenMetrics 文本格式进行联邦原则上是可行的,一旦该格式支持原生直方图,但无论如何出于效率原因,首选通过 protobuf 进行联邦。

TODO: 澄清 NHCB 联邦的状态。一旦 OM 支持 NH,请更新。

OTLP

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

显式 OTel 直方图相当于 Prometheus 的经典直方图。因此,Prometheus 默认将它们转换为经典直方图,但也可选地提供直接转换为 NHCB 的功能。(TODO:尚未实现,请参见 跟踪问题。)

TODO: OTLP 接收器是否有任何文档记录?在此处链接文档。

Pushgateway

原生直方图支持已逐步添加到 Pushgateway 中。完整支持在 v1.9 中实现。Pushgateway 一直以来都基于经典 protobuf 格式作为其内部数据模型,这使得必要的更改变得容易(主要是 UI 方面的问题)。可以推送组合直方图(带有经典和原生桶),并将通过 /metrics 端点公开。(但是,查询 API 可以用来将推送的指标作为 JSON 查询,它将只能返回一种类型的桶,并且如果存在原生桶,将优先选择原生桶。)

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
TODO: 随着进展更新。参见 跟踪问题

prom2json

prom2json 是一个小工具,它抓取 Prometheus /metrics 端点,将指标转换为定制的 JSON 格式,并将其转储到标准输出。这对于使用处理 JSON 的工具(例如 jq)进行进一步处理非常方便。

prom2json v1.4 添加了对原生直方图的支持。如果 exposition 中的直方图包含至少一个桶跨度,prom2json 将使用原生直方图的桶替换 JSON 输出中通常的经典桶,其格式灵感来自 Prometheus 查询 API

迁移注意事项

从经典直方图迁移到原生直方图时,有三个重要的问题来源需要考虑

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

为了解决(3),当然可以不迁移有问题的经典直方图,并保持原样。另一种选择是保持仪表检测不变,但在摄取时将经典直方图转换为 NHCB。这利用了原生直方图更高的存储性能,但仍然需要像完全迁移到原生直方图一样解决(1)和(2)(请参阅接下来的段落)。

解决(1)和(2)的保守方法是允许较长的过渡期,但这会带来在一段时间内并行收集和存储经典直方图和原生直方图的成本。

第一步是更新仪表检测,以并行公开经典直方图和原生直方图。(如果计划坚持在仪表检测中使用经典直方图,而只是在抓取期间将它们转换为 NHCB,则可以跳过此步骤。)

然后配置 Prometheus 以同时抓取经典直方图和原生直方图,请参阅上面关于同时抓取经典直方图和原生直方图的部分。(如果需要,也可以激活将经典直方图转换为 NHCB。)

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

一旦确信所有查询都已正确迁移,请配置 Prometheus 仅抓取原生直方图(这是“正常”设置)。(也可以在抓取配置中使用重新标记规则来逐步删除经典直方图。)如果一切仍然正常工作,则可以从仪表检测中删除经典直方图了。

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

本文档是开源的。请通过提交问题或拉取请求来帮助改进它。