原生直方图
原生直方图于 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 remote write 配置来启用。(从 v4 开始,scrape_native_histograms 和 send_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 之前出现重大更改,但本文档仍包含大量待办事项。这些待办事项是关于完成文档、修复次要问题和附加功能的提醒。
简介
原生直方图的核心思想是将直方图视为 Prometheus 数据模型中的一等公民。将直方图提升为“原生”样本类型是列出的关键属性的根本先决条件,这也解释了“原生直方图”这一名称的选择。
在引入原生直方图之前,所有 Prometheus 样本值都是 64 位浮点值(简称 `float64` 或 `float`)。这些浮点数可以直接表示 `gauges`(仪表)或 `counters`(计数器)。Prometheus 指标类型 `summary`(摘要)和(经典版本)`histogram`(直方图),如在导出格式中所见,在摄取时会分解为浮点组件:`sum`(总和)和 `count`(计数)组件,摘要有若干 `quantile`(分位数)样本,经典直方图有若干 `bucket`(桶)样本。
通过原生直方图,引入了一种新的结构化样本类型。单个样本代表先前已知的 `sum` 和 `count` 以及一组动态的桶。这不仅限于摄取,PromQL 表达式也可以返回新的样本类型,而以前只能返回浮点样本。
原生直方图具有以下关键属性:
- 稀疏的桶表示,允许空桶的成本(接近)为零。
- 覆盖 `float64` 值的整个范围。
- 在检测时无需配置桶边界。
- 根据简单的配置参数选择动态分辨率。
- 精密的指数桶模式,确保所有使用这些模式的直方图之间的可合并性。
- 高效的导出和存储数据表示。
这些关键属性通过标准的桶模式完全实现。还有其他模式具有不同的权衡,可能只具有这些属性的子集。有关详细信息,请参阅下面的“Schema”部分。
与之前存在的“经典”直方图相比,原生直方图(使用标准桶模式)在较低的存储和查询成本下,可以跨任意观测值范围提供更高的桶分辨率,且几乎无需配置。即使按标签划分直方图也变得更加经济高效。
由于稀疏表示(上面列表中的属性 1)对原生直方图的许多其他优点至关重要,“稀疏直方图”是设计早期对“原生直方图”的常用名称。然而,指数桶模式或桶的动态性等其他关键属性也非常重要,但“稀疏直方图”一词并未完全涵盖这些。
设计文档
这些设计文档指导了原生直方图的开发。其中一些细节现在已过时,但它们相当好地描述了底层概念及其演变。
- Prometheus 的稀疏高分辨率直方图 ,这是最初的设计文档。
- Prometheus 稀疏直方图和 PromQL ,这是一个关于原生直方图在 PromQL 中处理方式的探索性文档,而非正式的设计文档。
会议演讲
了解原生直方图的更易于接受的方式是观看会议演讲,下面将介绍其中的一部分。作为介绍,可以先观看这些演讲,然后再返回本文档了解所有细节和技术要点。
- Prometheus 直方图的秘密历史 ,关于经典的直方图以及 Prometheus 为何长期保留它们。
- Prometheus 直方图 – 过去、现在和未来 是关于导致原生直方图的新方法的首次演讲。
- Prometheus 的更好直方图 解释了这些概念为何在实践中有效。
- Prometheus 中的原生直方图 在实际实现之后,展示并解释了原生直方图。
- 原生直方图的 PromQL 解释了原生直方图在 PromQL 中的用法。
- 生产环境中的 Prometheus 原生直方图 提供了性能和资源消耗的分析。
- 在 Prometheus 中使用 OpenTelemetry 的指数直方图 涵盖了与 OpenTelemetry 的互操作性。
术语表
- 原生直方图是本文档所讨论的代表完整直方图的新复杂样本类型的一个实例。在上下文中足够清晰时,下面通常称之为“直方图”。
- 经典直方图是代表具有固定桶的旧样本类型的一个实例,以前仅称为“直方图”。它以这种形式存在于导出格式中,但在摄取到 Prometheus 时会分解为多个浮点样本。
- 稀疏直方图是“原生直方图”的一个较旧、现已弃用的名称。在旧文档中偶尔仍会看到此名称。稀疏桶仍然是原生直方图桶的有意义的术语。
数据模型
本节描述了原生直方图的通用数据模型。它尽可能避免实现细节。这包括术语。例如,本节中描述的“列表”在 protobuf 实现中将成为“repeated message”,在 Go 实现中(很可能)成为“slice”。
通用结构
与经典直方图类似,原生直方图有一个用于观测值 `count`(计数)和一个用于观测值 `sum`(总和)的字段。虽然观测值的计数通常是非负的(唯一例外是PromQL 中的中间结果),但观测值的总和可以取任何 `float64` 值。
此外,原生直方图还包含以下组件,将在下面的专门部分中详细描述:
- 一个用于确定任意给定桶(索引为 `i`)边界的方法的 `schema`(模式)。
- 索引桶的稀疏表示,对正值和负值观测值进行镜像。
- 一个 `zero bucket`(零桶)来计算接近零的观测值。
- 一个(可能是空的)`custom values`(自定义值)列表。
- Exemplars(示例子).
Flavors(类型)
任何原生直方图都有两个独立维度的特定类型:
- Counter(计数器)vs. gauge(仪表):通常,直方图是“计数器类”的,即其每个桶都作为观测值的计数器。但是,也有“仪表类”直方图,其中每个桶都是一个仪表,表示某个时间点的任意分布。仪表直方图的概念之前由OpenMetrics 为经典直方图引入。
- 整数 vs. 浮点数(简称 `float`):直方图的明显用例是计数观测值,导致每个桶中的观测值数量 ≥ 0(包括 `zero bucket`),以及总观测值 `count`(计数),这些都表示为无符号 64 位整数(简称 `uint64`)。然而,存在特定的用例导致“加权”或“缩放”直方图,其中所有这些值都表示为 64 位浮点数(简称 `float64`)。请注意,在这两种情况下,观测值的 `sum` 都是 `float64`。
`Float` 直方图偶尔用于直接检测的“加权”观测值,例如,计算观测值落在直方图不同桶中的秒数。`Float` 直方图更常见的用例是在 PromQL 中。PromQL 通常只处理浮点值,因此 PromQL 引擎首先将从 TSDB 检索到的每个直方图转换为 `float` 直方图,然后任何通过记录规则存储回 TSDB 的直方图都是 `float` 直方图。如果此类直方图实际上是整数直方图(因为所有非 `sum` 字段的值都可以精确地表示为 `uint64`),则 TSDB 实现**可能**将其转换回整数直方图以提高存储效率。(截至 Prometheus v3.00,Prometheus 内的 TSDB 实现并未利用此选项。)但是,请注意,应用于计数器直方图最常见的 PromQL 函数是 `rate`,它通常会产生非整数,因此记录规则的结果通常会是带有非整数值的 `float` 直方图。
PromQL 表达式甚至可能创建“负”直方图(例如,通过将直方图乘以 -1)。这些负直方图仅允许作为中间结果,否则被视为无效。它们不能在任何交换格式(导出格式、远程写入、OTLP)中表示,也不能存储在 TSDB 中。另请参阅关于负直方图的详细部分。
将原生直方图明确视为整数直方图与 `float` 直方图,这与对传统简单数值样本的处理方式有显著不同,后者出于简洁性考虑,在整个堆栈中始终被视为浮点数。
处理直方图更复杂的主要原因是 Protobuf(一种基于 Protobuf 的导出格式)的易于实现的效率提升。Protobuf 对整数使用 varint 编码,这在不需要额外压缩层的情况下减小了小整数值的大小。这一优势通过整数桶的差量编码得到放大,后者通常导致更小的整数值。相比之下,浮点数在 Protobuf 中始终需要 8 字节。实际上,整数直方图中许多整数将适合 1 字节,而大多数将适合 2 字节,因此 Protobuf 导出格式中明确存在的整数直方图直接导致直方图的数据大小减少了近 8 倍(对于具有许多桶的直方图)。这一点尤其相关,因为绝大多数由检测目标导出的直方图都是整数直方图。
出于类似的原因,整数直方图在内存和磁盘上的表示通常比浮点直方图更有效。然而,这一点不如导出格式中的优势那么重要。首先,Prometheus 对浮点数使用 Gorilla 风格的 XOR 编码,这会减小其大小,但不如对整数使用的双差量编码有效。更重要的是,实现可以始终决定在内部为实际上是整数值的直方图字段使用整数表示(见上文)。(历史说明:Prometheus v1 正是采用了这种方法来改进浮点样本的压缩,而 Prometheus v3 可能会在未来再次采用这种方法。)
在计数器直方图中,观测值的总 `count` 和桶中的计数分别表现得像 Prometheus 计数器,即它们在计数器重置时才会下降。但是,观测值的 `sum` 可能因为观测到负值而减少。PromQL 实现**必须**根据整个直方图检测计数器重置(有关详细信息,请参阅下面的计数器重置考虑因素部分)。(请注意,这对于经典直方图和摘要的 `sum` 组件来说一直是个问题。到目前为止的处理方式是接受在这种情况下计数器重置检测会默默地失效。幸运的是,负观测值对于 Prometheus 直方图和摘要来说是一个非常罕见的用例。)
Schema(模式)
`schema` 是一个 8 位有符号整数值(简称 `int8`)。它定义了桶边界的计算方式。当前有效值为 -53 以及 -4 到 +8 之间的范围(包括两端)(预留了 -9 到 +52 之间的更大范围,详情见下文)。将来可能会添加更多模式。-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` 可表示的最大和最小有限值(在以下称为 `MaxFloat64` 和 `MinFloat64`)以及正负无穷大(`+Inf` 和 `-Inf`)的值,上述规则存在例外情况。
- 包含 `MaxFloat64` 的正桶(根据上述边界公式)的上限(包含)为 `MaxFloat64`(而不是上述公式计算的将溢出的值)。
- 下一个正桶(相对于上一项中的桶,索引为 `i`+1)的下限(不包含)为 `MaxFloat64`,上限(包含)为 `+Inf`。(可以称之为**正溢出桶**。)
- 包含 `MinFloat64` 的负桶(根据上述边界公式)的下限(包含)为 `MinFloat64`(而不是上述公式计算的将下溢的值)。
- 下一个负桶(相对于上一项中的桶,索引为 `i`+1)的上限(不包含)为 `MinFloat64`,下限(包含)为 `-Inf`。(可以称之为**负溢出桶**。)
- 上述 `+Inf` 和 `-Inf` 桶之外的桶**不得**使用。
有关接近零的值的更多例外情况,请参阅下面的“零桶”部分。
当前最低分辨率的 -4 和最高分辨率的 8 的限制是基于实际可用性选择的。如果出现需要更低或更高分辨率的实际需求,将考虑扩展该范围。但是,大于 52 的模式没有意义,因为从一个桶到下一个桶的增长因子将小于可表示的 `float64` 数字之间的差异。同样,小于 -9 的模式也没有意义,因为增长因子将超过 `float64` 可表示的最大浮点数。因此,-9 到 +52 之间(包含)的模式编号是为未来的标准模式(遵循上述桶边界公式)保留的,**不得**用于任何其他模式。
原生直方图的接收者**可以**在摄取时通过适当地合并桶来降低模式,从而降低摄取直方图的分辨率。接收者**可以**接受 9 到 52 之间的模式,如果它们在摄取时将模式降低到有效数字(即 -4 到 8 之间),并遵循上述桶边界公式。
如果在此可选的模式转换后,接收者仍然不知道该模式,则有以下选项:
- 如果抓取(包括联合)包含一个或多个具有未知模式的直方图,则整个抓取**必须**失败,遵循 Prometheus 避免不完整抓取的做法。
- 对于任何其他摄取路径(包括重放 WAL/WBL),接收者**可以**忽略具有未知模式的直方图,并且**应该**以合适的方式通知用户此遗漏。
当 TSDB 实现从其永久存储(不包括重放 WAL/WBL)中读取直方图时,也适用类似的指南:9 到 52 之间的模式**可以**转换为有效模式。否则,未知模式在检索时**必须**返回错误,触发检索的 PromQL 查询**必须**失败。
对于模式 -53,桶边界是通过 `custom values`(自定义值)显式设置的,详情请参阅下面的“自定义值”部分。这会生成一个具有自定义桶边界(或简称**自定义桶**,常缩写为 NHCB)的原生直方图。此类直方图可用于将经典直方图表示为原生直方图。如果标准模式提供的指数桶不适合直方图所要表示的分布,也可以使用它。具有不同自定义桶边界的直方图通常不可合并。因此,模式 -53 **应该**仅在特定用例中作为明智的决定使用。
桶
对于标准模式,桶表示为两个列表,一个用于正桶,一个用于负桶。对于自定义桶(模式 -53),只使用正桶列表,但将其重新用于所有桶。
任何未填充的存储桶都可能从列表中排除。(这也是为什么存储桶通常被称为稀疏存储桶的原因。)
对于浮点直方图,列表的元素是 float64,直接表示存储桶的填充。存储桶的填充通常是非负的,唯一的例外是在 PromQL 中的中间结果。
对于整数直方图,列表的元素是带符号的 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 (对于浮点直方图) 跟踪。与常规存储桶一样,这个数字通常是非负的。
零存储桶有一个附加参数,称为零阈值,它是一个 ≥ 0 的 float64。如果将阈值设置为零,则只有精确为零的观测值进入零存储桶,这就是上述情况。如果阈值为正值,则 [-阈值, +阈值] 闭区间内的所有观测值将进入零存储桶,而不是常规存储桶。这有两个用例
- 接近零的噪声观测值往往会填充大量存储桶。这些观测值可能是由于数值不准确,或者观测值的来源是实际物理测量。带有相对较小阈值的零存储桶会将这些观测值重定向到一个存储桶。
- 如果用户更关注分布的长尾,远离零,则零存储桶相对较大的阈值有助于避免为不感兴趣的范围内的大量高分辨率存储桶。
零存储桶的阈值应与常规存储桶的边界重合,这避免了零存储桶与常规存储桶部分重叠的复杂性。但是,如果发生此类重叠,在零存储桶的 [-阈值, +阈值] 区间之外的观测值将被计入与零存储桶重叠的常规存储桶。
为了合并具有相同零阈值的直方图,两个零存储桶直接相加。然而,如果源直方图中的零阈值不同,则选择任何源直方图中最大的阈值。如果该阈值恰好存在于其他源直方图的任何填充存储桶中,则将增加阈值,直到对每个源直方图满足以下任一条件
- 新阈值与填充存储桶的边界重合。
- 新阈值不在任何填充存储桶内。
然后将源零存储桶和现在位于新阈值内的任何源存储桶相加,以得到新零存储桶的填充值。
如果模式为 -53 (自定义存储桶),则不使用零存储桶。
自定义值
自定义值列表对于标准模式是未使用的。它被非标准模式以自定义方式使用,以防需要存储额外数据。
目前定义的唯一使用自定义值的模式是 -53 (自定义存储桶)。本节的其余部分将更详细地描述在此特定情况下自定义值的用法。
自定义值表示自定义存储桶的包含上限。它们按升序排序。自定义存储桶本身使用正存储桶列表和正跨度列表进行存储,尽管它们的边界 (通过自定义值确定) 可能是负数。每个“正”存储桶的索引定义了其上限在自定义值列表中的零基位置。
下界(不包含)由紧随上限的自定义值定义。对于第一个自定义值 (列表中的位置为零),没有前一个值,在这种情况下,下界被认为是包含性的 -Inf。因此,索引为零的自定义存储桶统计从 (包括) -Inf 到第一个自定义值之间的所有观测值。在通常只期望正观测值的情况下,索引为零的自定义存储桶的上限应为零,以清楚地标记是否有任何观测值在零或以下。(如果确实只有正观测值,则索引为零的自定义存储桶将保持未填充状态,因此不会被显式表示。唯一的成本是自定义值列表开头的额外零元素。)
自定义值不得为 +Inf。大于最后一个自定义值的观测值进入一个上限为 +Inf 的溢出存储桶。此溢出存储桶以等于自定义值列表长度的索引添加。因此,经典直方图中通常包含的 +Inf 存储桶的上限未在自定义值中显式表示。
自定义值不得为 NaN。这在 OpenMetrics 中明确排除,但其他暴露格式原则上可以为经典直方图提供 NaN 的上限 (可能是由于某种错误——这种边界没有意义)。此类经典直方图必须被拒绝,并且不能转换为 NHCB。
Exemplars(范例)
原生直方图样本可以有一个、零或多个示例。它们的工作方式与常规示例相同,但它们被组织成一个列表 (因为可以有多个),并且它们必须具有时间戳。
作为经典直方图一部分暴露的示例可以被原生直方图使用,如果它们具有时间戳。
观测值的特殊情况
仪器化代码应避免观察 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 中的刻度,但 OTel 允许低于 -4 和高于 +8 的值。如上所述,Prom 保留了更多模式数字,以便在实际需要时扩展其范围。
索引偏移 1,即 Prom 存储桶索引为 n 的对应 OTel 索引为 n-1。
OTel 具有密集而不是稀疏的存储桶表示。人们可能会将 OTel 视为“只有一个跨度的 Prom”。
Prom零存储桶在 OTel 中称为零计数。(Prom 还使用零计数来命名存储零存储桶中观测值计数的字段)。两者功能相同,包括零阈值的存在。请注意,如果未给出零阈值,OTel 则默认为零阈值。
(待办: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 部分)。
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.
}
// [...]
请注意以下几点
- 原生直方图和经典直方图都由相同的
Histogramproto 消息编码,即现有的Histogram消息已扩展了用于原生直方图的字段。 - 用于计算总和、观测值计数和
created_timestamp的字段在经典直方图和原生直方图之间共享,并且两者的工作方式相同。 - 该格式最初不支持经典浮点直方图。在扩展该格式以支持原生直方图的同时,作为副产品也添加了对经典浮点直方图的支持 (请参阅字段
sample_count_float、cumulative_count_float)。 Bucket字段和Bucket消息用于经典直方图的存储桶。完全有可能创建一个Histogram消息,同时表示同一直方图的经典版本和原生版本。解析器可以自由选择其中一个或两个版本 (另请参阅 抓取配置 部分)。- 在浮点直方图的情况下,存储桶填充以绝对值编码;在整数直方图的情况下,则以相对于前一个存储桶 (或第一个存储桶相对于零) 的增量编码。后者会导致较小的数字,因为 protobuf 对
sint64类型使用 varint 编码,所以编码后的消息尺寸更小。 - 尚未接收到任何观测值的原生直方图,以及没有配置存储桶的经典直方图,在 protobuf 消息中看起来完全相同。因此,意图被解析为原生直方图的
Histogram消息必须在重复的positive_span字段中包含一个“无操作跨度”,即BucketSpan,其offset和length设置为 0。 - 在
Histogram消息的重复Exemplar字段中,可以添加任意数量的原生直方图示例,但每个示例都必须具有时间戳。如果此处未提供任何示例,解析器可以使用为经典存储桶提供的带时间戳的示例 (因为Bucket消息的Exemplar字段中每个存储桶最多有一个示例)。 - 原生直方图示例的数量和分布应符合当前用例。通常,示例负载的尺寸不应远大于
Histogram消息的其余部分,并且示例应落在不同的存储桶中,并大致均匀地覆盖所有存储桶。(这通常优于比例表示观测值分布的示例分布,因为后者很少能产生分布长尾中的示例,而这些长尾通常是最有趣的示例。) - NHCB 所需的自定义值没有表示。NHCB 从不直接暴露,而是呈现为经典直方图,在摄取时 (重新) 转换为 NHCB。对于联合也是如此。如果将来需要,例如对于也使用自定义值的模式,我们仍可能添加用于自定义值的字段。
OpenMetrics
目前 (2024-11-03),OpenMetrics 不支持原生直方图。
由于其与经典 Prometheus protobuf 格式的相似性,为 OpenMetrics 的 protobuf 版本添加支持相对简单。一个PR 形式的提案 正在审议中。
为 OpenMetrics 的文本版本添加支持比较困难,但也是非常理想的,因为在许多情况下生成 protobuf 是不可行的。文本格式必须在人类可读性和机器高效处理 (编码、传输、解码) 之间进行权衡。相关工作正在进行中。有关更多详细信息,请参阅 设计文档 。
(待办:根据进展更新部分。)
仪器化库
protobuf 规范支持使用特定语言的绑定低级别创建包括原生直方图在内的指标暴露,这些绑定由 protobuf 编译器创建。但是,对于直接的代码仪器化,需要一个仪器化库。
目前 (2024-11-03),有两个官方 Prometheus 仪器化库支持原生直方图
为其他仪器化库添加原生直方图支持相对容易,前提是该库已支持 protobuf 暴露。对于纯文本库,完成一个基于文本的暴露格式是先决条件。(待办:根据需要更新此项。)
本节不涵盖如何使用各个仪器化库的细节 (请参阅上面链接的文档),而是侧重于通用用法模式,并提供有关如何作为仪器化库一部分实现原生直方图支持的通用指南。现有Go 实现 用于举例。关于数据模型和暴露格式的部分对于仪器化库的实现非常重要 (但本节不再重复!)。
直方图的实际仪器化 API 对于原生直方图没有改变。经典直方图和原生直方图都以相同的方式接收观测值 (在示例方面有细微差别,请参见下一段)。仪器化库甚至可以维护同一直方图的经典版本和原生版本,并并行暴露它们,以便抓取器可以选择要摄取的版本 (有关详细信息,请参阅暴露格式部分)。用户通过配置设置选择是公开经典直方图、原生直方图还是两者都公开。
经典直方图的样本通常通过存储和暴露每个桶(bucket)的最新样本来跟踪。只要定义了经典桶,仪表盘库MAY为同一直方图的本地版本暴露相同的样本,前提是每个样本都有一个时间戳。(实际上,即使刮取器(scraper)目前只摄取本地版本,它也可以使用经典直方图版本提供的样本,请参阅暴露格式部分的详细信息。)然而,一个本地直方图MAY被赋予任意数量的样本,并且仪表盘库SHOULD利用这种自由来满足暴露格式部分所描述的样本最佳实践。
仪表盘库SHOULD为遵循标准模式(schema)的本地直方图提供以下配置参数。名称是Go库的示例——它们需要根据其他语言的惯用风格进行调整。括号中的值是库SHOULD提供的默认值。
NativeHistogramBucketFactor(1.1): 一个大于一的浮点数,用于确定初始分辨率。库会选择一个起始模式,使得从一个桶到下一个桶的桶宽增长因子不超过提供的值。有关示例值,请参见下表。NativeHistogramZeroThreshold(2-128): 一个值为零或大于零的浮点数,用于设置零桶(zero bucket)的初始阈值。
分辨率是通过增长因子而不是直接提供模式来设置的,因为大多数用户不知道模式数字背后的数学原理。桶到桶的增长因子的上限的概念,在不知道本地直方图内部工作原理的情况下也是可以理解的。下表列出了每个有效模式的示例因子。
NativeHistogramBucketFactor | resulting schema |
|---|---|
| 65536 | -4 |
| 256 | -3 |
| 16 | -2 |
| 4 | -1 |
| 2 | 0 |
| 1.5 | 1 |
| 1.2 | 2 |
| 1.1 | 3 |
| 1.05 | 4 |
| 1.03 | 5 |
| 1.02 | 6 |
| 1.01 | 7 |
| 1.005 | 8 |
限制桶的数量
本地直方图的桶是在首次填充时动态创建的。观察值分布异常宽泛可能导致桶数量异常高,需要比预期更多的内存。如果观察值的分布可以从外部操纵,这甚至可能成为一种通过耗尽程序可用内存的DoS攻击向量。因此,仪表盘库SHOULD提供一个桶限制策略。它MAY默认设置一个,具体取决于库的典型用例。(待办:也许我们应该说一个策略 SHOULD 默认设置。Go库目前不默认限制桶,并且到目前为止还没有收到相关问题。)
以下描述了Go仪表盘库实现的桶限制策略。其他库MAY遵循此示例,但其他策略也可能可行,具体取决于库的典型使用模式。
该策略由三个参数定义:一个无符号整数 NativeHistogramMaxBucketNumber,一个持续时间 NativeHistogramMinResetDuration,以及一个浮点数 NativeHistogramMaxZeroThreshold。如果 NativeHistogramMaxBucketNumber 为零(这是默认值),则不对桶进行限制,并且另外两个参数将被忽略。如果 NativeHistogramMaxBucketNumber 被设置为一个正值,库会尝试将每个直方图的桶数保持在提供的值。限制的典型值是160,这与OTel指数直方图在类似策略中使用的默认值相同。(请注意,按标签分区将创建多个直方图。限制分别适用于每个直方图,而不是所有直方图的总和。)如果超出限制,会按顺序应用一系列补救措施,直到桶数量再次在限制范围内。
- 如果自上次重置(包括创建直方图)以来至少经过了
NativeHistogramMinResetDuration时间,则会重置整个直方图,即删除所有桶,并将观察值的总和、计数以及零桶重置为零。Prometheus将此视为正常计数器重置,这意味着在两次刮取之间会丢失一些观察值,因此重置应该比刮取间隔发生得更频繁。(请参阅TSDB部分了解详情)。NativeHistogramMinResetDuration设置为一小时是一个在大多数情况下都能良好工作的数值。 - 如果没有自上次重置以来经过足够的时间(或者如果
NativeHistogramMinResetDuration设置为零,这是默认值),则不会执行重置。相反,零阈值会增加,将靠近零的桶合并到零桶中,从而减少桶的数量。阈值的增加受NativeHistogramMaxZeroThreshold的限制。如果已达到此值(或者它被设置为零,这是默认值),则在此步骤中什么也不做。 - 如果桶的数量仍然超过限制,则通过转换为下一个较低的模式(即合并相邻的桶,从而使桶的宽度加倍)来降低直方图的分辨率。这会一直重复,直到桶数量在配置的限制范围内,或者达到模式 -4。
如果步骤2或3改变了直方图,一旦自上次重置以来经过了 NativeHistogramMinResetDuration 时间,就会执行重置,这不仅是为了删除桶,也是为了将零阈值和桶分辨率恢复到初始值。请注意,这在所有方面都被视为出于其他原因的重置,包括更新所谓的创建时间戳。
设置一个非常低的 NativeHistogramBucketFactor(例如1.005)并结合一个合理的 NativeHistogramMaxBucketNumber(例如160)是诱人的。这样,每个直方图总是有在给定桶数“预算”内可负担的最高分辨率。(这是OTel指数直方图使用的默认策略。它从一个更高的模式(20)开始,这个模式目前在Prometheus本地直方图中甚至不可用。)然而,对于Prometheus用例,该策略通常**不**被推荐。分辨率在创建后以及每次重置后,随着观察值的进入,会经常降低。这会在仪器化程序和TSDB中产生大量变化,这对后者尤其成问题。所有这些努力大部分都是徒劳的,因为涉及直方图的典型查询需要合并许多直方图,在此过程中使用最低的共同分辨率,所以用户最终只会得到一个较低的分辨率。TSDB可以通过限制摄取时的分辨率来防止这种变化(参见下面的内容),但如果摄取时仍然会强制执行一个合理的分辨率,那么在仪器化时已经设置这个分辨率会更直接。然而,在某些特定情况下,如果仪器化时无法假定合理的分辨率,并且刮取器应该有灵活性在刮取时选择所需分辨率,那么这种策略可能值得仪器化程序内的资源开销。
按标签分区
虽然对具有许多桶的经典直方图按标签进行分区需要谨慎,但对于本地直方图的情况则更为宽松。对本地直方图进行分区仍然会创建多个单独的直方图。然而,由此产生的分区直方图通常每个填充的桶数会比原始未分区直方图少。(例如,如果一个跟踪HTTP请求持续时间的直方图按HTTP状态码分区,那么跟踪由状态码404响应的请求的单个直方图可能在识别未知路径的典型持续时间周围有一个非常尖锐的桶分布,只填充几个桶。)所有分区直方图填充的总桶数仍然会增加,但增加的因子小于分区直方图的数量。(例如,如果向一个已经相当重的经典直方图添加标签导致100个带标签的直方图,总成本将增加100倍。对于本地直方图,如果经典直方图具有高分辨率,单个直方图的成本可能已经较低。分区后,带标签的本地直方图中填充的总桶数将远小于原始本地直方图桶数的100倍。)
NHCB
目前(2024-11-03),仪表盘库没有直接用自定义桶边界(NHCBs)配置本地直方图的方法。NHCBs的用例是允许启用本地直方图的刮取器在摄取时将经典直方图转换为NHCBs(参见下一节)。然而,在某些情况下,在仪器化过程中直接使用自定义桶是可取的。在这些情况下,当前的方法是使用经典直方图进行仪器化,并将刮取器配置为在摄取时将其转换为NHCB。然而,未来仪表盘库中对NHCBs的更直接处理可能会发生。
刮取配置
要使Prometheus服务器能够刮取本地直方图,请在单个刮取配置或全局设置中设置 scrape_native_histograms: true。启用 scrape_native_histograms 也会改变内容协商,优先于OpenMetrics 1.x文本格式选择经典protobuf格式。
微调内容协商
可以通过 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功能标志的文档。
限制桶的数量和分辨率
虽然仪表盘库SHOULD提供配置选项来限制本地直方图的分辨率和桶数量,但仍然需要强制执行这些限制。用户可能无法更改给定程序的仪器化,或者程序可能被故意以高分辨率直方图进行仪器化,以便不同的刮取器可以根据需要选择降低分辨率。
Prometheus刮取配置提供了两个设置来解决这个问题
native_histogram_bucket_limit设置为单个直方图中桶数量的上限(包含)。如果超出限制,具有标准模式的直方图的分辨率会重复降低(通过使桶宽度加倍,即降低模式),直到达到限制。如果NHCB超出限制,或在罕见情况下即使达到模式-4也无法满足限制,则刮取失败。native_histogram_min_bucket_factor设置为桶到桶增长因子的下限(包含)。此设置仅对标准模式相关,对NHCBs无效。同样,如果超出限制,直方图的分辨率会重复降低(通过使桶宽度加倍,即降低模式),直到达到限制。然而,一旦达到模式-4,刮取仍然会成功,即使指定了更高的增长因子。
这两个设置都接受零作为有效值,表示“无限制”。在桶限制的情况下,这意味着实际上不会检查桶的数量。在桶因子的情况下,Prometheus仍然会确保标准模式不会超过所用存储后端的容量。Prometheus目前以最多8个的标准指数模式存储直方图。然而,它接受大于8的指数模式(直到保留的52限制),但在摄取时会降低其分辨率,以便达到模式8(或者如果 native_histogram_bucket_limit 或 native_histogram_min_bucket_factor 设置需要,则达到更低的模式)。
如果两个设置都有非零值,则模式会足够降低以满足两个限制。
请注意,在仪器化期间设置的桶因子是上限(暴露的桶增长因子≤配置值),而刮取配置中设置的桶因子是下限(摄取的桶增长因子≥配置值)。因此,由此产生的模式略有不同。一些例子
native_histogram_min_bucket_factor | resulting max schema |
|---|---|
| 65536 | -4 |
| 256 | -3 |
| 16 | -2 |
| 4 | -1 |
| 2 | 0 |
| 1.4 | 1 |
| 1.1 | 2 |
| 1.09 | 3 |
| 1.04 | 4 |
| 1.02 | 5 |
| 1.01 | 6 |
| 1.005 | 7 |
| 1.002 | 8 |
关于设置限制的一般考虑: native_histogram_bucket_limit 适合为单个直方图的成本设置硬限制。 native_histogram_min_bucket_factor 无法达到同样的目的,因为即使分辨率很低,直方图也可能有很多桶,如果观察值的分布足够宽的话。 native_histogram_min_bucket_factor 非常适合避免不必要的整体资源成本。例如,如果当前用例只需要特定分辨率,为所有直方图设置相应的 native_histogram_min_bucket_factor 可能会释放足够的资源来接受少数具有宽泛观察值分布的直方图的高桶数。另一个例子是某些直方图由于某种原因(可能是在仪器化端)具有低分辨率的情况。如果聚合经常包含这些低分辨率直方图,结果将具有相同的低分辨率(参见下面的PromQL细节)。将其他高分辨率的直方图与低分辨率直方图定期聚合存储可能没什么用处。
同时刮取经典和本地直方图
如上文所述,由仪器化程序暴露的直方图可能同时包含经典直方图和本地直方图,甚至某些部分是共享的(例如观察值的计数和总和)。本节将解释Prometheus将刮取哪些部分,以及如何控制行为。
如果在刮取配置中 scrape_native_histograms 设置为 false(v3中的默认值),Prometheus将在刮取时完全忽略本地直方图部分。如果 scrape_native_histograms 设置为 true(v4+中的默认值),Prometheus将优先选择本地直方图部分而非经典直方图部分,即使两者都为同一直方图暴露。对于没有本地直方图数据的直方图,Prometheus仍会刮取经典直方图部分。
在迁移场景等情况下,可能希望为同一个直方图刮取两个版本(经典和本地),前提是仪器化程序暴露了两个版本。要启用此行为,刮取配置中有一个布尔值设置 always_scrape_classic_histograms。它默认为false,但如果设置为true,则将刮取并摄取每个直方图的两个版本,前提是至少有一个经典桶和一个本地桶跨度(可能是一个空跨度)。这不会在TSDB中引起任何冲突,因为经典直方图被摄取为一系列带后缀的系列,而本地直方图只作为单个系列被摄取,其名称未修改。(例如:一个名为 rpc_latency_seconds 的直方图将生成一个名为 rpc_latency_seconds 的本地直方图系列,以及一系列经典部分系列,即 rpc_latency_seconds_sum、rpc_latency_seconds_count,以及一系列具有不同 le 标签的 rpc_latency_seconds_bucket 系列。)
将经典直方图刮取为NHCB
前面提到的NHCB能够将经典直方图建模为本地直方图。通过布尔值刮取配置选项 convert_classic_histograms_to_nhcb,可以将Prometheus配置为将经典直方图摄取为NHCB。
虽然NHCB支持不同桶布局之间的自动对齐,但它们的合并性在根本上仍然是有限的。对齐只保留涉及NHCB之间桶边界的精确匹配。如果大多数桶边界匹配,这会产生有用的结果。然而,桶布局的任意更改很容易导致一个情况,即没有边界匹配,从而导致一个只有一个桶(溢出桶)的直方图。
NHCB的一个关键优势是它们通常存储成本要低得多。特别是,添加额外桶的增量成本相对较低,这使得低成本地摄取具有大量桶的经典直方图成为可能。
TSDB
注意本节提供了在TSDB中存储本地直方图的高层概述,并解释了一些可能容易被忽略的重要个别方面。它无意解释实现细节、定义磁盘格式或指导代码库。有各种存储格式的详细文档 ,当然还有通常生成的GoDoc,以及tsdb包 和storage包 作为合适的起点。一个有用的资源是前面提到的Prometheus本地直方图开发者指南 。
整数直方图与浮点直方图
TSDB以不同的方式存储整数直方图和浮点直方图。一般来说,整数直方图的压缩效果更好,所以TSDB实现MAY将浮点直方图存储为整数直方图,如果所有桶计数和观察计数都是int64范围内的整数值,这样转换为整数直方图就会创建原始浮点直方图的数值精确表示。(请注意,Prometheus TSDB尚未利用此选项。)
编码
本地直方图在TSDB中需要两种新的块编码(Go类型 chunkenc.Encoding): chunkenc.EncHistogram(字符串表示 histogram,数值为2)用于整数直方图,以及 chunkenc.EncFloatHistogram(字符串表示 floathistogram,数值为3)用于浮点直方图。
类似地,WAL和内存快照(Go类型 record.Type)有两种新的记录类型: record.HistogramSamples(字符串表示 histogram_samples,数值为9)用于整数直方图,以及 record.FloatHistogramSamples(字符串表示 float_histogram_samples,数值为10)用于浮点直方图。出于向后兼容的原因,还有两种额外的直方图记录类型: record.HistogramSamplesLegacy(histogram_samples_legacy,7)和 record.FloatHistogramSamplesLegacy(float_histogram_samples_legacy,8)。它们在引入NHCB所需的自定义值之前使用。支持它们是为了能够读取旧的WAL。
Prometheus 仅通过标签来识别时间序列。系列中的样本是浮点数(因此是计数器或仪表)还是直方图(无论哪种类型)都不会影响系列本身的身份。因此,一个系列可能包含不同类型和口味的样本混合。在实践中,时间序列中样本类型的变化预计会非常罕见。它们通常发生在目标仪表发生变化后(在极少数情况下,在更改之前使用相同的指标名称作为例如浮点仪表,在更改之后用作计数器直方图)或在记录规则发生变化后(例如,旧版本的规则创建了浮点仪表,而新版本的规则现在创建了仪表直方图,但保留了其名称)。样本类型频繁更改通常是配置错误的后果(例如,两个不同的记录规则创建了不同的样本类型,但馈入到同一个系列)。因此,TSDB 实现必须处理样本类型的变化,但可以相对低效地进行。当 Prometheus TSDB 遇到无法写入当前正在使用的块的样本类型时,它会关闭该块并启动一个新的具有适当编码的块。(在时间序列中,样本类型在每个样本之间来回切换将导致每个样本都有一个新的块,这确实非常低效。)
直方图块使用多种自定义编码来处理数值,目的是通过用比不常见值更少的位数编码常见值来减小数据大小。每种自定义编码的详细信息都记录在 低级别块格式文档 (最终链接到其中的代码)中。以下三种编码用于许多不同的字段,因此在此处命名以便以后引用。
- varbit-int 是一种用于有符号整数的可变比特宽度编码。它使用 1 位到 9 字节。接近零的数字需要更少的比特。这与浮点样本块中的时间戳编码类似,但具有不同的比特长度分桶,针对原生直方图中常见的数值分布进行了优化。
- varbit-uint 是一种类似的编码,但用于无符号整数。
- varbit-xor 是一种用于浮点数序列的可变比特宽度编码。它基于对序列中当前和前一个浮点值进行 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(当前和前一个样本之间的 XOR)。
- 正存储桶的填充,每个存储桶是相对于前一个存储桶的增量(或第一个存储桶中的绝对填充),编码为 varbit-int,使用与时间戳相同的“增量增量”方法。(换句话说,“双增量”编码应用于已经是增量的值,这就是为什么这有时被称为“三增量”编码。)
- 负存储桶的填充方式相同。
浮点直方图的样本数据有以下区别:
- 观察次数和零存储桶的填充现在是浮点数,因此以与观察总和相同的方式编码(第一个样本为 float64,后续样本为 varbit-xor)。
- 存储桶填充不仅是浮点数,而且是绝对填充计数而不是存储桶之间的增量。在第一个样本中,所有存储桶填充都表示为纯 float64,而在所有后续样本中,它们被编码为 varbit-xor,XOR 当前样本和前一个样本中的相应存储桶。
以下事件会触发切割新块(原因如括号中所示):
- 整数直方图和浮点直方图之间的样本类型更改(因为两者都需要完全不同的块编码)。
- 仪表直方图和计数器直方图之间的样本类型更改(因为首字节必须表示不同的类型)。
- 计数器直方图的计数器重置(将在首字节中存储为计数器重置信息,详见下文)。
- 模式更改(这意味着我们需要新的块布局,而一个块只能有一个块布局)。
- 零阈值更改(这会改变块布局,见上文)。
- 自定义值更改(这会改变块布局,见上文)。
- 陈旧标记后跟一个常规样本(这并不严格要求新块,但可以假定大多数直方图在消失和返回时会发生很大变化,因此切割新块是最佳选择)。
- 块大小限制超出(详见 下文)。
跨度的差异也会改变块布局,但可以通过添加(显式表示的)未填充存储桶来解决,以使块中的所有直方图共享相同的跨度结构。如果存储桶消失,这很简单,因为缺失的存储桶会被简单地添加为未填充的存储桶到新的直方图中,同时将直方图追加到块中。然而,曾经填充过的存储桶的消失构成了计数器重置(参见 下文),因此这种情况只能发生在仪表直方图中(它们不具有计数器重置)。更常见的情况是,在新追加的直方图中存在之前追加的直方图中不存在的存储桶。在这种情况下,这些存储桶必须作为显式未填充的存储桶添加到所有先前追加的直方图中。这需要对整个块进行完全重新编码。(仅重新编码受影响部分存在一些优化潜力。实现这一点会非常复杂。到目前为止,完全重新编码的性能影响并未突出为问题。)
陈旧标记
注意为了理解以下部分,重要的是要回忆起 TSDB 中陈旧标记的工作方式。浮点序列中的陈旧标记由许多可用于表示 `NaN` 值的特定位模式表示。这个非常特定的浮点值在以下部分中称为“特殊陈旧 `NaN` 值”。它(几乎可以肯定)永远不会由常规算术浮点运算返回,因此与“自然发生的” `NaN` 值不同,包括在 特殊观察值 中讨论的那些。事实上,特殊陈旧 `NaN` 值永远不会直接在查询 TSDB 时返回,但它会在到达调用者之前在内部处理。
为了在直方图序列中标记陈旧,可以使用常规的特殊陈旧 `NaN` 值。然而,这需要切割一个新块,仅仅是为了标记系列为陈旧,因为一个跟在直方图值后面的浮点值必须存储在不同的块中(见上文)。因此,还有一个直方图版本的陈旧标记,其中观察总和的字段被设置为特殊陈旧 `NaN` 值。在这种情况下,所有其他字段都被忽略,这使得它们可以设置为适合高效存储的值(因为陈旧标记的直方图版本本质上只是存储优化)。这适用于浮点和整数直方图(因为即使在整数直方图中,总和字段也是一个浮点值),并且可以使用适当的版本来避免切割新块。TSDB 必须将所有版本的陈旧标记(浮点、整数直方图、浮点直方图)视为等效。
块大小限制
浮点块的大小限制为 1024 字节。对于直方图块,通常也使用相同的尺寸限制。然而,单个直方图可能变得非常大,如果它们有许多存储桶,那么盲目强制执行尺寸限制可能导致块中的直方图很少。(在最极端的情况下,单个直方图甚至可能超过 1024 字节,以至于无法强制执行尺寸限制。)每个块的直方图很少,压缩率会变差。因此,在 1024 字节的尺寸限制生效之前,必须达到每个块至少 10 个直方图。这意味着直方图块可能远大于 1024 字节。
每个块要求至少 10 个直方图是一个初始的、非常简化的方法,未来可能会进行改进,以在块大小和压缩率之间找到更好的权衡。
计数器重置注意事项
通常,当其值从一个样本下降到下一个样本时,Prometheus 会认为计数器已重置(但另请参阅 有关创建时间戳的下一节)。在检测两个直方图样本之间的计数器重置时,情况更为复杂。
首先,仪表直方图和计数器直方图是明确不同的(而 Prometheus 通常在摄取后平等地对待所有浮点样本,无论它们是作为仪表还是计数器指标摄取的)。计数器重置不适用于仪表直方图。
如果一个仪表直方图后面跟着一个计数器直方图,则假定发生计数器重置,因为从仪表到计数器的更改被视为等同于仪表被删除和计数器从零开始创建。
最常见的情况是一个计数器直方图后面跟着另一个计数器直方图。在这种情况下,通过以下程序检测到可能的计数器重置:
如果两个直方图都使用标准模式,但模式或零存储桶宽度不同,这些更改可能是兼容分辨率降低的一部分(这会定期发生,以 减少直方图的存储桶计数)。兼容分辨率降低的以下两个条件都成立:
- 如果模式已更改,则其数字已从一个标准指数模式减少到另一个标准模式。
- 如果零存储桶宽度已更改,则第一个直方图中任何已填充的常规存储桶要么完全包含在第二个直方图的零存储桶中,要么不包含(即,旧的常规存储桶与新的零存储桶没有部分重叠)。
如果任一条件不满足,则该更改不是兼容分辨率降低。因为这种更改只能通过重置或重新创建直方图来实现,所以它被视为计数器重置,并且检测过程结束。
如果两个条件都满足,则必须将第一个直方图转换为使其模式和零存储桶宽度与第二个直方图匹配。这以与 前面描述 相同的方式进行:相邻的存储桶被合并以减少模式,常规存储桶与零存储桶合并以增加零存储桶的宽度。
如果两个直方图都是 NHCB(模式 -53),那么它们之间的任何自定义值差异将按照 下文 的描述进行协调。
在此过程的这一点,两个直方图具有相同的模式和零存储桶宽度,因为这要么从一开始就是这种情况,要么其中一个直方图已相应转换。(请注意,NHCB 不使用零存储桶。为了此过程的目的,它们的零存储桶宽度和填充计数被视为相等。)在这种情况下,以下任何一项均构成计数器重置:
- 观察次数下降(但特别注意,观察总和不会下降)。
- 任何存储桶(包括零存储桶)的填充计数下降。这包括填充的存储桶消失的情况,因为未表示的存储桶等同于填充计数为零的存储桶。
如果以上任一情况均不适用,则没有计数器重置。
由于整个过程相对复杂,计数器重置检测最好在摄取期间进行一次,并将结果持久化以供以后使用。摄取期间的计数器重置检测无论如何都必须发生,因为计数器重置是触发切割新块的原因之一。
切割新块以应对计数器重置旨在提高压缩率。计数器重置会将所有存储桶的填充设置为零,因此需要表示的存储桶数量更少。然而,块必须表示块中所有直方图的超集,因此切割新块可以为新块启用一组更简单的存储桶。
这反过来意味着块中的第一个样本之后将不再发生计数器重置。因此,唯一需要持久化的计数器重置信息是块中第一个直方图的信息。这发生在所谓的直方图标志中,一个字节直接存储在块中的样本数量之后。此字节目前仅用于计数器重置信息,但将来可能会用于其他标志。计数器重置信息使用前两位。四种可能的位模式在 `chunkenc` 包中表示为 `CounterResetHeader` 类型 Go 常量。它们的名称和含义如下:
GaugeType(位模式 `11`):块包含仪表直方图。计数器重置与仪表直方图无关。CounterReset(位模式 `10`):在前一个块的最后一个直方图和此块的第一个直方图之间发生了计数器重置。(很可能计数器重置实际上是切割新块的原因。)NotCounterReset(位模式 `01`):在前一个块的最后一个直方图和此块的第一个直方图之间没有发生计数器重置。(这通常发生在块因达到尺寸限制而被切割时。)UnknownCounterReset(位模式 `00`):在最后一个块的最后一个直方图和此块的第一个直方图之间是否发生计数器重置是未知的。
UnknownCounterReset 始终是安全的选择。它不会阻止计数器重置检测,但仅要求在需要计数器重置信息时(再次)执行计数器重置检测过程。
当查询 TSDB 时,计数器重置信息会传播给调用者(在 Go 代码中,作为 `CounterResetHint` 类型的字段,在 Go 类型 `Histogram` 和 `FloatHistogram` 中,使用与位模式常量相同的名称的枚举常量)。
对于仪表直方图,`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 都将以 `CounterReset` 的 `CounterResetHint` 返回给查询器,尽管现在 C 和 D 之间没有计数器重置。与前一个示例中的情况类似,必须在 A 和 B 之间进行新的计数器重置检测,并在 C 和 D 之间进行另一项检测。或者,B 和 D 都必须以 `UnknownCounterReset` 的 `CounterResetHint` 返回。
总之,每当 TSDB 无法安全地确定在摄取时已在两个样本之间进行了计数器重置检测时,它要么必须执行另一次计数器重置检测,要么必须为第二个样本返回 `UnknownCounterReset` 的 `CounterResetHint`。
请注意,存在上述过程无法检测到的计数器重置的可能性,即如果重置直方图中的计数增加得足够快,以至于计数器重置后的第一个样本没有与重置前最后一个样本相比下降的计数。(这也正是浮点计数器的问题,实际上更容易发生。)通过上述机制,即使在这种情况下也可以存储计数器重置,前提是计数器重置是通过其他方式检测到的。然而,由于块的插入和删除、乱序样本和重叠块(如上所述)造成的复杂性,如果需要第二次计数器重置检测,此信息可能会丢失。(待办事项:目前,此信息可靠地丢失,请参阅上面的待办事项。)通过创建时间戳(请参阅下一节)安全标记计数器重置的更好方法。
创建时间戳处理
OpenMetrics 为计数器、摘要和经典计数器直方图引入了所谓的创建时间戳。(该术语可能是“创建于时间戳”的缩写。更合适的术语可能是“创建时间戳”或“重置时间戳”,但“创建时间戳”一词现在已牢固确立。)
创建时间戳提供指标的最近创建或重置时间。一个 设计文档 描述了 Prometheus 如何处理创建时间戳。
创建时间戳对原生直方图也很有用。与浮点计数器中插入合成零样本的方式相同,为计数器直方图插入了直方图样本的零值。直方图的零值没有任何已填充的存储桶,观察总和、观察次数和零存储桶填充都为零。模式、零存储桶宽度、自定义值以及直方图的浮点或整数类型应与紧随合成零样本之后的样本匹配(以避免触发虚假计数器重置的检测)。
合成零样本的计数器重置信息始终设置为 `CounterReset`。
Exemplar
原生直方图的示例是作为整个直方图样本附加的,而不是附加到单个存储桶。(另请参阅 暴露格式部分。)因此,允许(实际上是常见情况)单个原生直方图样本带有多个附加示例。
示例可能从一次抓取到下一次抓取发生变化,也可能不变。抓取器应检测未更改的示例,以避免存储许多重复的示例。然而,鉴于单个样本可能包含许多示例,其中任何子集都可能与上一次抓取中的示例重复,因此重复检测可能非常昂贵。TSDB 可能会依赖于任何新示例的时间戳都比任何先前暴露的示例更新的假设。(记住,原生直方图的示例必须包含时间戳。)然后可以高效地进行重复检测:
- 新摄取的原生直方图的示例按以下字段排序:首先是时间戳,然后是值,然后是标签。
- 示例按排序顺序追加到示例存储中。
- 对于比最后成功追加的示例(可能是同一指标的上一次抓取)要早排序或相等(等于)的示例,追加会失败。
- 对于比最后成功追加的示例要晚排序的示例,追加会成功。
仅当摄入直方图的所有示例都比最后成功追加的示例排序靠前时,示例才被视为乱序。这不会检测到与较新示例混合的乱序示例,或与最后成功追加的示例重复的示例,这被认为是可接受的。
PromQL
本节描述了 PromQL 如何处理原生直方图。它侧重于一般概念,而不是单个操作的每一个细节。对于后者,请参阅 PromQL 关于 运算符 和 函数 的文档。
注解
原生直方图的引入会产生某些情况,导致 PromQL 表达式返回意外结果,最常见的情况是输出向量中的某些或所有元素意外丢失。为了帮助用户检测和理解这些情况,作用于原生直方图的操作经常使用注解。注解可以有警告和信息级别,并描述了在评估过程中可能遇到的问题。警告级别用于标记最可能是用户必须采取行动的实际问题的情况。信息级别用于标记可能也是故意的,但仍然不寻常到足以标记的情况。
整数直方图与浮点直方图
PromQL 始终作用于浮点直方图。从 TSDB 检索的整数直方图会自动转换为浮点直方图。
直方图之间的兼容性
当运算符或函数作用于两个或多个原生直方图时,所涉及的直方图需要具有相同的模式、相同的零存储桶宽度,以及(如果适用)相同的自定义值。在一定限制内,直方图可以即时转换为以满足这些兼容性标准。
- NHCB(模式 -53)仅与其他 NHCB 兼容。不同的自定义值需要通过以下方式进行转换和协调:
- 识别所有原始 NHCB 共享的自定义值。这些是新的统一自定义值。
- 将每个原始 NHCB 转换为新的自定义值,方法是将其存储桶合并到由新自定义值描述的统一存储桶集中。
- 请注意,原始 NHCB 很可能不共享任何自定义值。在这种情况下,新的存储桶集将只包含溢出存储桶,接收所有原始存储桶的所有观察值。
- 任何需要协调自定义值的查询都将标记有信息级别的注解。
- 具有标准模式的直方图始终可以通过降低具有较大模式(即更高分辨率)的直方图的分辨率来转换为最小(即最低分辨率)的公共模式。这以通常的方式发生,即将相邻的桶合并到较小模式的较大桶中。
- 不同的零桶宽度通过扩展较小的零桶来处理,并将任何已填充的常规桶适当地合并到扩展的零桶中。如果最大的公共宽度恰好落在任何已填充桶的中间,则它会进一步扩展以与该桶的桶边界重合。(有关更多详细信息,请参阅上文的零桶部分。)
如果由于不兼容而无法执行某个操作,则会在结果中添加一个警告级别的注释。
计数器重置
计数器重置的定义如上文所述。TSDB 返回的计数器重置提示可能会被考虑在内,以避免显式计数器重置检测,并正确处理通常方法无法检测到的计数器重置。(这意味着这些计数器重置仅在尽力而为的基础上考虑。然而,TSDB 本身也一样,见上文。)与经典直方图和摘要的计数器重置处理的一个显著区别是,观察总数的减少本身并不构成计数器重置。(例如,即使直方图观察到负值,计算原生直方图的速率仍然会正确工作。)
请注意,除非 PromQL 引擎能够安全地检测到从子查询返回的连续计数器直方图也与 TSDB 中的连续直方图相同,否则在避免显式计数器重置检测时,绝对不应考虑从子查询返回的计数器直方图的计数器重置提示。
Gauge 直方图 vs. Counter 直方图
通过 TSDB 返回的计数器重置提示,PromQL 可以得知原生直方图是 gauge 直方图还是 counter 直方图。为了镜像 PromQL 对浮点样本的处理(在其中它无法可靠地区分浮点计数器和 gauge),作用于计数器的函数仍将处理 gauge 直方图,反之亦然,但结果中会返回一个警告级别的注释。请注意,在这种情况下,必须对 gauge 直方图执行显式计数器重置检测,将其视为 counter 直方图。
桶内插值
在估计分位数或分数时,PromQL 必须在桶内进行插值。在经典直方图中,这种插值是线性进行的。它基于观察值在桶内均匀分布的假设。实际上,这个假设可能离实际情况很远。(例如,一个 API 端点可能对几乎所有请求的响应延迟为 110 毫秒。那么中位数延迟甚至 90% 百分位延迟都将接近 110 毫秒。如果一个经典直方图在 100 毫秒和 200 毫秒处有桶边界,它将看到大部分观察值在此范围内,并估计中位数为 150 毫秒,90% 百分位数为 190 毫秒。)最坏的情况是在桶的一端进行估计,而实际值在桶的另一端。因此,最大可能误差是桶的整个宽度。不进行任何插值并使用桶内某个固定中点(例如算术平均值甚至调和平均值)会最小化最大可能误差(在算术平均值的情况下,这将是桶宽度的一半),但实际上,线性插值在平均误差上较低。由于线性插值在多年的经典直方图使用中一直表现良好,因此也将其应用于原生直方图。
对于 NHCB(原生计数器直方图),PromQL 应用与经典直方图相同的插值方法,以保持结果的一致性。(NHCB 的主要用例是经典直方图的即插即用替代品。)然而,对于标准的指数模式,线性插值可能不合适。虽然指数模式主要旨在最小化分位数估计的相对误差,但它们也受益于桶的平衡使用,至少在观察值的某些范围内。基本假设是,对于大多数实际发生的分布,观察值的密度倾向于在较小的观察值处更高。因此,PromQL 对标准模式使用指数外插,这模拟了在增加模式编号(即分辨率翻倍)时将一个桶分成两个,平均而言,这两个新桶具有相似的分布。更详细的解释可以在实现插值方法的 PR 中找到。
零桶内的插值是一个特殊情况。零桶打破了指数分桶模式。因此,在零桶内应用线性插值。此外,如果直方图的所有已填充常规桶都是正数,则假设零桶中的所有观察值也是正数,即插值在零和零桶的上边界之间进行。如果直方图中所有已填充的常规桶都是负数,则情况相反,即零桶内的插值在零桶的下边界和零之间进行。
混合序列
如上文所述,原生直方图的样本类型或类型都不是序列的身份的一部分。因此,同一个序列可能包含不同样本类型和类型的混合。
计数器直方图和 gauge 直方图的混合不会阻止任何 PromQL 操作,但如果某些输入样本具有不合适的类型,则结果中会返回一个警告级别的注释(参见上文)。
浮点样本和直方图样本的混合更具问题。许多作用于范围向量的函数将从结果中删除包含浮点数和直方图混合的元素。如果发生这种情况,则会在结果中添加一个警告级别的注释。具体示例可在下方找到。
一元负号和负直方图
一元负号可用于原生直方图。它返回一个直方图,其中所有桶的填充数以及观察值的计数和总和都已取反。计数器重置提示在任何情况下都设置为 GaugeType。其他所有内容保持不变。需要强制设置为 GaugeType,因为显式的计数器重置检测会因取反的符号而失效。
通常,具有负桶填充数或负观察计数的直方图本身没有太大意义,它们仅用作其他表达式中的中间结果。在 PromQL 中,它们始终被视为 gauge 直方图。它们不能作为记录规则的结果进行持久化。(评估为负直方图的规则会导致错误。)在任何交换格式(暴露格式、远程写入、OTLP)中都无法表示负直方图。
二元运算符
大多数二元运算符不能用于两个直方图之间,或直方图与浮点数之间,或直方图与标量之间。如果运算符处理这种不可能的组合,则相应的元素将从输出向量中移除,并在结果中添加一个信息级别的注释。(这种情况有点类似于标签匹配,其中样本类型起着类似于标签的作用。因此,这种不匹配可能是已知的且是故意的,这也是注释级别仅为信息的原因。)
以下描述了所有实际有效的操作。
加法(+)和减法(-)在两个兼容的直方图之间有效。这些运算符将所有匹配的桶填充以及计数和观察总数相加或相减。缺失的桶被假定为空并按此处理。通常,两个操作数都应该是 gauge。添加和减去计数器直方图需要谨慎,但 PromQL 允许这样做。添加 gauge 直方图和计数器直方图会产生 gauge 直方图。添加两个计数器直方图会产生计数器直方图。如果两个操作数共享相同的计数器重置提示,则生成的计数器直方图将保留该计数器重置提示。否则,生成的计数器重置提示将被设置为 UnknownCounterReset。减法的结果始终标记为 gauge 直方图,因为它可能产生负直方图,请参阅上文的说明。对具有直接矛盾的计数器重置提示(即 CounterReset 和 NotCounterReset)的两个计数器直方图进行加法或减法会触发警告级别的注释。(待办:如上文所述,TSDB 目前不返回 NotCounterReset,因此此注释仅在涉及 HistogramStatsIterator 的特定情况下发生,该迭代器包含额外的计数器重置跟踪。请参阅跟踪问题 。)
乘法(*)在一侧是浮点样本或标量,另一侧是直方图时有效,顺序不限。它将所有桶填充以及计数和观察总数乘以浮点数(样本或标量)。这将导致“缩放”的、甚至有时是负的直方图,这通常仅作为其他表达式中的中间结果有用(另请参阅上文的说明)。乘法适用于计数器直方图和 gauge 直方图,它们的类型在操作后保持不变。
除法(/)在左侧是直方图,右侧是浮点样本或标量时有效。它等同于乘以浮点数(样本或标量)的倒数。零除法会导致一个没有常规桶的直方图,零桶填充以及计数和观察总数都设置为 +Inf、-Inf 或 NaN,具体取决于输入直方图中的值(分别为正数、负数或零/NaN)。
相等(==)和不等(!=)在两个直方图之间有效,既在过滤版本中,也带有 bool 修饰符。它们比较模式、自定义值、零阈值、所有桶填充以及总数和观察计数。直方图具有计数器或 gauge 类型对比较无关紧要。(计数器直方图可能等于 gauge 直方图。)
逻辑/集合二元运算符(and, or, unless)在涉及直方图样本时也能按预期工作。它们仅检查向量元素的 P存在性,并且不根据元素的类型或类型(浮点数或直方图、计数器或 gauge)改变其行为。
“修剪”运算符 >/ 和 </ 是专门为原生直方图引入的。它们仅对左侧的直方图和右侧的浮点样本或标量有效。(它们不适用于两侧都是浮点样本或标量的情况。在这种情况下,会返回一个信息级别的注释。)这些运算符分别从直方图中移除大于或小于右侧浮点值的观察值,并返回结果直方图。只有当阈值与桶边界重合时,移除才是精确的。否则,必须使用受影响桶内的插值,如上文所述。直方图的计数器与 gauge 类型得以保留。(待办:这些运算符尚未实现,并且在细节上可能还会发生变化,请参阅跟踪问题 。)
聚合运算符
以下聚合运算符与浮点和直方图样本的运行方式相同(原因见括号)
group(此聚合的结果不取决于样本值。)count(此聚合的结果不取决于样本值。)count_values(Go 的FloatHistogram.String方法生成的文本表示形式用作直方图的值。)limitk(采样元素将原样返回。)limit_ratio(采样元素将原样返回。)
sum 聚合运算符使用原生直方图,通过将要聚合的直方图相加来完成,方式与上文的 + 运算符中所述相同。avg 聚合运算符的运行方式相同,但将总和除以聚合直方图的数量(与上文的 / 运算符中所述相同)。
sum 和 avg 都会从输出向量中移除需要聚合浮点样本与直方图样本的元素。此类移除会通过警告级别的注释进行标记。
sum 和 avg 都应仅应用于 gauge 直方图。PromQL 允许聚合计数器直方图(甚至两者的混合),但这需要谨慎处理以使其有意义。对于 gauge 与 counter 类型的影响以及由此产生的计数器重置提示,将从上文的 + 运算符的说明中推导出来。
- 如果所有聚合直方图共享相同的计数器重置提示,则结果将保留相同的计数器重置提示。
- 如果聚合直方图中至少有一个 gauge 直方图,则结果是 gauge 直方图。
- 在所有其他情况下,结果的计数器重置提示将设置为
UnknownCounterReset。 - 在任何情况下,聚合直方图之间任何直接矛盾的计数器重置提示(即
CounterReset和NotCounterReset)都会触发警告级别的注释。
所有其他聚合运算符不适用于原生直方图。输入向量中的直方图将被简单忽略,并为每个被忽略的直方图添加一个信息级别的注释。
函数
以下函数作用于原生直方图的范围向量,通过对匹配的桶(包括零桶)以及观察值的总数和计数应用常规浮点操作,从而生成新的原生直方图。
delta()(用于 gauge 直方图。)increase()(用于计数器直方图。)rate()(用于计数器直方图。)idelta()(用于 gauge 直方图。)irate()(用于计数器直方图。)
如上所述,这些函数应应用于 gauge 直方图或计数器直方图。然而,它们都适用于两种类型,但如果范围内包含至少一个不合适类型的直方图,则结果中会添加一个警告级别的注释。
delta()、increase() 和 rate() 对于范围内包含浮点样本和直方图样本混合的序列不会返回结果。idelta() 和 irate() 对于范围内最后两个样本是浮点样本和直方图样本的混合的序列不会返回结果。在任何一种情况下,都会为因此缺少输出元素的情况添加一个警告级别的注释。
所有这些函数都返回 gauge 直方图作为结果。
一如既往,这些函数尝试通过尽可能将模式转换为公共模式来协调不同的模式。但是,应用于计数器的函数(increase()、rate()、irate())在第一个样本与第二个样本之间存在计数器重置时,不会为第一个样本执行此转换。在这种情况下,将忽略第一个样本,因此第一个样本与其他样本之间不兼容的桶布局将被默默忽略。
为了防止在零以下进行外插,采用了与浮点计数器 相同的回溯法,但仅基于观察计数。因此,在某些情况下,单个桶可能仍然在零以下进行外插。另一种选择是找到一个最小的外插值,使得计数和任何桶都不会在零以下进行外插。然而,这并不一定会导致更好的回溯法,同时会带来显著的复杂性成本。在第一个样本是源自创建时间戳的合成零样本的常见且重要的情况下,有限的外插将完美准确,因为在该合成样本的时间戳处,计数和所有桶都为零,这也是限制外插的时间点。请注意,经典直方图独立地将回溯法应用于每个桶以及计数和总和(因为它们都是单独的序列)。这已知会导致不一致。NHCB 不会重现此问题,并且与所有其他原生直方图的工作方式相同,这意味着比较经典直方图和等效 NHCB 时,rate() 和 increase() 的结果可能会略有不同。
avg_over_time() 和 sum_over_time() 的原生直方图工作方式与相应的聚合运算符相对应。特别是,如果一个序列在范围内包含浮点样本和直方图样本的混合,则相应的输出将从输出向量中完全删除。这种删除会通过警告级别的注释进行标记。
changes() 和 resets() 函数在原生直方图样本上的作用与在浮点样本上的作用相同。它们甚至可以在同一序列中处理浮点样本和直方图样本的混合。在这种情况下,从浮点样本到直方图样本以及反之的更改都算作 changes() 的一次更改,并算作 resets() 的一次重置。类型从计数器直方图到 gauge 直方图以及反之的更改不计入 changes() 的更改。resets() 应仅应用于计数器浮点数和计数器直方图,但该函数仍然适用于 gauge 直方图,在这种情况下会应用显式的计数器重置检测。此外,从计数器直方图到 gauge 直方图以及反之的更改被计为一次重置。
histogram_quantile() 函数起着非常特殊的作用,因为它是唯一一个特殊处理特定“魔术”标签的函数,即经典直方图使用的 le 标签。histogram_quantile() 对原生直方图的作用也类似,但没有 le 标签的特殊作用。该函数继续以已知的方式处理浮点样本,同时对原生直方图样本使用新的“原生”方式。
经典直方图(包括 rate 和聚合)的典型查询示例
histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket[10m])))
这是原生直方图的相应查询
histogram_quantile(0.9, sum by (job) (rate(http_request_duration_seconds[10m])))
与经典直方图一样,可以使用 1 和 0 分别作为 histogram_quantile 的第一个参数来估计直方图中的最大值和最小值。然而,具有标准模式的原生直方图能够提供更有用的结果,这不仅是因为原生直方图通常具有更高的分辨率,更重要的是因为具有标准模式的原生直方图在整个浮点数范围内都能维持相同分辨率。对于经典直方图,最大观察值很可能在 +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) 表示小于或等于 y 的观察值比例为 q。由于 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() 和之前未提及的所有 <aggregation>_over_time() 函数,原生直方图样本将从输入范围向量中移除。如果任何序列在范围内包含浮点样本和直方图样本的混合,则移除直方图将被标记为信息级别的注释。
记录规则
记录规则可能产生原生直方图值。它们将像正常摄入一样被写回 TSDB,包括直方图是 gauge 直方图还是计数器直方图。在后一种情况下,还将存储由计数器重置提示明确标记的计数器重置,而否则在摄入期间启动新的计数器重置检测。
TSDB 实现可能会将记录规则创建的浮点直方图转换为整数直方图,如果此转换精确地表示原始直方图中的所有浮点值。
警报规则
虽然警报像往常一样使用原生直方图工作,但强烈建议避免将原生直方图作为警报的输出值。如果在模板中使用原生直方图样本,它们会以其简单的文本形式(由 Go 的 FloatHistogram.String 方法生成)显示,这对于人类来说难以阅读。
测试框架
PromQL 测试框架已得到扩展,因此通过 promtool 的 PromQL 单元测试和规则单元测试都可以包含原生直方图。直方图样本的表示法很复杂,具体解释请参见规则单元测试文档。
在单元测试框架中,有一个名为 load_with_nhcb 的备用 load 命令,它会将经典直方图转换为 NHCB,并加载经典直方图的浮点数列以及转换产生的 NHCB 列。
在单元测试框架中,expect 关键字(尽管它并非仅限于原生直方图,但在其上下文中非常有用)可以定义关于 info 和 warn 级别注释的预期。
优化
一如既往,PromQL 实现可以在不改变行为的情况下应用任何认为合适的优化。解码原生直方图可能相当昂贵,因为潜在的桶数量可能很多。同样,在 PromQL 引擎中深度复制直方图样本比复制简单的浮点数样本昂贵得多。与总是解码一切并总是复制一切的朴素方法相比,这提供了巨大的优化潜力。
Prometheus 目前会尝试避免不必要的复制(待办:但仍需实现真正的写时复制(CoW)方法,因为那会更清晰且不易出错),并在只需要观察总和和计数的情况下跳过特殊情况下的桶解码。
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>" ], ... ]
}
count 和 sum 直接对应于同名的直方图字段。每个桶都显式地用其边界和计数表示,包括零桶。因此,跨度(spans)和模式(schema)不包含在响应中,并且直方图对象的结构不依赖于所使用的模式。
<boundary_rule> 占位符是一个介于 0 和 3 之间的整数,含义如下
- 0:“左开”(左边界排他,右边界包容)
- 1:“右开”(左边界包容,右边界排他)
- 2:“两边都开”(两边界都排他)
- 3:“两边都闭”(两边界都包容)
对于标准模式,正桶是“左开”,负桶是“右开”,零桶(左边界为负,右边界为正)是“两边都闭”。对于 NHCB,所有桶都是“左开”(反映了经典直方图的行为)。未来的模式可能会使用不同的边界规则。
元数据
对于 series 端点,包含原生直方图的系列与只包含浮点数的传统系列包含方式相同。该端点不提供任何关于包含样本类型的信息(实际上,任何 系列都可能包含任一样本类型或两者都包含)。请特别注意,由目标公开的名为 request_duration_seconds 的直方图,如果以原生直方图的形式公开和摄取,将导致一个名为 request_duration_seconds 的系列;如果以经典直方图的形式公开和摄取,则会导致一系列名为 request_duration_seconds_sum、request_duration_seconds_count 和 request_duration_seconds_bucket 的系列。如果直方图被同时作为原生直方图和经典直方图摄取,则 series 端点将返回所有上述系列名称。
目标和指标元数据(targets/metadata 和 metadata 端点)工作方式略有不同,因为它们作用于目标公开的原始名称。这意味着一个名为 request_duration_seconds 的经典直方图,将仅通过这些元数据端点表示为 request_duration_seconds(而不是 request_duration_seconds_sum、request_duration_seconds_count 或 request_duration_seconds_bucket)。一个名为 request_duration_seconds 的原生直方图也将在此名称下表示。即使 request_duration_seconds 被同时作为经典和原生直方图摄取,也不会发生冲突,因为返回的元数据实际上是相同的(最值得注意的是,返回的 type 将是 histogram)。换句话说,目前无法仅通过元数据端点区分原生直方图和经典直方图。需要通过 series 端点进行额外的查找。没有计划更改这一点,因为现有的元数据端点无论如何都受到严重限制(没有历史信息,没有规则创建的指标的元数据,处理不同目标之间冲突元数据的能力有限)。但是,有计划改进 Prometheus 的元数据处理。这些工作也将考虑如何正确支持原生直方图。(待办:随着进展更新。)
Prometheus UI
本节描述 Prometheus 自身 UI 对直方图的渲染。这可以作为第三方图形前端的指导。
在表格视图中,直方图数据点以图形方式渲染为条形图,并附有所有桶的文本表示,包括它们的上下限以及观察值的计数和总和。条形图中的每个条形代表一个桶。每个条形在x轴上的位置由相应桶的上下限决定。每个条形的面积与相应桶的总体积成比例(这是渲染直方图的一般核心原则)。
图形直方图允许在指数和线性x轴之间进行选择。前者是默认设置。它非常适合标准模式。(待办:对于非指数模式,考虑将线性作为默认值。)方便的是,指数模式的所有常规桶在指数x轴上都具有相同的宽度。这意味着y轴可以显示实际的桶总体积,而不会违反上述条形图面积(而不是高度)代表桶总体积的原则。零桶是例外。技术上,它的宽度是无限的。Prometheus 简单地将其渲染为与常规指数桶相同的宽度(这意味着x轴在零点附近不严格呈指数增长)。(待办:如何渲染非指数模式。)
使用线性x轴时,桶的宽度通常不同。因此,y轴显示桶总体积除以其宽度。Prometheus UI 不会渲染y轴上的值,因为它们对人类来说很难理解。总体积仍然可以在文本表示中查看。
在图表视图中,Prometheus 显示一个热力图(待办:尚未实现,见下文),这可以被看作是随时间变化的直方图序列,旋转 90 度,并将桶总体积编码为颜色而不是条形的高度。用于将计数器类直方图渲染为热力图的典型查询是 rate 查询。热力图是一种非常强大的表示方法,可以让人类轻松地识别分布随时间变化的特征。
待办事项热力图尚未实现。取而代之的是,UI 只将观察值的总和显示为常规图表。有关详细信息,请参阅跟踪问题 。 same issue also discusses how to deal with the rendering of range vectors in the Table view.
模板扩展
原生直方图在模板扩展中工作。它们以受开闭区间数学表示法启发的文本表示形式渲染(这由 Go 的 FloatHistogram.String 方法生成)。由于原生直方图可能有许多桶,并且桶边界往往具有很多小数位,因此这种表示法不一定非常易读。请谨慎在模板扩展中使用原生直方图。
浮点数直方图文本表示的示例
{count:3493.3, sum:2.349209324e+06, [-22.62741699796952,-16):1000, [-16,-11.31370849898476):123400, [-4,-2.82842712474619):3, [-2.82842712474619,-2):3.1, [-0.01,0.01]:5.5, (0.35355339059327373,0.5]:1, (1,1.414213562373095]:3.3, (1.414213562373095,2]:4.2, (2,2.82842712474619]:0.1}
远程写入与远程读取
用于远程写入和远程读取的protobuf 规范 已扩展以支持原生直方图。无法处理原生直方图的接收器将简单地忽略新添加的字段。尽管如此,Prometheus 必须配置为通过远程写入发送原生直方图(通过将 send_native_histograms 远程写入配置设置为 true)。
在远程写入 v2 中,原生直方图是一项稳定功能。
在发送或接收经典直方图时,将其转换为 NHCB 可能会很有吸引力。然而,这并不能克服经典直方图在通过远程写入传输时所遭受的已知一致性问题。相反,经典直方图应在抓取时转换为 NHCB。同样,显式 OTel 直方图应在OTLP 摄取时转换为 NHCB。
待办事项远程写入可能存在的遗留问题是,如果为同一个原生直方图最初摄取的多个示例在不同的远程写入请求中发送,该如何处理。
联邦
只要联邦抓取使用 protobuf 格式,原生直方图的联邦就能按预期工作。理论上,一旦 OpenMetrics 文本格式支持了原生直方图,就可以实现通过 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 analyze 和 promtool tsdb list 可以正常处理原生直方图。前者使用 --extended 参数的输出包含直方图块的特定部分。
promtool tsdb dump 使用原生直方图的常规文本表示(由 Go 方法 FloatHistogram.String 生成)。
promtool tsdb create-blocks-from rules 支持生成原生直方图的规则。
promtool promql ... 命令支持为原生直方图添加的所有 PromQL 功能。
虽然 promtool tsdb bench write 原则上可以包含原生直方图,但目前不计划支持。
以下命令依赖于 OpenMetrics 文本格式,因此只要 OpenMetrics 不支持原生直方图,就无法支持它们:
promtool check metricspromtool push metricspromtool tsdb dump-openmetricspromtool tsdb create-blocks-from openmetrics
待办事项随着进展更新。请参阅跟踪问题 。
prom2json
prom2json 是一个小型工具,它抓取 Prometheus /metrics 端点,将指标转换为自定义 JSON 格式,然后将其转储到 stdout。这对于使用处理 JSON 的工具(例如 jq)进一步处理非常方便。
prom2json v1.4 添加了对原生直方图的支持。如果暴露的直方图中至少有一个桶跨度,prom2json 将在 JSON 输出中用原生直方图的桶替换常规的经典桶,格式借鉴了Prometheus 查询 API。
迁移注意事项
从经典直方图迁移到原生直方图时,有三个重要的潜在问题需要考虑:
- 查询原生直方图的方式与查询经典直方图不同。在大多数情况下,更改是最小且直接的,但存在一些棘手的边缘情况,这使得执行可靠的自动转换变得困难。
- 经典直方图和原生直方图不能互相聚合。在某个时间点从经典直方图更改为原生直方图,会使得创建跨越过渡点的仪表板变得困难,并且包含过渡点的范围向量将不可避免地不完整(即,选择经典直方图的范围向量仅包含范围内较早部分的数据点,而选择原生直方图的范围向量仅包含范围内较晚部分的数据点)。
- 经典的直方图可能被定制为在感兴趣的点精确设置桶边界。具有标准模式的原生直方图可以具有高分辨率,但不能将桶边界设置在任意值。在这种情况下,原生直方图的用户体验实际上可能会更差。
为了解决 (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 文档包含详细的迁移指南 ,遵循与本节相同的哲学。