深入理解和使用 Prometheus 的 Histogram 指标类型

6,708 阅读11分钟

我正在参加「掘金·启航计划」

现在的微服务系统中,链路上服务的性能是非常重要的,因为一个服务变慢,就会拖慢链路上的所有相关请求,这直接关系到系统的性能和给用户的体验。

对服务性能进行优化的基础是,我们需要先进行对应数据的采集和观测。这样才能发现系统中的瓶颈点。

Prometheus 是我们常用的监控服务的开源组件。Prometheus 是由前 Google 工程师从 2012 年开始在 Soundcloud 以开源软件的形式进行研发的系统监控和告警工具包,自此以后,许多公司和组织都采用了 Prometheus 作为监控告警工具。Prometheus 现在是一个独立的开源项目,开发者和用户社区非常活跃。

而在Prometheus的各种指标类型中,Histogram类型是很重要但是比较难理解一些的,所以这篇文章主要想对 Prometheus 系统中比较复杂的 Histogram 数据类型进行讲述。

我们先来看看 Prometheus 采集的数据格式是什么样的。

一、Prometheus的数据格式和指标类型

数据格式

Prometheus 会将所有采集到的样本数据以时间序列的方式保存在内存数据库中,并且定时保存到硬盘上。时间序列是按照时间戳和值的序列顺序存放的,我们称之为向量(vector),每条时间序列通过指标名称(metrics name)和一组标签集(labelset)命名。如下所示,可以将时间序列理解为一个以时间为 X 轴的数字矩阵:

  ^
  │   . . . . . . . . . . . . . . . . .   . .   node_cpu_seconds_total{cpu="cpu0",mode="idle"}
  │     . . . . . . . . . . . . . . . . . . .   node_cpu_seconds_total{cpu="cpu0",mode="system"}
  │     . . . . . . . . . .   . . . . . . . .   node_load1{}
  │     . . . . . . . . . . . . . . . .   . .
  v
    <------------------ 时间 ---------------->

在时间序列中的每一个点称为一个样本(sample),样本由以下三部分组成:

指标(metric):指标名和描述当前样本特征的标签集合 时间戳(timestamp):一个精确到毫秒的时间戳 样本值(value): 一个 float64 的浮点型数据表示当前样本的值

<--------------- metric ---------------------><-timestamp -><-value->
http_request_total{status="200", method="GET"}@1434417560938 => 94355
http_request_total{status="200", method="GET"}@1434417561287 => 94334

http_request_total{status="404", method="GET"}@1434417560938 => 38473
http_request_total{status="404", method="GET"}@1434417561287 => 38544

http_request_total{status="200", method="POST"}@1434417560938 => 4748
http_request_total{status="200", method="POST"}@1434417561287 => 4785

指标类型

指标类型一般分为四类。

Counter 计数器,用于保存计数型数据,如网站访问量等。

Gauge 仪表盘,用于存储有着起伏特征的指标数据,如空间空闲大小等。

Summary 摘要,Histogram的扩展类型,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间计算

Histogram 直方图,在一段时间范围内对数据进行采样,并将其计入可配置的存储中,后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。

Counter 和 Gauge比较好理解,而 Histogram 类型则是相对来说复杂一些的。理解 Histogram 类型是能够帮我们更好地使用 Prometheus 监控,尤其是对于调用耗时这一类需要设置分位监控的数据。

二、Histogram类型

Histogram 中文的含义是直方图,我们在学习概率统计的时候都学习过直方图。直方图是对数据如何分布的一种总结方式——有多少值是高的,有多少是低的,有多少介于两者之间。

截屏2022-10-10 下午6.09.55.png

如图所示,Histogram 显示了数据集的分布。有些值低(<=10),有些值中等(>10 和 <=100),有些值高(>100 和 <=1000)。直方图根据这些范围将数据分组到存储桶bucket中,并计算每个存储桶中有多少值。这使我们对数据如何在其值范围内分布有所了解。在决定如何绘制直方图时,通常会选择对数据敏感且对分析有意义的bucket范围。

关于Prometheus 的 Histogram 类型

现在来让我们看一下 Prometheus Histogram。 Prometheus Histogram在三个方面与上述示例略有不同:

  1. 桶是累积的——也就是说,每个桶包含的值小于或等于桶的上限阈值。
  2. Prometheus 直方图度量也是一个时间序列——我们上面说的例子可以认为是 Prometheus 直方图在某一时刻的记录。但是,Prometheus 会随着时间的推移一直记录这些直方图,因此在开始查询时需要理解了这些才能写查询语句。
  3. 时间序列本身是累积的 - 直方图中的桶总是在增加,因此直方图的最新实例显示自首次记录指标以来每个桶的总值。

让我们依次关注这些差异。

1. 桶是累积的

在上面的示例中,每个存储桶都不包含任一侧的值。小于或等于 10 的值仅出现在 <=10 的存储桶中,而不会出现在其他任何存储桶中。

而Prometheus 直方图是累积的。在 Prometheus 中,我们上面的示例将有不同的桶:<=10、<=100 和 <=1000。

截屏2022-10-10 下午6.58.14.png

现在可以看到,每个桶都比之前的大,并且包含所有值的总和,直到桶的阈值。从左往右来看,一定是非递减的。

2. Prometheus 直方图是时间序列

Prometheus 每隔一段时间从进程中抓取指标。每次它抓取一个直方图指标时,它都会收到一个类似于上面的直方图——一个带有“小于或等于”桶的累积直方图。

重要的是当查询直方图时,需要处理直方图的时间序列。直方图指标本身是包含一个值范围的——每个发生数据收集的时间点会有一个值——每个这样的值代表一个类似于上面的直方图。

每个直方图值(从上一次收集到这次收集的时间间隔)总结了自上次收集以来进程记录的值的分布。

3. 时间序列本身是累积的

每次 Prometheus 抓取直方图时,这些值都不会重置。这意味着每个桶中的计数在指标的整个生命周期内是累积的(至少在每个进程的内存中),并且实际上每个桶的值的变化告诉我们自上次抓取以来观察值的分布。

三、使用示例

从实际的使用中来看Histogram,更好理解一些。

package main

import (
   "log"
   "math/rand"
   "net/http"
   "time"

   "github.com/prometheus/client_golang/prometheus"
   "github.com/prometheus/client_golang/prometheus/promauto"
   "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
   // 定义一个Histogram类型的指标
   histogram := promauto.NewHistogram(prometheus.HistogramOpts{
      Name:    "histogram_showcase_metric",
      Buckets: []float64{5.0, 10.0, 20.0, 50.0, 100.0}, // 根据场景需求配置bucket的范围
   })

   go func() {
      for {
         // 这里搜集一些0-100之间的随机数
         // 实际应用中,这里可以搜集系统耗时等指标
         histogram.Observe(rand.Float64() * 100.0)
         time.Sleep(1 * time.Second)
      }
   }()
   // 指标上报的路径,可以通过该路径获取实时的监控数据
   http.Handle("/metrics", promhttp.Handler())
   log.Fatal(http.ListenAndServe(":8080", nil))
}

上面这段程序做的事情就是 模拟了一个后端服务的接口,每次调用都会有不同的耗时(这里使用随机数生成),耗时的数据会被Prometheus的客户端采集。

通过 /metrics 路径,可以看到数据采集的情况:

截屏2022-10-10 上午11.36.41.png

如果本地安装了Prometheus,并且配置了对应的服务采集地址,那么 就可以在通过Prometheus自带的可视化界面,看到如下收集的数据

截屏2022-10-09 下午6.43.55.png

截屏2022-10-10 上午11.19.52.png

可以看到 histogram_showcase_metric这个指标的数据已经被搜集了。我们的配置中,分了<=10, <=20, <=30, <=40, <=50 这5个桶,默认还有+Inf这个桶,数据肯定落在这6个桶中。

好的,那么当我们查询指标时我们在看什么?有几个关键点需要注意:

我们正在查看一个瞬时向量,这意味着我们看到的是 Prometheus 抓取的最新一组值(而不是一段时间内的一系列值)。换句话说,我们正在查看一个累积直方图,就像我们上面讨论的一样。

  • 每次 Prometheus 抓取直方图时,它都会收集一个像这样的即时向量。
  • {} 中的指标名称之后是一组标签,它们为直方图的数据添加维度(您可以根据标签标准过滤值)。
  • 其中最重要的是 le 标签,它是“小于或等于”桶阈值。
  • le=1 的值是所有观察值 <=1 的累积计数。其他 le 值也是如此,这就是为什么计数总是随着 le 值的增加而增加,所有的桶都是累积的。
  • 因此,现在我们对每个桶中的所有观察结果进行了累积计数。虽然(希望)到目前为止是有道理的,但它对于帮助我们理解在我们的仪器化程序中所做的观察的潜在分布并不是特别有用。我们需要一种查询直方图的好方法。

那么我们如何进行查询呢:

首先,从上面的指标收集图中,可以看到 Histogram 类型的数据还会收集了一个 _count 类数据,我们可以用它来了解每秒监控的频率。可用于观测每秒收到多少请求。使用以下 Prometheus 查询可以进行观察:

rate(histogram_showcase_metric_count[1m])

此查询获取最后一分钟 ([1m]) 的观察结果并计算计数的每秒变化率,从而为我们提供每秒的请求次数。

观测分布

这是实际使用中,我们最需要 Histogram 的地方。我们有一些高频事件(再次以Web请求来举例),会在 Prometheus 的每个抓取间隔之间进行了大量数据采集(例如请求持续时间),然后使用Histogram来记录观察结果,我们想查询该度量,以深入了解值在抓取间隔内和随时间的分布情况。

现在来逐步构建查询。首先,使用 histogram_showcase_metric_bucket 查询指标:

截屏2022-10-10 下午3.24.05.png

然后,使用 histogram_showcase_metric_bucket[1m] 从最后一分钟的指标中获取一系列值:

截屏2022-10-10 下午3.46.02.png

现在有了每个 bucket 的累积计数,但是在最后一分钟的抓取间隔内。

通过使用 rate(histogram_showcase_metric_bucket[1m]) 查看每个 bucket 的每秒变化率来了解这些 bucket 是如何随时间变化的:

截屏2022-10-10 下午4.07.07.png

这里展示了在最后一分钟在每个 bucket 中进行观测的频率。

最后,我们可以使用 Prometheus 的函数 histogram_quantile() 将这些信息转化为更直观有价值的数据。因为我们有每个直方图 bucket 的变化率,Prometheus 现在可以计算出哪个 bucket 标签包含给定的分位数(例如第 95 个百分位数)。这意味着我们现在可以找出数据中表示给定分位数的近似值。例如

histogram_quantile(0.95, rate(histogram_showcase_metric_bucket[1m]))

截屏2022-10-10 下午4.09.38.png

在这里可以看到,在最后一分钟,第 95 个百分位处的观测值的近似值为 48.125。这里因为我们的耗时是0-50模拟随机生成的,所以这个近似值是非常合理的。

截屏2022-10-11 上午11.21.29.png

每次 Prometheus 采集 Histogram 的值时,它都会拉回其中中所有桶的累积计数(就像本文开头的示例一样)。随着时间的推移(例如本例中的一分钟间隔),会积累大量的数据,而在时间序列(例如显示过去 6 小时的图表)中,是需要拉取大量数据,这也引入了时间范围过大时查询速度慢的问题,当然这里就不继续探究这个问题了。

四、使用建议

  1. 接口的QPS

接口的QPS = query per second, 每秒接收的请求量,这个是反映业务指标的。

我们一般用counter定义指标收集,但是也可以直接对 Histogram 收集的count进行查询处理。

rate(histogram_showcase_metric_count[1m])

查询的时候,通过rate或者irate进行查询。

  1. 接口的耗时

接口的耗时,是反映接口性能的最关键因素。因为现在后端服务的请求链路都是比较长的,所以做好延时的监控对于问题排查和后续优化有很重要的指导意义。

截屏2022-10-11 上午10.58.43.png

用Histogram定义指标收集是比较合适的一种方式,这里上面已经提到过了。一般来说,我们会监控核心接口的p90,p95和p99耗时,这些都可以通过histogram_quantile函数来对 Histogram类型的数据进行查询直接得到。

五、总结

文章主要讲述了 Prometheus 的 Histogram 这个指标类型的原理定义,以及如何对该类型指标进行查询。熟练用好Histogram类型的指标,可以为我们的后端服务建立非常有价值的监控系统,从而指导服务的性能优化。