请参与 Prometheus 用户调研(2026 年 3 月版) ,帮助社区确定未来开发工作的优先级!

原生直方图

原生直方图于 2022 年 11 月作为实验性功能引入。它们是一个触及 Prometheus 堆栈几乎所有部分的概念。第一个支持原生直方图的 Prometheus 服务器版本是 v2.40.0。支持功能必须通过特性标志 --enable-feature=native-histograms 启用。从 v3.8.0 开始,原生直方图被支持为稳定功能。然而,抓取原生直方图仍需通过 scrape_native_histograms 配置项显式激活。为了简化从特性标志到配置项的过渡,在 v3.8 中设置该特性标志的唯一作用就是将 scrape_native_histograms 默认设置为 true。从 v3.9 开始,该特性标志将完全无效,必须显式设置 scrape_native_histograms。通过远程写入 (Remote-Write) 发送原生直方图需要通过远程写入配置中的 send_native_histograms 启用。(从 v4 开始,scrape_native_histogramssend_native_histograms 将默认设置为 true。)

由于与原生直方图相关的变更具有广泛性,这些变更的文档及基础概念的解释分散在各个渠道中(如受影响的 Prometheus 组件的文档、源代码中的文档注释、有时是源代码本身、设计文档、会议演讲等)。本文档旨在汇集所有这些信息,并在统一的上下文中简洁地呈现它们。本文档倾向于链接现有的详细文档,而不是重述它们,但它包含足够的信息,无需参考其他来源即可理解。话虽如此,应当注意,本文档既不适合作为初学者的入门指南,也不侧重于开发者的需求。对于前者,计划提供一份更新版的直方图与摘要最佳实践文章。(待办:以及一篇博客文章,甚至可能是一系列文章。)对于后者,有 Carrie Edward 编写的 Prometheus 原生直方图开发者指南 

虽然正式规范应该在其各自的上下文中进行(例如,OpenMetrics 的变更将在通用 OpenMetrics 规范中说明),但本文档的某些部分采取了规范的形式。在这些部分中,关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”的使用方式如 RFC 2119  中所述。

尽管该功能被视为稳定版,且我们预计在 v4.0.0 之前不会有破坏性变更,但本文档仍包含许多待办事项 (TODO)。这些待办事项是用于完善文档、修复小问题及补充额外功能的提醒。

简介

原生直方图的核心思想是将直方图视为 Prometheus 数据模型中的一等公民。将直方图提升为“原生”样本类型是实现下方所列关键属性的基本前提,这也解释了为什么命名为原生直方图

在引入原生直方图之前,所有 Prometheus 样本值都是 64 位浮点值(简称 float64 或仅 float)。这些浮点数可以直接表示仪表 (gauges)计数器 (counters)。在展示格式中存在的 Prometheus 指标类型摘要 (summary) 和(经典版本)直方图 (histogram),在摄取时被分解为浮点组件:对于这两种类型,都有一个总和 (sum) 和一个计数 (count) 组件,对于摘要来说还有若干分位数 (quantile) 样本,对于(经典)直方图来说有若干桶 (bucket) 样本。

通过原生直方图,引入了一种新的结构化样本类型。单个样本表示之前已知的总和计数,加上动态的桶集合。这不仅限于摄取,PromQL 表达式现在也可以返回这种新的样本类型,而以前只能返回浮点样本。

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

  1. 稀疏的桶表示,允许空桶的开销接近于零。
  2. 覆盖完整的 float64 数值范围。
  3. 在埋点期间无需配置桶边界。
  4. 根据简单的配置参数选择动态分辨率。
  5. 复杂的指数分桶方案,确保使用这些方案的所有直方图之间可以合并。
  6. 高效的展示和存储数据表示。

这些关键属性通过标准的分桶方案完全实现。还有其他具有不同权衡的方案可能仅具备这些属性的一部分。有关详细信息,请参阅下方的方案 (Schema) 章节

与先前存在的“经典”直方图相比,原生直方图(采用标准分桶方案)允许在任意观察值范围内以更低的存储和查询成本获得更高的桶分辨率,且几乎不需要配置。即使是通过标签对直方图进行分区,现在也变得更加经济实惠。

由于稀疏表示(上方列表中的属性 1)对于原生直方图的许多其他好处至关重要,因此在设计过程早期,稀疏直方图 (sparse histograms)原生直方图的常用名称。然而,其他关键属性(如指数分桶方案或桶的动态特性)也非常重要,但这些名称并没有涵盖在稀疏直方图一词中。

设计文档

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

会议演讲

了解原生直方图的一种更易于入门的方法是观看会议演讲,以下是精选的一些内容。作为入门,可以先观看这些演讲,然后再回到本文档深入了解所有细节和技术细节。

术语表

  • 原生直方图是本文档所讨论的代表完整直方图的新的复杂样本类型的实例。在上下文足够明确的情况下,它在下文中通常简称为直方图
  • 经典直方图是代表带有固定桶的直方图的旧样本类型的实例,以前仅称为直方图。它在展示格式中以此形式存在,但在摄入 Prometheus 时被分解为若干浮点样本。
  • 稀疏直方图原生直方图的一个较旧且现已弃用的名称。此名称偶尔仍可能出现在旧文档中。稀疏桶 (Sparse buckets) 仍然是原生直方图桶的一个有意义的术语。

数据模型

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

通用结构

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

此外,原生直方图包含以下组件,这些组件在下方的专门章节中进行了详细描述

  • 一种方案 (schema),用于标识确定给定索引为 i 的桶边界的方法。
  • 一种稀疏表示的索引桶,为正观察值和负观察值分别映射。
  • 一个零桶 (zero bucket),用于计数接近零的观察值。
  • 一个(可能为空的)自定义值 (custom values) 列表。
  • 样本 (Exemplars).

变体

任何原生直方图在以下两个独立维度中的每一个上都有特定的变体

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

浮点直方图有时用于“加权”观察的直接埋点,例如计算观察值落入直方图不同桶中的秒数。然而,浮点直方图在 PromQL 中的用例要常见得多。PromQL 通常仅对浮点值进行操作,因此 PromQL 引擎会将从 TSDB 获取的每个直方图首先转换为浮点直方图,而通过记录规则存回 TSDB 的任何直方图都是浮点直方图。如果此类直方图实际上是整数直方图(因为除总和字段外的所有字段的值都可以精确表示为 uint64),TSDB 实现 MAY 将其转换回整数直方图以提高存储效率。(截至 Prometheus v3.00,Prometheus 中的 TSDB 实现尚未使用此选项。)但请注意,应用于计数器直方图的最常见 PromQL 函数是 rate,它通常产生非整数数值,因此记录规则的结果通常无论如何都会是具有非整数值的浮点直方图。

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

将原生直方图显式视为整数直方图与浮点直方图,是与传统简单数值样本处理方式的一个显著偏差,因为简单数值样本为了简洁起见,在整个堆栈中始终被视为浮点数。

对直方图进行更复杂处理的主要原因是可以在基于 protobuf 的展示格式中轻松获得效率增益。Protobuf 对整数使用变长编码 (varint),这减少了小整数值的数据大小,而无需额外的压缩层。这种优势通过 整数桶的差值编码 (delta encoding) 得到了放大,这通常会产生更小的整数值。相比之下,浮点数在 protobuf 中总是需要 8 字节。在实践中,整数直方图中的许多整数将占用 1 字节,大多数占用 2 字节,因此在 protobuf 展示格式中明确存在整数直方图,对于具有许多桶的直方图,直接导致数据大小减少接近 8 倍。这尤为重要,因为绝大多数由埋点目标展示的直方图都是整数直方图。

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

在计数器直方图中,观察的总计数和桶中的计数表现得像 Prometheus 计数器,即它们仅在计数器重置时才会下降。然而,观察的总和可能会因观察到负值而减少。PromQL 实现 MUST 根据整个直方图检测计数器重置(有关详细信息,请参阅下方的 计数器重置考量章节)。(请注意,这对于经典直方图和摘要的总和组件来说一直是个问题。目前为止的方法是接受在这些情况下计数器重置检测会静默失败。幸运的是,负观察值对于 Prometheus 直方图和摘要来说是一个非常罕见的用例。)

方案 (Schema)

方案 (schema) 是一个大小为 8 位的有符号整数值(简称:int8)。它定义了桶边界的计算方式。当前有效的值是 -53 以及 -4 到 +8(包含边界)之间的范围(以及 -9 到 +52 之间的一个更大保留范围,详细信息见下文)。未来可能会添加更多方案。-53 是用于所谓的自定义桶边界或简称自定义桶的方案,而其他方案数字则代表不同的标准指数方案(简称:标准方案)。

标准方案彼此之间是可合并的,并且 RECOMMENDED 用于通用用例。较大的方案数字对应较高的分辨率。方案 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 的上限(包含)。(它可以称为正溢出桶。)
  • 包含 MinFloat64 的负数桶(根据上述边界公式)具有 MinFloat64 的下限(包含),而不是上述公式计算出的限制(这会导致 float64 下溢)。
  • 下一个负数桶(相对于前一项的桶,索引为 i+1)具有 MinFloat64 的上限(排除)和 -Inf 的下限(包含)。(它可以称为负溢出桶。)
  • 不能使用超出上述 +Inf-Inf 桶范围的桶。

对于接近零的值,存在更多例外情况,请参阅下方的 零桶章节

当前最低分辨率 -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 之间的直方图可以转换为有效模式。否则,未知的模式在检索时必须返回错误,并且触发该检索的 PromQL 查询必须失败。

对于模式 -53,桶边界通过自定义值进行显式设置,详见下文的自定义值部分。这将产生一个带有自定义桶边界的原生直方图(简称 NHCB)。此类直方图可用于将经典直方图表示为原生直方图。如果标准模式提供的指数分桶方式不适合直方图所代表的分布,也可以使用它。具有不同自定义桶边界的直方图通常无法相互合并。因此,模式 -53 应仅在特定用例中经过慎重考虑后使用。

桶 (Buckets)

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

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

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

对于整数直方图,列表中的元素是有符号 64 位整数(简称 int64),每个元素表示该桶的统计数量相对于列表中前一个桶的增量。每个列表中的第一个桶包含一个绝对统计数量(也可以视为相对于零的增量)。增量不得导致计算出的绝对桶统计数量为负数。

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

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

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

空跨度(长度为零)是有效的,可以使用,尽管它们通常没有用处,并且应当通过将它们的偏移量加到下一个跨度的偏移量中来消除。同样,非列表第一个跨度的跨度其偏移量可能为零,尽管这些偏移量也应通过将其长度加到前一个跨度中来消除。允许这两种情况是为了让原生直方图的生产者可以根据当前的资源权衡选择任何表示方式。例如,如果直方图经过多个阶段处理,则仅在最后一个处理阶段后消除冗余跨度可能是最有效的。

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

请注意,未来的高分辨率模式可能需要超出 int32 表示范围的偏移量。在这种情况下,将需要对数据模型进行扩展。(当前最高分辨率的标准模式是模式 8,包含 MaxFloat64 的桶索引为 262144,因此 +Inf 溢出桶的索引为 262145,而 int32 可表示的最大数为 2147483647。仍能使用 int32 偏移量的最高标准模式为模式 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]]

零桶 (Zero bucket)

完全为零的观测值不符合上述标准模式定义的任何桶。它们被记录在一个称为零桶的专用桶中。

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

零桶有一个附加参数称为零阈值 (zero threshold),它是一个 float64 ≥ 0。如果阈值设置为零,则只有完全为零的观测值进入零桶,即上述情况。如果阈值具有正值,则闭区间 [-threshold, +threshold] 内的所有观测值都会进入零桶,而不是常规桶。这有两个用例:

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

零桶的阈值应与常规桶的边界重合,这避免了零桶与常规桶部分重叠带来的复杂性。但是,如果确实发生了这种重叠,则与零桶重叠的常规桶中记录的观测值必须位于 [-threshold, +threshold] 区间之外。

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

  • 新阈值与已填充桶的边界重合。
  • 新阈值未位于任何已填充桶内。

然后将源零桶和现在位于新阈值内的所有源桶相加,得出新零桶的统计数量。

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

自定义值 (Custom values)

自定义值列表不用于标准模式。在需要存储额外数据的情况下,它由非标准模式以自定义方式使用。

目前定义的唯一使用自定义值的模式是 -53(自定义桶)。本节剩余部分详细描述了此特定情况下自定义值的用法。

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

下排他边界由上边界之前的自定义值定义。对于第一个自定义值(在列表中的位置零处),没有前值,此时下边界被认为是包含 -Inf 的。因此,索引为零的自定义桶计算所有介于(包含)-Inf 和第一个自定义值之间的观测值。在通常只预期正数观测值的情况下,索引为零的自定义桶应具有为零的上边界,以清晰标记是否存在零或以下的观测值。(如果确实只有正数观测值,索引为零的自定义桶将保持为空,因此永远不会被显式表示。唯一的代价是自定义值列表开头多了一个零元素。)

自定义值不得为 +Inf。大于最后一个自定义值的观测值进入上边界为 +Inf 的溢出桶。该溢出桶的索引等于自定义值列表的长度。因此,经典直方图通常包含的 +Inf 桶的上边界在自定义值中不显式表示。

自定义值不得为 NaN。OpenMetrics 明确排除了这一点,但其他展示格式原则上可能在经典直方图中以 NaN 作为上边界(大概是某些错误的结果——这样的边界没有任何意义)。此类经典直方图必须被拒绝,且不能转换为 NHCB。

Exemplars(范例)

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

如果经典直方图暴露的样本点包含时间戳,则它们可用于原生直方图。

观测值的特殊情况

已埋点的代码应避免观测 NaN±Inf 值,因为它们在直方图的上下文中意义有限。但是,这些值必须仍按如下所述进行适当处理。

观测值的总和按照常规通过将观测值加到观测总和中来计算,遵循正常的浮点算术。(例如,观测到 NaN 会将总和设为 NaN。观测到 +Inf 会将总和设为 +Inf,除非它已经是 NaN-Inf,在这种情况下总和设为 NaN。)

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

观测到 +Inf-Inf 会增加观测次数并按照以下方式增加所选的桶:

  • 使用标准模式时,+Inf 观测值增加上述的正溢出桶
  • 使用标准模式时,-Inf 观测值增加上述的负溢出桶
  • 使用模式 -53(自定义桶)时,+Inf 观测值增加索引等于自定义值列表长度的桶。
  • 使用模式 -53(自定义桶)时,-Inf 观测值增加索引为零的桶。

OpenTelemetry 互操作性

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

Prom 的模式 (schema) 等于 OTel 中的缩放 (scale),限制在于 OTel 允许比 -4 更小和比 +8 更大的值。如上所述,Prom 预留了更多的模式编号以扩展其范围(如有实际需要)。

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

OTel 采用稠密而非稀疏的桶表示。人们可以将 OTel 视为“仅有一个跨度的 Prom”。

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

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

OTel 指数直方图仅支持标准指数分桶模式(顾名思义)。因此,NHCB(或具有未来其他分桶模式的原生直方图)无法直接转换为 OTel 指数直方图。但是,转换为带有固定桶的传统 OTel 直方图仍然是可行的。

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

展示格式

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

相反,遵循上述数据模型的原生直方图包含更多数值数据。这放大了基于 protobuf 格式的优势。因此,之前被废弃的基于 protobuf 的展示被恢复,以高效展示和抓取原生直方图。

经典 Prometheus 格式

在构思原生直方图时,OpenMetrics 的采用率仍然不足,特别是 OpenMetrics 的 protobuf 版本没有任何已知的应用。因此,最初的方法是扩展经典 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).

  // 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 消息用于经典直方图的桶。完全可以创建一个既表示同一个直方图的经典版本又表示原生版本的 Histogram 消息。解析器可以自由选择其中一个或两个版本(另请参阅抓取配置部分)。
  • 桶的统计数量在浮点直方图中编码为绝对数字,而在整数直方图中编码为相对于前一个桶(或第一个桶相对于零)的增量。后者产生的数字更小,因为 protobuf 对 sint64 类型使用 varint 编码,这使得消息体积更小。
  • 一个尚未接收到任何观测值的原生直方图和一个没有配置桶的经典直方图在 protobuf 消息中看起来完全相同。因此,旨在作为原生直方图解析的 Histogram 消息必须在重复的 positive_span 字段中包含一个“空操作跨度 (no-op span)”,即 offsetlength 设置为 0 的 BucketSpan
  • 任何数量的原生直方图样本点都可以添加到 Histogram 消息的重复 Exemplar 字段中,但每一个都必须有一个时间戳。如果未提供此类样本点,解析器可以使用为经典桶提供的带时间戳的样本点(作为 Bucket 消息中 Exemplar 字段中每个桶最多一个样本点)。
  • 原生直方图样本点的数量和分布应适应当前的用例。通常,样本点负载不应比 Histogram 消息的其余部分大得多,并且样本点应落入不同的桶中,并大致均匀地覆盖桶的整个散布范围。(这通常优于成比例代表观测分布的样本点分布,因为后者很少能从分布的长尾中产生样本点,而这些长尾通常是最有趣的样本点。)
  • 没有 NHCB 所需的自定义值的表示。NHCB 从不直接暴露,而是作为经典直方图呈现,以便在摄入时(重新)转换为 NHCB。对于联合 (federation) 也是如此。如果有需要,我们将来可能会增加自定义值的字段,例如为了使用自定义值的未来模式。

OpenMetrics

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

由于与经典 Prometheus protobuf 格式的相似性,为 OpenMetrics 的 protobuf 版本添加支持相对简单。一个以 PR 形式提出的建议 正在审核中。

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

(TODO:随进展更新本节。)

埋点库 (Instrumentation libraries)

Protobuf 规范使得利用 protobuf 编译器创建的特定语言绑定来创建包含原生直方图的指标展示成为可能。但是,对于直接的代码埋点,需要一个埋点库。

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

如果库已经支持 protobuf 展示,那么为其他埋点库添加原生直方图支持相对容易。对于纯文本库,完成基于文本的展示格式是先决条件。(TODO:根据需要更新此项。)

本节不涵盖如何使用各个埋点库的详细信息(参见上述链接的文档),而是侧重于通用使用模式,并为如何在埋点库中实现原生直方图支持提供通用准则。现有的 Go 实现 被用于示例。有关数据模型展示格式的章节对于实现埋点库非常重要(但在本节中不再赘述!)。

直方图的实际埋点 API 对于原生直方图并没有改变。经典直方图和原生直方图接收观测值的方式相同(关于样本点有细微差别,见下一段)。埋点库甚至可以维护同一个直方图的经典和原生版本,并并行展示它们,以便抓取器可以选择摄入哪个版本(详见关于展示格式的部分)。用户通过配置设置选择是否展示经典直方图和/或原生直方图。

经典直方图的样本点通常通过存储和展示每个桶的最新样本点来跟踪。只要定义了经典桶,埋点库就可以为同一个直方图的原生版本展示相同的样本点,只要每个样本点都有一个时间戳。(事实上,抓取器即使在仅摄入原生版本时,也可以使用随直方图经典版本提供的样本点,参见 展示格式 部分中的详细信息。)但是,原生直方图可以被分配任意数量的样本点,埋点库应利用这种自由度来满足 展示格式 部分中描述的样本点最佳实践。

埋点库应针对遵循标准模式的原生直方图提供以下配置参数。名称是 Go 库中的示例,必须根据其他语言的惯用风格进行调整。括号中的值是库应提供的默认值。

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

分辨率通过增长因子设置,而不是直接提供模式,因为大多数用户不会了解模式编号背后的数学原理。对于桶与桶之间增长因子的上限概念,即便不知道原生直方图的内部工作原理也是可以理解的。下表列出了每个有效模式的示例因子。

NativeHistogramBucketFactorresulting 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

限制桶数量 (Limiting the bucket count)

原生直方图的桶是在第一次填充时动态创建的。异常广泛的观测值分布可能导致异常多的桶,从而需要比预期更多的内存。如果观测值的分布可以从外部操纵,这甚至可以通过耗尽程序可用的所有内存来用作 DoS 攻击向量。因此,埋点库应提供桶限制策略。它可以默认设置一个,具体取决于库所使用的典型用例。(TODO:也许我们应该说默认应该设置一个策略。Go 库目前没有默认限制桶,并且到目前为止还没有报告任何相关问题。)

以下描述了 Go 埋点库实现的桶限制策略。其他库可以参考此示例,但根据库的典型使用模式,其他策略也可能是可行的。

该策略由三个参数定义:一个无符号整数 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 免受抖动,但如果无论如何都要在摄入时强制执行合理的分辨率,那么在埋点时就设置好该分辨率会更直接。不过,在无法在埋点时假设合理分辨率的特定情况下,这种策略在埋点程序内部可能值得资源开销,并且抓取器应在抓取时拥有选择所需分辨率的灵活性。

按标签分区 (Partitioning by labels)

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

NHCB

目前(2024-11-03),埋点库没有提供直接配置具有自定义桶边界的原生直方图(NHCB)的方法。NHCB 的用例是允许支持原生直方图的抓取器在摄入时将经典直方图转换为 NHCB(见下一节)。但是,在埋点时直接使用自定义桶是合理的用例。在这些情况下,目前的方法是用经典直方图进行埋点,并配置抓取器在摄入时将其转换为 NHCB。但是,将来可能会在埋点库中对 NHCB 进行更直接的处理。

抓取配置 (Scrape configuration)

要使 Prometheus 服务器能够抓取原生直方图,请在各个抓取配置或全局设置中设置 scrape_native_histograms: true。启用 scrape_native_histograms 还会将内容协商更改为优先选择经典的基于 protobuf 的展示格式,而不是 OpenMetrics 1.x 文本格式。

微调内容协商

可以通过 scrape_protocols 配置设置在全局或每个抓取配置级别对抓取协议协商进行微调。这是一个定义内容协商优先级的列表。它的值取决于启用的特性标志(例如 --enable-feature=created-timestamp-zero-ingestion)、用户直接在其中设置的值,以及最后是否启用了 scrape_native_histograms

如果启用了 scrape_native_histograms,并且没有通过特性标志或用户全局/按配置设置 scrape_protocols,则抓取配置的有效值将更改为 [ PrometheusProto, OpenMetricsText1.0.0, OpenMetricsText0.0.1, PrometheusText0.0.4 ] 以启用抓取原生直方图。

scrape_protocols 设置可用于配置 protobuf 抓取而不摄入原生直方图,或者即使在启用了 scrape_native_histograms 时也为某些目标强制执行非 protobuf 格式。只要经典 Prometheus protobuf 格式(配置列表中的 PrometheusProto)是唯一支持原生直方图的格式,就需要同时启用 scrape_native_histograms 和 protobuf 协商来实际摄入原生直方图。

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

限制桶数量和分辨率

虽然埋点库应提供配置选项来限制原生直方图的分辨率和桶数量,但仍需要在摄入时强制执行这些限制。用户可能无法更改给定程序的埋点,或者程序可能被特意用高分辨率直方图进行埋点,以让不同的抓取器能够按需降低分辨率。

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

  1. native_histogram_bucket_limit 设置单个直方图中桶数的上包含限制。如果超过限制,具有标准模式的直方图的分辨率会反复降低(通过使桶宽度加倍,即降低模式),直到达到限制。如果 NHCB 超过限制,或者在罕见情况下即使使用模式 -4 也无法满足限制,则抓取失败。
  2. native_histogram_min_bucket_factor 设置桶与桶之间增长因子的下包含限制。此设置仅适用于标准模式,对 NHCB 没有影响。同样,如果超过限制,直方图的分辨率会反复降低(通过使桶宽度加倍,即降低模式),直到达到限制。但是,一旦达到模式 -4,即使指定了更高的增长因子,抓取仍将成功。

这两个设置都接受零作为有效值,这意味着“无限制”。对于桶限制,这意味着确实根本不检查桶的数量。对于桶因子,Prometheus 仍将确保标准模式不会超过所用存储后端的容量。Prometheus 目前存储最多 8 级的标准指数模式直方图。但是,它接受大于 8 但最高达到预留限制 52 的指数模式,但在摄入时会降低其分辨率,以便达到模式 8(如果 native_histogram_bucket_limitnative_histogram_min_bucket_factor 设置有要求,则会更低)。

如果两个设置都有非零值,则模式将降低足够多以满足两个限制。

请注意,埋点期间设置的桶因子是上限(暴露的桶增长因子 ≤ 配置值),而抓取配置中设置的桶因子是下限(摄入的桶增长因子 ≥ 配置值)。因此,特定限制导致的模式略有不同。一些示例:

native_histogram_min_bucket_factorresulting max 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 抓取,以及如何控制行为。

如果在抓取配置中 scrape_native_histogramsfalse(v3 中的默认值),Prometheus 将在抓取时完全忽略原生直方图部分。如果 scrape_native_histogramstrue(v4+ 中的默认值),即使两者为同一个直方图同时展示,Prometheus 也会优先选择原生直方图部分而不是经典直方图部分。Prometheus 仍将抓取没有原生直方图数据的直方图的经典直方图部分。

在像迁移场景这样的情况下,可能希望为同一个直方图抓取经典和原生两个版本,前提是这两个版本都由埋点程序展示。要启用此行为,抓取配置中有一个布尔设置 always_scrape_classic_histograms。它默认为 false,但如果设置为 true,只要至少有一个经典桶和至少一个原生桶跨度(这可能是一个空操作跨度),每个直方图的两个版本都将被抓取并摄入。这不会在 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 支持不同桶布局之间的自动协调,但它们的可合并性仍然受到根本限制。协调仅保留相关 NHCB 之间桶边界的精确匹配。如果大多数桶边界匹配,这会产生有用的结果。但是,桶布局的任意更改很容易造成没有任何边界匹配的情况,从而导致只有一个桶(溢出桶)的直方图。

NHCB 的一个主要优势是存储它们通常成本低得多。特别是,添加额外桶的增量成本相对较低,这使得以经济实惠的方式摄入具有许多桶的经典直方图成为可能。

TSDB

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

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

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

编码 (Encoding)

原生直方图在 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.HistogramSamplesLegacyhistogram_samples_legacy,7)和 record.FloatHistogramSamplesLegacyfloat_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)的阈值,使用一种自定义编码,将常见值(零或特定的 2 的幂)编码为仅一个字节,但任意值需要 9 个字节。
  • 模式(schema),以 varbit-int 编码。
  • 正桶跨度(positive spans),编码为跨度数量(varbit-uint),随后是重复序列中每个跨度的长度(varbit-uint)和偏移量(varbit-int)。
  • 负桶跨度(negative spans),编码方式相同。
  • 仅针对 schema -53 (NHCB),包含自定义值,编码为自定义值的数量(varbit-uint),随后是重复序列中的自定义值,使用自定义编码。

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

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

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

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

以下事件会触发切割新的数据块(括号中为原因):

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

跨度差异也会改变数据块布局,但它们通过根据需要添加(显式表示的)未填充桶来协调,以便数据块中的所有直方图共享相同的跨度结构。如果桶消失,处理很简单,因为在将直方图追加到数据块时,缺失的桶仅作为未填充桶添加到新直方图中。然而,之前填充的桶消失构成计数器重置(见下文),因此这种情况仅适用于仪表直方图(仪表直方图无计数器重置)。更常见的情况是,新追加的直方图中存在之前追加的直方图中不存在的桶。在这种情况下,必须将这些桶作为显式的未填充桶添加到所有之前追加的直方图中。这需要对整个数据块进行完全重新编码。(仅重新编码受影响的部分存在一定的优化潜力。实现这一点相当复杂。到目前为止,完全重新编码的性能影响并不明显。)

陈旧标记(Staleness markers)

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

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

数据块大小限制(Chunk size limit)

浮点数据块的大小限制为 1024 字节。同样的尺寸限制通常也适用于直方图数据块。然而,如果单个直方图有很多桶,它们可能会变得非常大,因此盲目执行大小限制可能会导致数据块中仅包含极少数的直方图。(在最极端的情况下,单个直方图甚至可能超过 1024 字节,从而导致根本无法强制执行大小限制。)如果每个数据块中的直方图很少,压缩比就会变差。因此,必须先达到每个数据块至少 10 个直方图,然后才会触发 1024 字节的大小限制。这意味着直方图数据块可以远大于 1024 字节。

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

计数器重置注意事项(Counter reset considerations)

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

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

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

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

如果两个直方图都使用标准模式,但在模式或零桶宽度上有所不同,这些更改可能是兼容分辨率缩减的一部分(这定期发生,以减少直方图的桶数)。对于兼容的分辨率缩减,以下两点均为真:

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

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

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

如果两个直方图都是 NHCBs(模式 -53),则它们自定义值的任何差异将按下文所述进行协调。

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

  • 观测计数的下降(但值得注意的是,不是观测总和的下降)。
  • 任何桶(包括零桶)的填充计数下降。这包括填充桶消失的情况,因为未表示的桶等同于填充为零的桶。

如果上述情况均不成立,则没有计数器重置。

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

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

这反过来意味着数据块中的第一个样本之后永远不会有计数器重置。因此,必须持久化的唯一计数器重置信息是数据块中第一个直方图的信息。这发生在所谓的 histogram flags 中,这是一个存储在数据块中样本数之后的一个字节。该字节目前仅用于计数器重置信息,但将来可能用于其他标志。计数器重置信息使用前两位。四种可能的位模式在 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

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

待办事项目前,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 都将以 CounterResetHintCounterReset 返回给查询器,尽管现在 C 和 D 之间没有计数器重置。类似于上一个示例中的情况,必须在 A 和 B 之间,以及 C 和 D 之间进行新的计数器重置检测。或者,B 和 D 都必须以 CounterResetHintUnknownCounterReset 返回。

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

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

创建时间戳处理(Created timestamp handling)

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

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

创建时间戳对于原生直方图也很有用。正如为浮点计数器插入合成零样本一样,也会为计数器直方图插入零值的直方图样本。直方图的零值没有填充桶,观测总和、观测计数和零桶填充量均为零。模式、零桶宽度、自定义值以及直方图的浮点与整数风格应该与直接跟在合成零样本后面的样本匹配(以免触发虚假计数器重置的检测)。

合成零样本的计数器重置信息始终设置为 CounterReset

Exemplar

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

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

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

只有当摄入直方图的所有样例都排在最后一次成功追加的样例之前时,样例才被视为无序。这不会检测到与较新样例或与最后成功追加的样例的副本混合的无序样例,这被认为是可接受的。

PromQL

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

注释(Annotations)

原生直方图的引入产生了一些 PromQL 表达式返回意外结果的情况,最常见的是输出向量中的某些或所有元素意外缺失的情况。为了帮助用户检测和理解这些情况,对原生直方图执行的操作通常使用注释。注释可以有警告(warn)和信息(info)级别,并描述评估过程中遇到的可能问题。警告级别用于标记最可能是用户必须采取行动的实际问题的情况。信息级别用于也可能是故意的,但仍然足够异常需要标记的情况。

整数直方图与浮点直方图

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

直方图之间的兼容性

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

  • NHCB(模式 -53)仅彼此兼容。不同的自定义值需要通过以下方式转换来协调:
    • 确定所有原始 NHCB 共享的自定义值。这些是新的协调后的自定义值。
    • 通过将其桶合并到新自定义值描述的统一桶集中,将每个原始 NHCB 转换为新的自定义值。
    • 请注意,原始 NHCB 完全有可能不共享任何自定义值。在这种情况下,新的桶集将仅由溢出桶组成,从所有原始桶中获取所有观测值。
    • 任何需要协调自定义值的查询都会被标记为信息级注释。
  • 具有标准模式的直方图始终可以通过降低具有更大模式(即更高分辨率)的直方图的分辨率来转换为最小(即最低分辨率)的公共模式。这通过将相邻桶合并为较小模式的较大桶的常规方式发生。
  • 不同的零桶宽度通过扩展较小的零桶来处理,酌情将任何填充的常规桶合并到扩展的零桶中。如果最大公共宽度恰好位于任何填充桶的中间,它会进一步扩展以与该桶的桶边界重合。(详见上文零桶章节。)

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

计数器重置(Counter resets)

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

请注意,子查询返回的计数器直方图的计数器重置提示不得用于避免显式的计数器重置检测,除非 PromQL 引擎可以安全地检测到子查询返回的连续计数器直方图在 TSDB 中也是连续的。

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

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

桶内插值(Interpolation within a bucket)

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

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

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

混合序列(Mixed series)

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

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

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

一元减运算符和负直方图(Unary minus and negative histograms)

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

通常,具有负桶填充量或观测计数为负的直方图本身并没有真正意义,仅被视为其他表达式内的中间结果。它们在 PromQL 中始终被视为仪表直方图。它们不能作为记录规则的结果持久化。(计算结果为负直方图的规则会导致错误。)不可能在任何交换格式(展示格式、远程写入、OTLP)中表示负直方图。

二元运算符(Binary operators)

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

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

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

乘法 (*) 在浮点样本或标量的一侧与直方图的另一侧之间起作用,顺序不限。它将所有桶填充量以及观测计数和总和乘以该浮点数(样本或标量)。这将导致“缩放”甚至有时是负的直方图,这通常仅作为其他表达式内的中间结果有用(另请参阅上文注释)。乘法适用于计数器直方图和仪表直方图,并且它们的风格不受该操作的影响。

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

相等 (==) 和不相等 (!=) 在两个直方图之间起作用,既可以在过滤版本中,也可以使用 bool 修饰符。它们比较模式、自定义值、零阈值、所有桶填充量以及观测的总和和计数。直方图具有计数器还是仪表风格对于比较是不相关的。(计数器直方图可以等于仪表直方图。)

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

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

聚合运算符(Aggregation operators)

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

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

sum 聚合运算符通过以上述 + 运算符相同的方式对要聚合的直方图进行求和来处理原生直方图。avg 聚合运算符以相同的方式工作,但将总和除以聚合直方图的数量(以与上述 / 运算符相同的方式)。

sumavg 都会从输出向量中移除需要聚合浮点样本和直方图样本的元素。此类移除会由警告级注释标记。

sumavg 都应仅应用于仪表直方图。PromQL 允许聚合计数器直方图(甚至是两者的混合),但这样做需要谨慎才能有意义。仪表与计数器风格以及由此产生的计数器重置提示的影响源自上述 + 运算符的那些含义:

  • 如果所有聚合的直方图共享相同的计数器重置提示,则结果保留该相同的提示。
  • 如果聚合的直方图中至少有一个仪表直方图,则结果是仪表直方图。
  • 在所有其他情况下,结果的计数器重置提示设置为 UnknownCounterReset
  • 无论如何,聚合的直方图中任何直接矛盾的计数器重置提示(即 CounterResetNotCounterReset)都会触发警告级注释。

所有其他聚合运算符处理原生直方图。输入向量中的直方图将被简单忽略,并为每个被忽略的直方图添加信息级注释。

函数

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

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

如上所述,这些函数应应用于仪表直方图或计数器直方图。然而,它们都适用于两种风格,但如果范围向量中包含至少一个风格不合适的直方图,结果中会添加警告级注释。

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

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

通常,这些函数会尝试尽可能将模式转换为公共模式来协调不同模式。然而,如果第一个和第二个样本之间存在计数器重置,则应用于计数器的函数(increase()rate()irate())不会对此类转换执行第一个样本的转换。在这种情况下,第一个样本不包含在计算中,因此第一个样本与其他样本之间的不兼容桶布局被简单地悄悄忽略。

为了防止低于零的外推,应用了与浮点计数器 相同的启发式方法,但仅基于观测计数。因此,在某些情况下,单个桶仍可能被外推到零以下。另一种方法可能是找到最小外推,其中计数和任何桶都不会被外推到零以下。然而,这不一定会导致更好的启发式方法,同时会带来显著的复杂性成本。在范围内第一个样本是源自创建时间戳的合成零样本的常见且重要的情况下,有限的外推实际上会非常精确,因为计数和所有桶在合成样本的时间戳处恰好为零,这也是外推被限制的时间点。请注意,经典直方图独立地将启发式方法应用于每个桶以及计数和总和(因为它们都是独立的序列)。已知这会导致不一致。NHCB 不会重现此问题,并以与其他原生直方图相同的方式工作,这意味着在比较经典直方图和等效 NHCB 时,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 观测值的方式一致(在大多数实现中,它们最终会进入 +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() 函数,原生直方图样本将从输入范围向量中移除。如果任何序列在范围内包含浮点样本和直方图样本的混合,则直方图的移除会由 info 级别的注解进行标记。

记录规则

记录规则可能会产生原生直方图值。它们会像正常摄取时一样被存储回 TSDB 中,包括直方图是量规直方图(gauge histogram)还是计数器直方图(counter histogram)。对于后者,显式标记有计数器重置提示(counter reset hint)的计数器重置也会被存储,否则在摄取期间会启动新的计数器重置检测。

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

警报规则

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

测试框架

PromQL 测试框架已扩展,使得 PromQL 单元测试以及通过 promtool 进行的规则单元测试都可以包含原生直方图。直方图样本的符号表示比较复杂,并在规则单元测试文档中进行了说明。

在单元测试框架中,有一个名为 load_with_nhcb 的替代 load 命令,它将传统直方图转换为 NHCB,并同时加载传统直方图的浮点序列以及转换后得到的 NHCB 序列。

单元测试框架中的 expect 关键字虽然不专用于原生直方图,但在其上下文中非常有用,可以定义关于 info 和 warn 级别注解的预期。

优化

像往常一样,PromQL 实现可以在保持行为一致的前提下进行任何适当的优化。解码原生直方图可能会非常耗费资源,因为可能存在许多桶(buckets)。同样,在 PromQL 引擎中深拷贝直方图样本要比拷贝简单的浮点样本昂贵得多。与总是解码一切并总是拷贝一切的朴素方法相比,这提供了巨大的优化空间。

Prometheus 目前尝试避免不必要的拷贝(TODO:但仍需实现一种适当的写时复制(CoW)方法,因为这样会更整洁且不易出错),并针对只需要观测值的总和(sum)和计数(count)的特殊情况跳过对桶的解码。

Prometheus 查询 API

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

即时查询和范围查询

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

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

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

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

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

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

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

<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 计划总体上改进元数据处理。这些工作也会考虑如何正确支持原生直方图。(TODO:随着进展进行更新。)

Prometheus UI

本节描述了 Prometheus 自带 UI 对直方图的渲染。这可用作第三方绘图前端的指南。

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

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

在线性 x 轴下,桶的宽度通常各不相同。因此,y 轴显示的是桶的总体数量除以其宽度。Prometheus UI 不会在 y 轴上渲染数值,因为这些数值对人类来说很难解释。总体数量仍然可以在文本表示中查看。

图形视图中,Prometheus 显示热力图(TODO:尚未实现,见下文),这可以被视为随时间变化的一系列直方图,旋转 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)。

remote-write v2 中,原生直方图是一项稳定功能。

在发送或接收传统直方图时将其转换为 NHCB 可能看起来很诱人。然而,这并不能克服传统直方图在通过远程写入传输时所遭受的已知一致性问题。相反,传统直方图应该在抓取(scraping)时转换为 NHCB。同样,显式的 OTel 直方图也应该在 OTLP 摄取期间转换为 NHCB。

待办事项远程写入的一个遗留问题是,如果最初为同一个原生直方图摄取的多个样本(exemplars)在不同的远程写入请求中发送,该如何处理。

联邦(Federation)

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

待办事项一旦 OM 支持 NH,请更新此内容。

当通过联邦端点公开时,NHCB 被渲染为传统浮点直方图。抓取者可以选择将它们转回 NHCB 或将它们作为传统直方图摄取。不过,后者可能导致命名冲突。请注意,OpenMetrics v1 不支持传统浮点直方图。幸运的是,Prometheus 联邦无论如何不使用 OpenMetrics v1,而是使用 protobuf 格式或传统文本格式。

OTLP

内置于 Prometheus 中的 OTLP 接收器利用上文描述的兼容性,将传入的 OTel 指数直方图转换为 Prometheus 原生直方图。使用大于 8 的模式(在 OTel 中称为“scale”)的直方图,其分辨率将被降低以匹配模式 8。(在极少数使用小于 -4 的模式的情况下,摄取将会失败。)

显式的 OTel 直方图等同于 Prometheus 的传统直方图。因此,Prometheus 默认将它们转换为传统直方图,但也可以选择直接转换为 NHCB。

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 输出具有专门针对直方图数据块(chunks)的部分。

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 格式,并输出到 stdout。这对于使用处理 JSON 的工具(例如 jq)进行进一步处理非常方便。

prom2json v1.4 添加了对原生直方图的支持。如果公开中的直方图包含至少一个桶跨度(bucket span),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 配置为仅抓取原生直方图(这是“正常”设置)。(也可以通过抓取配置中的重贴标签(relabel)规则增量地移除传统直方图。)如果一切仍然正常工作,是时候从工具化中移除传统直方图了。

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

本页内容