指标是 Prometheus 堆栈摄取的核心资源,用于为您提供有用的信息。正确理解它们对于充分利用、管理甚至扩展此堆栈必须提供的可能性领域至关重要。从数据到信息,最后到知识,指标在这里为您提供帮助。
简而言之,本章将涵盖以下主题:
- Understanding the Prometheus data model
- A tour of the four core metric types
- Longitudinal and cross-sectional aggregations
指标是 Prometheus 堆栈摄取的核心资源,用于为您提供有用的信息。正确理解它们对于充分利用、管理甚至扩展此堆栈必须提供的可能性领域至关重要。从数据到信息,最后到知识,指标在这里为您提供帮助。
简而言之,本章将涵盖以下主题:
要了解 Prometheus 数据模型,我们需要了解时间序列的构成以及此类数据的存储。这些概念在本书中将是无价的。
时间序列数据通常可以定义为从同一来源按时间顺序索引的一系列数字数据点。在 Prometheus 的范围内,这些数据点是按固定的时间间隔收集的。因此,当以图形形式表示时,这种数据通常会绘制数据随时间的演变,x 轴是时间,y 轴是数据值。
这一切都始于随着时间的推移收集、存储和查询测量值的需要。在处理来自收集器和传感器(例如构成物联网的那些)的大量数据时,如果数据库在设计时没有考虑到该用例,那么查询结果数据集的速度会非常慢。没有什么可以阻止您使用标准的关系或 NoSQL 数据库来存储时间序列数据,但是性能损失和可伸缩性问题应该让您考虑这个决定。 Prometheus 选择实施一个针对其独特问题空间量身定制的时间序列数据库。
除了这些类型数据库的大量写入方面,这反过来意味着存储大量测量值,了解一个简单的查询可以跨越几个小时、几天甚至几个月,返回一个巨大的数据也很重要。数据点的数量,但仍有望以相当快的速度返回数据。
因此,现代时间序列数据库存储以下组件:
符合此时间序列数据库规范的数据抽象示例如下:
如您所见,这种数据可以很容易地存储到数据库中的单个表中:
timestamp | company | location | beverage | value |
1544978108 | ACME | headquarters | coffee | 40172 |
在这个简单的示例中,我们可以检查位于 ACME 公司总部的自动售货机提供的咖啡杯数。如果通过时间连续测量此示例,则该示例具有时间序列的所有必需组件。
本地存储是在 Prometheus 中存储数据的标准方法,因此,我们必须了解它的基础知识。在非常高的层次上,Prometheus 存储设计是一个索引实现的组合,它使用发布列表来记录所有当前存储的标签及其值,以及它自己的时间序列数据格式。
Prometheus 将收集到的数据存储在本地的方式可以看作是一个三部分的过程。以下主题描述了数据在成功持久化之前所经历的阶段。
最新的一批数据在内存中最多保留两个小时。这包括在两个小时的时间窗口内收集的一个或多个数据块。这种方法极大地减少了两倍的磁盘 I/O;最新数据在内存中可用,查询速度极快;并且数据块是在内存中创建的,避免了不断的磁盘写入。
在内存中,数据不会持久化,如果进程异常终止,数据可能会丢失。为了防止这种情况发生,磁盘中的预写日志 (WAL) 会保留内存中数据的状态,以便在 Prometheus 出于任何原因时可以重放它,崩溃或重新启动。
在两个小时的时间窗口之后,这些块被写入磁盘。这些块是不可变的,即使可以删除数据,它也不是原子操作。相反,墓碑文件是使用不再需要的数据信息创建的。
数据在 Prometheus 中的存储方式,正如我们在以下示例中所见,被组织成一系列目录(块),其中包含数据块、该数据的 LevelDB 索引、meta.json包含有关块的人类可读信息的文件,以及不再需要的数据的墓碑。这些块中的每一个都代表一个数据库。
在顶层,您还可以看到尚未刷新到自己的块中的数据的 WAL:
正如我们目前所见,Prometheus 将数据存储为时间序列,其中包括称为标签的键/值对、时间戳,最后是值。以下主题将扩展这些组件并提供每个组件的基础知识,我们将在 第 7 章,Prometheus 查询语言 - PromQL,专用于 PromQL。
Prometheus 中的时间序列表示如下:
如您所见,它表示为一个指标名称,可选地后跟一组或多组标签名称/值(在大括号内),然后是指标的值。此外,样本还将具有毫秒精度的时间戳。
尽管这是一个实现细节,但度量名称只不过是一个名为 "__name__" 的特殊标签的值。因此,如果您有一个名为 "beverages_total" 的指标,则在内部它表示为 "__name__=beverages_total"。请记住,由 "__" 包围的标签是 Prometheus 内部的,任何以 "__" 为前缀的标签仅在指标收集周期的某些阶段可用。
标签(键/值)和指标名称的组合定义了时间序列的标识。
Prometheus 中的每个指标名称都必须匹配以下正则表达式:
通俗地说,这意味着度量名称只允许英文字母的小写和大写字母 (a-z)、下划线 (_)、冒号 (:< /kbd>) 和阿拉伯数字 (0-9),但第一个字符除外,其中不允许使用数字。
标签或与某个指标关联的键/值对为指标添加维度。正如我们将在 第 7 章,Prometheus 查询语言– PromQL.
虽然 label 值可以是完整的 UTF-8,但 label 名称必须与正则表达式匹配才能被视为有效;例如,"[a-zA-Z0-9_]*"。
它们在度量名称方面的主要区别在于标签名称不允许使用冒号 (:)。
样本是采集到的数据点,代表时间序列数据的数值。定义样本所需的组件是 float64 值和毫秒精度的时间戳。 需要记住的是,无序收集的样本将被 Prometheus 丢弃。具有相同度量标识和不同样本值的样本也会发生同样的情况。
根据分配给 Prometheus 实例的计算资源(即 CPU、内存、磁盘空间和 IOPS),它将优雅地处理多个时间序列。这个数字可以被认为是该实例容量的主要指标,它将为您的抓取决策提供信息:您是否会拥有数千个指标相对较少的目标、更少的目标每个指标有 1000 个,还是介于两者之间?最后,Prometheus 将只能处理这么多时间序列而不会降低性能。
基数的概念正是在这种背景下出现的。该术语通常用于表示由度量名称及其相关标签名称/值组合产生的唯一时间序列的数量。例如,来自具有 100 个实例的应用程序中没有额外维度(例如标签)的单个指标自然意味着 Prometheus 将存储 100 个时间序列,每个实例一个(这里的实例是添加的维度在应用程序之外);该应用程序的另一个具有十个可能值标签的指标将转换为 1,000 个时间序列(每个实例 10 个时间序列,乘以 100 个实例)。这表明基数是乘法的——每个额外的维度都会通过为新维度的每个值重复现有维度来增加生成的时间序列的数量。在一个指标中拥有具有大量可能值的多个维度将导致 Prometheus 中所谓的基数爆炸,即创建大量时间序列。
当您的标签值没有明确的限制时,可以无限增加或超过数百个可能的值,您也会遇到基数问题。这些指标可能更适合在基于日志的系统中处理。
以下是一些不应用作标签值(或指标名称,就此而言)具有高或未绑定基数的数据示例:
Prometheus 指标分为四种主要类型:计数器、仪表、直方图和摘要。深入了解它们至关重要,因为 Prometheus 提供的大多数功能仅适用于给定的数据类型。因此,为此,这里是每个的概述。
这是一个严格累积的指标,其值只能增加。此规则的唯一例外是当度量标准被重置时,这会将它带回零。
这是最有用的指标类型之一,因为即使抓取失败,您也不会丢失数据的累积增量,这些数据将在下一次抓取时可用。需要明确的是,在抓取失败的情况下,粒度会丢失,因为保存的点数会减少。
为了帮助可视化这种类型的指标,这里有一些计数器示例及其基于我们在前一章中创建的测试环境的图形表示:
仪表是一种度量在收集时对给定度量进行快照的度量,该度量可以增加或减少e(例如温度、磁盘空间和内存使用情况)。
如果一次抓取失败,您将丢失该样本,因为下一次抓取可能会遇到不同值(更高/更低)的指标。
为了帮助可视化这种类型的度量,这里有一些仪表示例及其基于我们在上一章中创建的测试环境的图形表示:
Grafana 实例上的可用内存量——注意由实例重启引起的中间间隙,防止对该期间的可能值进行任何假设:
记录系统中每个事件所固有的数字数据可能会很昂贵,因此通常需要进行某种预聚合以至少保存有关所发生事件的部分信息。但是,通过预先计算每个实例的聚合(例如自进程启动以来的平均值、滚动窗口、指数加权等),会丢失大量粒度,并且某些计算的计算成本可能很高。除此之外,很多预聚合通常不能在不失去意义的情况下重新聚合——一千个预计算的 95% 的平均值没有统计意义。同样,从给定集群的每个实例(例如)收集的请求延迟的第 99 个百分位不会让您知道整个集群的第 99 个百分位,也无法准确计算它。
直方图允许您通过将事件计数到可在客户端配置的存储桶中以及通过提供所有观察值的总和来保留一些粒度。 Prometheus 直方图为每个配置的存储桶生成一个时间序列,另外还有两个用于跟踪观察到的事件的总和和计数。此外,Prometheus 中的直方图是累积的,这意味着每个桶将具有前一个桶的值,加上它自己的事件数。这样做是为了可以出于性能或存储原因删除一些桶,而不会失去使用直方图的整体能力。
使用直方图的缺点是所选存储桶需要适合预期收集的值的范围和分布。分位数计算的误差范围将与此拟合直接相关:桶太少或选择不当会增加分位数计算的误差范围。
这种类型的指标对于跟踪分桶延迟和大小(例如,请求持续时间或响应大小)特别有用,因为它可以跨不同维度自由聚合。另一个很好的用途是生成热图(直方图随时间的演变)。
为了帮助可视化这种类型的指标,下面是一个基于我们在前一章中创建的测试环境的直方图及其图形表示的示例:
摘要在某些方面类似于直方图,但呈现出不同的权衡,并且通常不太有用。它们还用于跟踪大小和延迟,还提供观察事件的总和和计数。此外(如果使用的客户端库支持它),摘要还可以在预定的滑动时间窗口内提供预先计算的分位数。使用汇总分位数的主要原因是当需要准确的分位数估计时,无论观察到的事件的分布和范围如何。
分位数和滑动窗口大小都在检测代码中定义,因此无法临时计算其他分位数或窗口大小。在客户端进行这些计算也意味着仪器和计算成本要高得多。最后要提到的缺点是生成的分位数不可聚合,因此用途有限。
摘要的一个好处是,如果没有分位数,它们的生成、收集和存储非常便宜。
为了帮助可视化这种类型的指标,下面是一个基于我们在上一章中创建的测试环境的摘要及其图形表示的示例:
考虑时间序列时要掌握的最后一个概念是聚合如何在抽象级别上工作。 Prometheus 的核心优势之一是它使时间序列数据的操作变得容易,而这种对数据的切片和切块通常归结为两种聚合,它们经常一起使用:纵向聚合和横截面聚合。
在时间序列的上下文中,聚合是减少或汇总原始数据的过程,也就是说,它接收一组数据点作为输入,并产生一个较小的集合(通常是单个元素)作为输出。时间序列数据库中一些最常见的聚合函数是最小值、最大值、平均值、计数和总和。
为了更好地理解这些聚合是如何工作的,让我们使用本章前面介绍的示例时间序列来查看一些数据。为了清楚起见,接下来的几节将解释这些聚合如何在抽象级别上工作,并暗示它们的 Prometheus 对应物是什么,但不应该与 PromQL 一对一匹配(我们将在第 7 章,Prometheus 查询语言– PromQL)。
假设我们选择了 {company=ACME, Drink=coffee},现在我们正在查看每个位置随时间变化的原始计数器。数据看起来像这样:
Location/Time | t=0 | t=1 | t=2 | t=3 | t=4 | t=5 | t=6 |
Factory | 1,045 | 1 | 2 | 3 | 4 | 5 | 6 |
Warehouse | 223 | 223 | 223 | 223 | 224 | 224 | 224 |
Headquarters | 40,160 | 40,162 | 40,164 | 40,166 | 40,168 | 40,170 | 40,172 |
为了论证的缘故,假设每分钟收集一次样本。度量类型可能是一个计数器,因为它是单调递增的,除了在 t=1 为 location=factory 重置的计数器。
横截面聚合是最容易理解的。正如我们在以下数据表示中所见,我们获取一列数据并对其应用聚合函数:
Location/Time | t=0 | t=1 | t=2 | t=3 | t=4 | t=5 | t=6 |
Factory | 1,045 | 1 | 2 | 3 | 4 | 5 | 6 |
Warehouse | 223 | 223 | 223 | 223 | 224 | 224 | 224 |
Headquarters | 40160 | 40,162 | 40,164 | 40,166 | 40,168 | 40,170 | 40,172 |
如果我们应用 max() 聚合,我们可以找出哪个位置报告了更多的咖啡被分配——在这种情况下,结果将是 40,172。应用 count() 将为我们提供报告所选维度数据的办事处数量({company=ACME, Drink=coffee}):3.
这种类型的聚合通常适用于请求集中的最后一个数据点。不正确的最常见情况是绘制一段时间内的聚合图,因为需要为图中的每个点计算聚合。
您会注意到所选数据类似于线性代数中的传统列向量。正如我们将在 第 7 章中看到的,Prometheus查询语言 – PromQL,专用于 PromQL,这些将被称为即时向量。
纵向聚合使用起来比较棘手,因为您需要选择一个时间窗口来应用聚合。这意味着它们在行上工作,正如我们在以下表示中看到的那样:
Location/Time | t=0 | t=1 | t=2 | t=3 | t=4 | t=5 | t=6 |
Factory | 1,045 | 1 | 2 | 3 | 4 | 5 | 6 |
Warehouse | 223 | 223 | 223 | 223 | 224 | 224 | 224 |
Headquarters | 40,160 | 40,162 | 40,164 | 40,166 | 40,168 | 40,170 | 40,172 |
由于我们使用的当前选择器返回三行数据,这意味着我们在应用纵向聚合时将得到三个结果。在此示例中,我们选择了最后三分钟的数据进行聚合(如前所述,我们正在考虑 1 分钟的采样间隔)。如果我们随着时间的推移应用 max() 聚合,因为这些是计数器并且在所选窗口中没有重置,我们将获得所选集中的最新值: 为 6 location=factory,location=warehouse 为 224,location=headquarters 为 40,172。 count() 将返回在指定时间范围内选择的点数——在这种情况下,由于收集每分钟发生一次,我们要求它持续三分钟,它将返回 3 为每个位置。
rate() 是一种更有趣但之前没有提到的聚合。它是与计数器一起使用的特别有用的聚合,因为您可以计算每单位时间的变化率——我们将在本书后面详细探讨这一点。在此示例中,rate() 将分别为每个位置返回 1、0 和 2。
再次,我们想指出所选数据与数学中的传统矩阵的相似之处。这些类型的选择将在 PromQL 中称为范围向量。
在本章中,我们开始了解什么是时间序列数据,并概述了现代时间序列数据库(如 Prometheus)的工作原理,不仅在逻辑上,而且在物理上也是如此。我们了解了 Prometheus 指标符号以及指标名称和标签之间的关系,还介绍了样本的定义。 Prometheus 指标有四种类型,我们有机会逐一介绍并提供一些有用的示例。最后,我们深入研究了纵向和横向聚合的工作原理,这对于充分利用 Prometheus 的查询语言至关重要。
在下一章中,我们将返回到更实际的方法并进入 Prometheus 服务器配置,以及如何在虚拟机和 Kubernetes 上管理它。