原生直方图 [实验性]

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

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

虽然正式规范应在其各自的上下文中进行(例如 OpenMetrics 更改将在通用 OpenMetrics 规范中指定),但本文档的某些部分采取了规范的形式。在这些部分中,关键字“MUST”(必须)、“MUST NOT”(不得)、“REQUIRED”(要求)、“SHALL”(应)、“SHALL NOT”(不应)、“SHOULD”(推荐)、“SHOULD NOT”(不推荐)、“RECOMMENDED”(建议)、“MAY”(可)和“OPTIONAL”(可选)的使用如 RFC 2119 中所述。

本文档仍然包含大量 TODO 项。在大多数情况下,它们不仅指本文档的不完整性,更重要的是指不完整的实现或悬而未决的问题。目前,这本质上是一个活文档,将随着实现和规范的完善而接收更新。

简介

原生直方图的核心思想是将直方图视为 Prometheus 数据模型中的一等公民。将直方图提升为“原生”样本类型是下面列出的关键属性的基本前提,这也解释了选择“原生直方图”这个名称的原因。

在引入原生直方图之前,所有 Prometheus 样本值都是 64 位浮点值(简称 float64float)。这些浮点值可以直接表示 Gauges(仪表)或 Counters(计数器)。Prometheus 指标类型 Summary(摘要)和(经典版本的)Histogram(直方图)在暴露格式中存在时,在摄取时会被分解为浮点组件:两种类型都有一个 Sum(总和)和 Count(计数)组件,Summary 有多个 Quantile(分位数)样本,而(经典)Histogram 有多个 Bucket(桶)样本。

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

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

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

这些关键特性通过标准分桶模式完全实现。还有其他具有不同权衡的模式,可能只包含这些特性的一部分。详情请参见下面的模式部分

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

由于稀疏表示(上述列表中的特性 1)对于原生直方图的许多其他优势至关重要,因此在设计过程早期,稀疏直方图曾是原生直方图的常用名称。然而,其他关键特性,如指数分桶模式或桶的动态性质,也非常重要,但并未完全体现在稀疏直方图这一术语中。

设计文档

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

会议演讲

了解原生直方图的一种更易懂的方式是观看会议演讲,以下是其中一部分。作为入门,可以先观看这些演讲,然后返回本文档以了解所有细节和技术要点。

术语表

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

数据模型

本节描述了原生直方图的通用数据模型。它尽可能避免实现细节,包括术语。例如,本节中描述的列表将在 protobuf 实现中成为repeated message,在 Go 实现中(很可能)成为切片

通用结构

与经典直方图类似,原生直方图有一个用于观察值计数的字段和一个用于观察值总和的字段。此外,它还包含以下组件,这些组件将在下面的专门章节中详细描述:

  • 一个模式,用于识别确定任何给定桶(索引为 i)边界的方法。
  • 索引桶的稀疏表示,针对正负观察值镜像。
  • 一个零桶,用于统计接近零的观察值。
  • 一个(可能为空的)自定义值列表。
  • 范例.

类型

任何原生直方图都在两个独立维度上具有特定类型:

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

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

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

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

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

在计数器直方图中,观察值的总计数和每个桶中的计数行为都类似于 Prometheus 计数器,即它们仅在计数器重置时减少。然而,由于观察到负值,观察值的总和可能会减少。PromQL 实现**必须**基于整个直方图检测计数器重置(详情请参见下面的计数器重置考量部分)。(请注意,这对于经典直方图和摘要的总和组件一直是一个问题。到目前为止的方法是接受在这种情况下计数器重置检测会悄悄失效。幸运的是,负值观察对于 Prometheus 直方图和摘要来说是一个非常罕见的用例。)

模式

模式是一个有符号 8 位整数值(简称:int8)。它定义了桶边界的计算方式。当前有效的值是 -53 以及介于 -4 和 +8 之间(包括 -4 和 +8)的范围。未来可能会添加更多模式。-53 是一种用于所谓自定义桶边界(或简称自定义桶)的模式,而其他模式数字则代表不同的标准指数模式(简称:标准模式)。

标准模式彼此可合并,并**推荐**用于一般用例。更大的模式数字对应更高的分辨率。模式 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 之间的模式数字保留用于未来的标准模式(遵循上述桶边界公式),并且**不得**用于任何其他模式。

对于模式 -53,桶边界通过自定义值明确设置,详情请参见下面的自定义值部分。这将生成一个具有自定义桶边界的原生直方图(或简称自定义桶,通常进一步缩写为 NHCB)。这种直方图可以用于将经典直方图表示为原生直方图。如果标准模式的指数分桶与直方图要表示的分布不匹配,也可以使用它。具有不同自定义桶边界的直方图通常不能相互合并。因此,模式 -53 **只应**在特定用例中作为知情决策使用。

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

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

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

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

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

每个跨度由一对数字组成,一个有符号 32 位整数(简称:int32)称为偏移量,一个无符号 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]]

零桶

精确为零的观察值不符合上述标准模式定义的任何桶。它们在一个称为零桶的专用桶中计数。

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

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

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

零桶的阈值**应**与常规桶的边界重合,这避免了零桶与常规桶部分重叠的复杂情况。但是,如果发生此类重叠,则与零桶重叠的常规桶中计数的观察值**必须**在 [-阈值, +阈值] 区间之外。

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

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

然后将源零桶和现在新阈值内的任何源桶相加,得到新零桶的填充量。

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

自定义值

自定义值列表不用于标准模式。如果需要存储额外数据,非标准模式会以自定义方式使用它。

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

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

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

最后一个自定义值绝不能是+Inf。大于最后一个自定义值的观测值将进入一个上限为+Inf的溢出桶。这个溢出桶的索引等于自定义值列表的长度。

范例

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

作为经典直方图一部分暴露的范例,如果它们具有时间戳,则可被原生直方图使用。

观测值的特殊情况

被检测的代码应避免观测NaN±Inf值,因为它们在直方图的语境中意义有限。然而,这些值仍必须按以下描述进行妥善处理。

观测值之和的计算方式是,按照正常的浮点算术,将观测值添加到观测值之和中。(例如,观测到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。如上所述,Prometheus 已保留更多的模式编号,以在实际需要时扩展其范围。

索引偏移量为一,即Prometheus桶的索引*n*在OTel中对应索引*n-1*。

OTel 对桶采用密集表示而非稀疏表示。可以把OTel看作是“只有一个跨度(span)的Prometheus”。

Prometheus 的*零值桶*在OTel中被称为*零计数*(zero count)。(Prometheus 也使用*零计数*来命名存储零值桶中观测计数的字段)。两者工作方式相同,包括*零阈值*的存在。请注意,如果未给定阈值,OTel 会隐含一个零阈值。

(待办:OTel 规范写道:“当 zero_threshold 未设置或为0时,此桶存储无法使用标准指数公式表示的值以及四舍五入到零的值。”请仔细检查这是否真的会产生相同的行为。如果接近零存在问题,我们可以使Prometheus的规范更精确。如果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).
  // 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可以通过抓取经典直方图来摄入。然而,最终在暴露格式中包含自定义桶可能仍然有用,例如用于联邦(federation),以及未来可能利用自定义值的模式。)

请注意以下几点

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

OpenMetrics

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

由于OpenMetrics的protobuf版本与经典Prometheus protobuf格式相似,为其添加支持相对简单。一份以PR形式提交的提案正在审核中

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

(待办:根据进展更新本节。)

检测库

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

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

如果其他检测库已经支持protobuf暴露,那么为其添加原生直方图支持相对容易。对于纯文本库,基于文本的暴露格式的完成是一个先决条件。(待办:根据需要更新此内容。)

本节不涵盖如何使用单个检测库的详细信息(请参阅上面链接的文档),而是侧重于常见的使用模式,并提供如何在检测库中实现原生直方图支持的通用指南。现有的Go实现用于示例。关于数据模型暴露格式的部分对于检测库的实现高度相关(但本节不再赘述!)。

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

经典直方图的范例通常通过存储和暴露每个桶的最新范例来跟踪。只要定义了经典桶,检测库就可以为同一直方图的原生版本暴露相同的范例,前提是每个范例都具有时间戳。(事实上,即使抓取器只摄入原生版本,它也可以使用直方图经典版本提供的范例,详情请参阅暴露格式部分。)然而,原生直方图可以被分配任意数量的范例,检测库应利用这一自由来满足暴露格式部分中描述的范例最佳实践。

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

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

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

NativeHistogramBucketFactor结果模式
65536-4
256-3
16-2
4-1
20
1.51
1.22
1.13
1.054
1.035
1.026
1.017
1.0058

限制桶数量

原生直方图的桶在首次填充时动态创建。观测值分布意外广泛可能导致桶数量意外增多,需要比预期更多的内存。如果观测值分布可以从外部操纵,这甚至可能被用作通过耗尽程序所有可用内存的DoS攻击载体。因此,检测库应提供一种桶限制策略。根据库的典型用例,它可能默认设置一种策略。(待办:或许我们应该说默认情况下应该设置一个策略。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中都产生搅动(churn),后者尤其成问题。所有这些努力大多是徒劳的,因为涉及直方图的典型查询需要合并许多直方图,在此过程中使用最低共同分辨率,因此用户最终得到的分辨率无论如何都会较低。TSDB可以通过在摄入时限制分辨率来防止搅动(参见下文),但如果无论如何都会在摄入时强制执行一个合理低的S分辨率,那么在检测时就设置此分辨率会更直接。然而,在某些特定情况下,如果在检测时无法假定合理的S分辨率,并且抓取器应在抓取时灵活选择所需的分辨率,那么这种策略可能值得被检测程序内的资源开销。

按标签分区

虽然对具有许多桶的经典直方图按标签进行分区需要谨慎,但对于原生直方图来说情况则更为宽松。对原生直方图进行分区仍然会创建多个独立的直方图。然而,由此产生的已分区直方图通常会比原始未分区直方图填充更少的桶。(例如,如果一个跟踪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格式。只要经典的Prometheus protobuf格式(配置列表中的PrometheusProto)是唯一支持原生直方图的格式,那么特性标志和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仍将确保标准模式不会超出所用存储后端的容量。(待办:这目前意味着模式最多为+8,这也是我们在暴露格式中允许的限制。OTel 允许更高的指数模式,Prometheus 因此也可能允许它们进入摄入路径,但在摄入时将其模式降低到+8,或者降到当前实现所需的任何限制。最终澄清请参阅https://github.com/prometheus/prometheus/issues/14168。)

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

请注意,在检测期间设置的桶因子是一个上限(暴露的桶增长因子 ≤ 配置值),而在抓取配置中设置的桶因子是一个下限(摄入的桶增长因子 ≥ 配置值)。因此,某些限制导致的结果模式略有不同。一些示例

native_histogram_min_bucket_factor结果最大模式
65536-4
256-3
16-2
4-1
20
1.41
1.12
1.093
1.044
1.025
1.016
1.0057
1.0028

关于设置限制的一般考虑:native_histogram_bucket_limit适用于为单个直方图的成本设置硬性限制。native_histogram_min_bucket_factor无法实现同样的目的,因为如果观测值分布足够宽泛,即使分辨率较低,直方图也可以拥有许多桶。native_histogram_min_bucket_factor非常适合避免不必要的整体资源成本。例如,如果当前用例只要求特定的分辨率,为所有直方图设置相应的native_histogram_min_bucket_factor可能会释放足够的资源,以接受少数几个具有广泛观测值分布的直方图上的非常高的桶计数。另一个例子是,某些直方图由于某种原因(可能已经在检测端)具有低分辨率的情况。如果聚合经常包含这些低分辨率直方图,结果将具有相同的低分辨率(详情请参阅下面的PromQL细节)。以更高分辨率存储定期与低分辨率直方图聚合的其他直方图可能用处不大。

同时抓取经典直方图和原生直方图

上所述,由被检测程序暴露的直方图可能同时包含经典直方图和原生直方图,并且某些部分甚至共享(例如观测值的计数和总和)。本节解释了Prometheus将抓取哪些部分,以及如何控制其行为。

在没有--enable-feature=native-histograms标志的情况下,Prometheus在抓取期间将完全忽略原生直方图部分。(待办:一旦特性标志被设为无操作,请更新此内容。)设置该标志后,即使同一直方图同时暴露了两种版本,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与经典直方图一样存在合并能力有限的问题,但它们的存储成本通常要低得多。

TSDB

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

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

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

编码

原生直方图在TSDB中需要两种新的块编码(Go类型chunkenc.Encoding):用于整数直方图的chunkenc.EncHistogram(字符串表示为histogram,数值为2),以及用于浮点直方图的chunkenc.EncFloatHistogram(字符串表示为floathistogram,数值为3)。

类似地,WAL和内存快照(Go类型record.Type)有两种新的记录类型:用于整数直方图的record.HistogramSamples(字符串表示为histogram_samples,数值为9),以及用于浮点直方图的record.FloatHistogramSamples(字符串表示为float_histogram_samples,数值为10)。出于向后兼容性原因,还有两种直方图记录类型:record.HistogramSamplesLegacyhistogram_samples_legacy,7)和record.FloatHistogramSamplesLegacyfloat_histogram_samples_legacy,8)。它们在引入NHCB所需的自定义值之前使用。支持它们是为了仍然能够读取旧的WAL。

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

直方图块使用多种自定义编码来处理数值,以便通过对常见值使用更少比特而不是不常见值来减少数据大小。每种自定义编码的详细信息在低级别块格式文档中(最终也在其中链接的代码中)有描述。以下三种编码用于多个不同字段,因此在此处命名以便后续参考

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

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

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

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

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

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

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

以下事件会触发切分新块(原因在括号中描述)

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

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

过期标记

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

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

块大小限制

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

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

计数器重置考虑事项

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

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

如果时间序列中的仪表直方图后跟计数器直方图,则假定发生了计数器重置,因为从仪表到计数器的变化被认为等同于仪表被删除且计数器从零重新创建。

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

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

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

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

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

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

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

如果以上情况均不属实,则没有计数器重置。

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

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

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

  • GaugeType(位模式 11):该块包含测量值直方图。计数器重置与测量值直方图无关。
  • CounterReset(位模式 10):前一个块的最后一个直方图与当前块的第一个直方图之间发生了计数器重置。(很可能计数器重置实际上是切割新块的原因。)
  • NotCounterReset(位模式 01):前一个块的最后一个直方图与当前块的第一个直方图之间没有发生计数器重置。(这通常发生在由于前一个块达到大小限制而切割新块时。)
  • UnknownCounterReset(位模式 00):前一个块的最后一个直方图与当前块的第一个直方图之间是否发生计数器重置是未知的。

UnknownCounterReset 总是安全的选择。它不会阻止计数器重置检测,但仅仅要求每当需要计数器重置信息时,都必须(重新)执行计数器重置检测过程。

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

对于测量值直方图,CounterResetHint 始终是 GaugeType。任何其他 CounterResetHint 值都意味着相关直方图是计数器直方图。通过这种方式,查询器(包括 PromQL 引擎,参见下文)可以获取直方图是测量值还是计数器类型的信息(这与浮点样本显著不同)。

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

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

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

TODO目前,Prometheus TSDB 无法确保前一个块与摄入时是同一个块。因此,Prometheus 目前对计数器直方图块中的*所有*第一个直方图返回 `UnknownCounterReset`。请参阅跟踪问题以了解相关努力。

如上文已暗示,如果 `CounterResetHint` 设置为 `UnknownCounterReset`,查询器*必须*(重新)执行计数器重置检测过程。

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

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

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

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

创建时间戳处理

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

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

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

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

范例

原生直方图的范例是作为一个整体附加到直方图样本上的,而不是附加到单个桶上。(另请参见暴露格式部分。)因此,一个原生直方图样本可以附加多个范例是允许的(实际上是常见情况)。

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

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

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

PromQL

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

注解

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

整型直方图与浮点型直方图

PromQL 始终对浮点型直方图进行操作。存储为整型直方图的原生直方图在从 TSDB 检索时会自动转换为浮点型直方图。

直方图之间的兼容性

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

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

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

计数器重置

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

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

测量值直方图与计数器直方图

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

桶内插值

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

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

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

混合序列

如上文已讨论,样本类型和原生直方图的类型(flavor)都不是序列标识的一部分。因此,同一个序列可能包含不同样本类型和类型的混合。

计数器直方图和测量值直方图的混合不会阻止任何 PromQL 操作,但如果部分输入样本具有不合适的类型,结果中会返回一个警告级别注解(参见上文)。

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

一元负号和负直方图

一元负号可用于原生直方图。它返回一个直方图,其中所有桶填充计数、观测计数和观测总和的符号都反转了。其他一切保持不变,包括计数器重置提示。但请注意,显式计数器重置检测会被反转的符号扰乱。(TODO:也许我们应该将所有负直方图标记为测量值?)负直方图本身没有实际意义,通常只作为其他表达式中的中间结果。

二元运算符

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

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

加法(+)和减法(-)在两个兼容的直方图之间工作。这些运算符添加或减去所有匹配的桶填充计数以及观测计数和观测总和。缺失的桶被假定为空并相应处理。减法可能导致负直方图,参见上文的说明。通常,两个操作数都应该是测量值。加减计数器直方图需要谨慎,但 PromQL 允许这样做。将测量值直方图和计数器直方图相加会得到一个测量值直方图。将两个计数器直方图与相互矛盾的计数器重置提示相加会触发一个警告级别注解。(TODO:后者尚未实现。此外,减法尚未检查/修改计数器重置提示。这应该在 PromQL 文档中详细说明。)

乘法(*)在浮点样本或标量与直方图之间以任意顺序工作。它将所有桶填充计数、观测计数和观测总和乘以浮点数(样本或标量)。这将导致“缩放”甚至有时为负的直方图,这通常仅作为其他表达式中的中间结果有用(另请参见上文的说明)。乘法适用于计数器直方图和测量值直方图,并且它们各自的类型(flavor)通过此操作保持不变。

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

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

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

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

聚合运算符

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

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

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

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

changes()resets() 函数对原生直方图样本的工作方式与对浮点样本的工作方式相同。它们甚至可以在同一序列中处理浮点样本和直方图样本的混合。在这种情况下,从浮点样本到直方图样本的改变,反之亦然,都算作 `changes()` 的改变,并算作 `resets()` 的重置。从计数器直方图到测量值直方图的类型改变,反之亦然,不计入 `changes()` 的改变。`resets()` *应*仅应用于计数器浮点数和计数器直方图,但该函数仍可用于测量值直方图,在这种情况下会应用显式计数器重置检测。此外,从计数器直方图到测量值直方图的改变,反之亦然,都被算作重置。

histogram_quantile() 函数具有非常特殊的角色,因为它是唯一一个特殊处理特定“魔法”标签(即经典直方图使用的 `le` 标签)的函数。histogram_quantile() 也以类似的方式适用于原生直方图,但没有 `le` 标签的特殊角色。该函数继续以已知方式处理浮点样本,同时对原生直方图样本使用新的“原生”方式。

经典直方图的典型查询示例(包括 `rate` 和聚合):

histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket[10m])))

这是原生直方图对应的查询:

histogram_quantile(0.9, sum by (job) (rate(http_request_duration_seconds[10m])))

与经典直方图一样,可以通过将 1 和 0 分别作为 `histogram_quantile` 的第一个参数来执行直方图中最大和最小观测值的估计。然而,具有标准模式的原生直方图可以提供更有用的结果,不仅因为原生直方图通常具有更高的分辨率,更重要的是因为具有标准模式的原生直方图在整个 float64 数字范围内保持相同的分辨率。对于经典直方图,最大观测值很可能在 +Inf 桶中,因此估计值只返回 +Inf 桶之前的最后一个桶的上限。类似地,最小观测值通常在最低桶中。

histogram_quantile 将值为 NaN 的观测值(不应该发生,参见上文)有效地视为 +Inf 的观测值。这遵循了 NaN 永远不小于 histogram_quantile 返回的任何值的原理。只要结果落在现有桶中,我们就会返回计算结果,就好像 NaN 观测值被观测为 +Inf,并同时发出信息级别注解,让用户知道结果因 NaN 而失真。这与经典直方图通常如何处理 NaN 观测值(在大多数实现中最终落在 +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` 中在给定边界(标量值 `lower` 和 `upper`)之间的估计观测值比例。估计的误差取决于底层原生直方图的分辨率以及给定边界与直方图中桶边界对齐的紧密程度。`+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<=x` 且 `histogram_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()` 以及之前未提及的所有 `_over_time()` 函数,原生直方图样本将从输入范围向量中移除。如果任何序列在范围内包含浮点样本和直方图样本的混合,直方图的移除将通过信息级别注解进行标记。

记录规则

记录规则*可能*产生原生直方图值。它们像正常摄入时一样存储回 TSDB,包括直方图是测量值直方图还是计数器直方图。在后一种情况下,由计数器重置提示明确标记的计数器重置也会被存储,否则在摄入期间会启动新的计数器重置检测。

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

告警规则

告警与原生直方图的配合正常。然而,*建议*避免将原生直方图用作告警的输出值。如果在模板中使用原生直方图样本,它们将以简单的文本形式呈现(由 Go `FloatHistogram.String` 方法生成),这对人类来说很难阅读。

测试框架

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

在单元测试框架中有一个名为 `load_with_nhcb` 的替代 `load` 命令,它将经典直方图转换为 NHCB,并加载经典直方图的浮点序列以及转换后产生的 NHCB 序列。

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

优化

与往常一样,只要行为保持不变,PromQL 实现*可以*应用他们认为合适的任何优化。解码原生直方图可能非常昂贵,因为它可能有许多桶。同样,在 PromQL 引擎中深度复制直方图样本比复制简单的浮点样本昂贵得多。与总是解码所有内容并总是复制所有内容的朴素方法相比,这为优化创造了巨大的潜力。

Prometheus 目前尝试避免不必要的复制(TODO:但仍需实现适当的写时复制(CoW)类似方法,因为它会更清晰,bug 更少),并且在只需要观测总和与计数的特殊情况下跳过桶的解码。

Prometheus 查询 API

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

即时查询和范围查询

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

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

matrix 结果类型在与现有 values 键相同的级别上获得一个新键 histograms。这些键*不*互斥。一个序列可以同时包含浮点值和直方图值,但对于给定的时间戳,只能有一个样本,要么是浮点数,要么是直方图。histograms 键的值结构与 values 键的值类似(一个包含 *n* 个两元素数组的数组),不同之处在于表示浮点样本值的字符串被下面描述的特定直方图对象替换。

请注意,这些键的更好命名应该是 `float`/`histogram` 和 `floats`/`histograms`,因为浮点值和直方图值都是值。当前的命名是历史原因造成的。(过去,只有一种值类型,即浮点数,因此将键简单地命名为 `value` 和 `values` 是显而易见的选择。)这里的意图是不要破坏不了解原生直方图的现有消费者。

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

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

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

<boundary_rule> 占位符是一个介于 0 和 3 之间的整数,具有以下含义:

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

对于标准模式,正桶是“左开”,负桶是“右开”,零桶(具有负左边界和正右边界)是“两边闭合”。对于 NHCBs,所有桶都是“左开”(模仿经典直方图的行为)。未来的模式可能会利用不同的边界规则。

元数据

对于 `series` 端点,包含原生直方图的序列以与仅包含浮点数的常规序列相同的方式包含在内。该端点不提供任何关于包含哪些样本类型的信息(实际上,*任何*序列都可以包含一种或两种样本类型)。特别要注意的是,如果目标以 `request_duration_seconds` 的名称暴露并摄入一个直方图作为原生直方图,它将导致一个名为 `request_duration_seconds` 的序列;但如果它作为经典直方图暴露和摄入,它将导致一组名为 `request_duration_seconds_sum`、`request_duration_seconds_count` 和 `request_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 用户界面

本节描述了 Prometheus 自身用户界面对直方图的渲染。这可作为第三方图形前端的指南。

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

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

使用线性 x 轴时,桶的宽度通常不同。因此,y 轴显示的是桶总体除以其宽度。Prometheus 用户界面不会在 y 轴上渲染数值,因为它们无论如何都很难被人解释。仍可在文本表示中检查总体。

图表视图中,Prometheus 显示了一个热力图(TODO:尚未实现,见下文),它可被视为一系列随时间变化的直方图,旋转 90 度并将桶总体编码为颜色而非条形高度。将计数器类直方图渲染为热力图的典型查询将是 rate 查询。热力图是一种极其强大的表示方式,它允许人们轻松发现分布随时间变化的特征。

TODO热力图尚未实现。相反,UI 只将观测值的总和绘制成常规图表。请参阅跟踪问题。同一问题还讨论了如何在表格视图中处理范围向量的渲染。

模板展开

原生直方图在模板展开中工作。它们以受数学开放和闭合区间表示法启发的文本形式呈现。(这是由 Go 中的 FloatHistogram.String 方法生成的。)由于原生直方图可以有很多桶,并且桶边界往往有很多小数位,因此表示不一定非常易读。请慎重使用原生直方图进行模板展开。

浮点直方图文本表示示例

{count:3493.3, sum:2.349209324e+06, [-22.62741699796952,-16):1000, [-16,-11.31370849898476):123400, [-4,-2.82842712474619):3, [-2.82842712474619,-2):3.1, [-0.01,0.01]:5.5, (0.35355339059327373,0.5]:1, (1,1.414213562373095]:3.3, (1.414213562373095,2]:4.2, (2,2.82842712474619]:0.1}

远程写入与读取

远程写入与读取的 protobuf 规范已扩展,将原生直方图作为实验性功能。不具备处理原生直方图能力的接收方将简单地忽略新添加的字段。然而,Prometheus 必须配置为通过远程写入发送原生直方图(通过将 send_native_histograms 远程写入配置设置为 true)。

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

在发送或接收经典直方图时将其转换为 NHCBs 可能看起来很诱人。然而,这并不能解决经典直方图通过远程写入传输时所面临的已知一致性问题。相反,经典直方图应在抓取时转换为 NHCBs。类似地,显式 OTel 直方图应在OTLP 摄入时就转换为 NHCBs。

TODO远程写入中一个可能存在的问题是,如果原本为同一原生直方图摄入的多个示例在不同的远程写入请求中发送,该如何处理。

联邦

原生直方图的联邦功能按预期工作,前提是联邦抓取使用 protobuf 格式。一旦 OpenMetrics 文本格式支持原生直方图,原则上也可以通过 OpenMetrics 文本格式进行联邦,但出于效率考虑,仍首选通过 protobuf 进行联邦。

TODO澄清 NHCBs 联邦的状态。一旦 OM 支持 NH,将进行更新。

OTLP

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

显式 OTel 直方图等同于 Prometheus 的经典直方图。因此,Prometheus 默认将其转换为经典直方图,但也可以选择直接转换为 NHCBs。

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 添加了对原生直方图的支持。如果暴露中的直方图包含至少一个桶跨度,prom2json 将使用原生直方图的桶替换 JSON 输出中通常的经典桶,其格式受Prometheus 查询 API 启发。

迁移注意事项

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

  1. 查询原生直方图与查询经典直方图的方式不同。在大多数情况下,更改是最小且直接的,但存在棘手的边缘情况,这使得进行可靠的自动转换变得困难。
  2. 经典直方图和原生直方图不能相互聚合。在某个时间点从经典直方图改为原生直方图,这使得创建在过渡点上也能工作的仪表盘变得困难,并且包含过渡点的范围向量将不可避免地不完整(即选择经典直方图的范围向量将只包含范围前半部分的数据点,而选择原生直方图的范围向量将只包含范围后半部分的数据点)。
  3. 经典直方图可能被设计成桶边界恰好位于关注点。具有标准模式的原生直方图可以具有高分辨率,但不能在任意值处设置桶边界。在这些情况下,原生直方图的用户体验实际上可能会更差。

为了解决 (3),当然可以不迁移相关经典直方图,保持现状。另一种选择是保持仪表化不变,但在摄入时将经典直方图转换为 NHCBs。这利用了原生直方图提高的存储性能,但仍然需要像完全迁移到原生直方图一样解决 (1) 和 (2)(参见下一段)。

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

第一步是更新仪表化以并行暴露经典直方图和原生直方图。(如果计划在仪表化中坚持使用经典直方图并仅在抓取期间将其转换为 NHCBs,则可以跳过此步骤。)

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

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

一旦确信所有查询都已正确迁移,请配置 Prometheus 仅抓取原生直方图(这是“正常”设置)。(也可以通过抓取配置中的重命名规则逐步移除经典直方图。)如果一切仍然正常,就可以从仪表化中移除经典直方图了。

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

本页内容