Counter(计数器)

Counter 类型代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置。例如,你可以使用 counter 类型的指标来表示服务的请求数、已完成的任务数、错误发生的次数等。counter 主要有两个方法:

1
2
3
4
//将counter值加1.
Inc()
// 将指定值加到counter值上,如果指定值<0 会panic.
Add(float64)

Counter 类型数据可以让用户方便的了解事件产生的速率的变化,在 PromQL 内置的相关操作函数可以提供相应的分析,比如以 HTTP 应用请求量来进行说明:

1
2
3
4
//通过rate()函数获取HTTP请求量的增长速率
rate(http_requests_total[5m])
//查询当前系统中,访问量前10的HTTP地址
topk(10, http_requests_total)

不要将 counter 类型应用于样本数据非单调递增的指标,例如:当前运行的进程数量(应该用 Guage 类型)。

不同语言关于 Counter 的客户端库使用文档:

Guage(仪表盘)

Guage 类型代表一种样本数据可以任意变化的指标,即可增可减。guage 通常用于像温度或者内存使用率这种指标数据,也可以表示能随时增加或减少的“总数”,例如:当前并发请求的数量。

对于 Gauge 类型的监控指标,通过 PromQL 内置函数 delta() 可以获取样本在一段时间内的变化情况,例如,计算 CPU 温度在两小时内的差异:

1
dalta(cpu_temp_celsius{host="zeus"}[2h])

你还可以通过PromQL 内置函数 predict_linear() 基于简单线性回归的方式,对样本数据的变化趋势做出预测。例如,基于 2 小时的样本数据,来预测主机可用磁盘空间在 4 个小时之后的剩余情况:

1
predict_linear(node_filesystem_free{job="node"}[2h], 4 * 3600) < 0

不同语言关于 Guage 的客户端库使用文档:

Histogram(直方图)

在大多数情况下人们都倾向于使用某些量化指标的平均值,例如 CPU 的平均使用率、页面的平均响应时间。这种方式的问题很明显,以系统 API 调用的平均响应时间为例:如果大多数 API 请求都维持在 100ms 的响应时间范围内,而个别请求的响应时间需要 5s,那么就会导致某些 WEB 页面的响应时间落到中位数的情况,而这种现象被称为长尾问题。

为了区分是平均的慢还是长尾的慢,最简单的方式就是按照请求延迟的范围进行分组。例如,统计延迟在 0~10ms 之间的请求数有多少而 10~20ms 之间的请求数又有多少。通过这种方式可以快速分析系统慢的原因。Histogram 和 Summary 都是为了能够解决这样问题的存在,通过 Histogram 和 Summary 类型的监控指标,我们可以快速了解监控样本的分布情况。

Histogram 在一段时间范围内对数据进行采样(通常是请求持续时间或响应大小等),并将其计入可配置的存储桶(bucket)中,后续可通过指定区间筛选样本,也可以统计样本总数,最后一般将数据展示为直方图。

Histogram 类型的样本会提供三种指标(假设指标名称为 <basename>):

  • 样本的值分布在 bucket 中的数量,命名为 <basename>_bucket{le="<上边界>"}。解释的更通俗易懂一点,这个值表示指标值小于等于上边界的所有样本数量。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      // 在总共2次请求当中。http 请求响应时间 <=0.005 秒 的请求次数为0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0
      // 在总共2次请求当中。http 请求响应时间 <=0.01 秒 的请求次数为0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0
      // 在总共2次请求当中。http 请求响应时间 <=0.025 秒 的请求次数为0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0
      // 在总共2次请求当中。http 请求响应时间 <=10 秒 的请求次数为 2
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0
      io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0
    
  • 所有样本值的大小总和,命名为 <basename>_sum。

    1
    2
    
      // 实际含义: 发生的2次 http 请求总的响应时间为 13.107670803000001 秒
      io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
    
  • 样本总数,命名为 <basename>_count。值和 <basename>_bucket{le="+Inf"} 相同。

    1
    2
    
    // 实际含义: 当前一共发生了 2 次 http 请求
    io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0
    

注意:bucket 可以理解为是对数据指标值域的一个划分,划分的依据应该基于数据值的分布。注意后面的采样点是包含前面的采样点的,假设 xxx_bucket{…,le=“0.01”} 的值为 10,而 xxx_bucket{…,le=“0.05”} 的值为 30,那么意味着这 30 个采样点中,有 10 个是小于 10 ms 的,其余 20 个采样点的响应时间是介于 10 ms 和 50 ms 之间的。

可以通过 histogram_quantile() 函数来计算 Histogram 类型样本的分位数。分位数可能不太好理解,你可以理解为分割数据的点。我举个例子,假设样本的 9 分位数(quantile=0.9)的值为 x,即表示小于 x 的采样值的数量占总体采样值的 90%。Histogram 还可以用来计算应用性能指标值(Apdex score)。

不同语言关于 Histogram 的客户端库使用文档:

Summary(摘要)

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

Summary 类型的样本也会提供三种指标(假设指标名称为 ):

  • 样本值的分位数分布情况,命名为 <basename>{quantile="<φ>"}。

    1
    2
    3
    4
    
      // 含义:这 12 次 http 请求中有 50% 的请求响应时间是 3.052404983s
      io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983
      // 含义:这 12 次 http 请求中有 90% 的请求响应时间是 8.003261666s
      io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
    
  • 所有样本值的大小总和,命名为 <basename>_sum。

    1
    2
    
    // 含义:这12次 http 请求的总响应时间为 51.029495508s
    io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
    
  • 样本总数,命名为 <basename>_count。

    1
    2
    
    // 含义:当前一共发生了 12 次 http 请求
    io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0
    

Histogram 与 Summary

  • 它们都包含了 <basename>_sum 和 <basename>_count 指标
  • Histogram 需要通过 <basename>_bucket 来计算分位数,而 Summary 则直接存储了分位数的值。

计算百分位数(quantile)

Prometheus中称为quantile,其实叫percentile更准确。百分位数是指小于某个特定数值的采样点达到一定的百分比。例如,假设0.9-quantile的值为120,意思就是所有的采样值中,小于120的采样值的数量占总体采样值的90%。相应的,假设0.5-quantile的值为x,那么意思就是指小于x的采样值占总体的50%,所以0.5-quantile也指中值(median)。

相对于简单的平均值来说,百分位数更丰富,更能反应出真实的用户体验。常用的百分位数为0.5-quantile,0.9-quantile以及0.99-quantile。这也是Prometheus默认的设置。

注:这只是Prometheus中Summary目前版本的默认设置,在版本v0.10中,这些默认值会废弃,意味着默认的Summary将没有quantile设置。

用Summary计算quantile

Summary是Prometheus在client端支持的四种metrics类型之一。每定义一个Summary类型的metrics,实际会生成几个metrics。例如,下面的summary是用于监控http请求的响应时间,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
httpCallDurations = prometheus.NewSummaryVec(

prometheus.SummaryOpts{

Name:       "http_request_duration_millisecond",

Help:       "http request durations in millisecond.",

Objectives: map[float64]float64,

},

[]string{path"})

上面的summary实际上会生成5类metrics,分别如下。后面三个就是百分位数值。

1
2
3
4
5
6
7
8
9
http_request_duration_millisecond_count

http_request_duration_millisecond_sum

http_request_duration_millisecond

http_request_duration_millisecond

http_request_duration_millisecond

眼尖的同学可能已经注意到每个quantile后面还有一个数,0.5-quantile后面是0.05,0.9-quantile后面是0.01,而0.99后面是0.001。这些是我们设置的能容忍的误差。0.5-quantile: 0.05意思是允许最后的误差不超过0.05。假设某个0.5-quantile的值为120,由于设置的误差为0.05,所以120代表的真实quantile是(0.45, 0.55)范围内的某个值。

之所以要设置误差,原因很简单,就是用一定的误差换取内存空间和CPU计算能力。换句话说,如果不允许有误差,Prometheus为了计算精确的百分位数,需要缓存并处理所有的采样值,从而可能要消耗大量的内存和计算资源,而设置一定了误差,则可以减缓内存和CPU的消耗。

使用Summary要注意一点,就是不能对Summary产生的quantile值进行aggregation运算(例如sum, avg等)。例如有两个实例同时运行,都对外提供服务,分别统计各自的响应时间。最后分别计算出的0.5-quantile的值为60和80,这时如果简单的求平均(60+80)/2,认为是总体的0.5-quantile值,那么就错了。如果你闭上眼睛,简单思考一下,就会明白对两个quantile值求平均毫无意义。所以如果需要对多个实例的quantile值进行aggregation操作,那么就不能使用Summary。

用Histogram计算quantile

Histogram也是Prometheus在client端支持的四种metrics类型之一。与Summary类似,每定义一个Histogram类型的metrics,实际也会生成几个metrics。例如,

1
2
3
4
5
6
7
8
9
metric := prometheus.NewHistogramVec(prometheus.HistogramOpts{

Name: "http_request_duration_millisecond",

Help:  "http request durations in millisecond.",

Buckets: []float64,

}, []string{path})

上面的Histogram会产生下面6类metrics。后面4个可以用于计算quantile值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
http_request_duration_millisecond_count

http_request_duration_millisecond_sum

http_request_duration_millisecond_bucket

http_request_duration_millisecond_bucket

http_request_duration_millisecond_bucket

http_request_duration_millisecond_bucket

Histogram主要是设置不同的bucket,采用值分别落入不同的bucket。例如上面第一个bucket就是响应时间小于10ms的采样点的数量,第二个bucket就是响应时间小于50ms的采样点的数量,依此类推。

注意后面的采样点是包含前面的采用点的,例如xxx_bucket的值为30,而xxx_bucket的值为120,那么意味着这120个采用点中,有30个是小于10ms的,其余90个采样点的响应时间是介于10ms和50ms之间的。

注意+Inf是最高bucket的上限值,所以xxx_bucket是所有采样点的数量,是Prometheus自动增加的一个bucket。

计算quantile值直接用函数histogram_quantile即可,例如下面是计算0.9-quantile的值,

1
histogram_quantile(0.9, rate(http_request_duration_milliseconds_bucket[10m]))

上面会针对每种label组合计算出一个0.9-quantile值,也就是对每个"path"会计算出一个值。如果要针对所有path计算出一个汇总的值,则用如下语句,

1
histogram_quantile(0.9, sum(rate(http_request_duration_milliseconds_bucket[10m])) by (le))

使用Histogram计算quantile值,最大的问题就是:因为Histogram采用了线性插值法,所以如果bucket设置不合理,那么最后计算出的值可能偏差比较大。例如在前面的例子中,假设0.9-quantile的结果在10ms~50ms之间,但是表达式必须返回一个具体的值,这时就采用线性插值法得出36ms。显然这种方法计算出的值可能会有误差,而且范围越大,例如10ms ~ 500ms,那么误差也会越大。

1
50-10*0.9=36ms

Summary和Histogram对比

根据前面分别对Summary和Histogram的描述,很显然Summary和Histogram计算quantile有很大的差别。

另外,它们之间一个重要的区别在于,Summary对quantile的计算是在client端完成的,而Histogram对quantile的计算是在server端完成的。这里client端是指使用了prometheus client library的模块。server端自然是指prometheus server。

分析client library中对Summary的实现源码,不难发现summary对quantile的计算是依赖第三方库perk实现的,

1
github.com/beorn7/perks/quantile

而perks采用的算法主要来自下面的一篇论文,对算法感兴趣的同学可以自行研究,

http://www.cs.rutgers.edu/~muthu/bquant.pdf

而Histogram对quantile的计算是在prometheus server端进行的,也就是前面讲的对histogram_quantile函数的计算是在server端完成的。所以很显然,client端处理summary的消耗比Histogram大,server端则正好反过来。

结合前面的描述,Summary和Histogram对quantile的处理的区别可以总结如下:

  • Summary不能对quantile值进行aggregation操作,而Histogram则可以;所以如果针对多实例的场景计算quantile,只能使用Histogram;
  • 如果histogram的bucket设置不合理,则最后误差可能会很大;所以如果需要相对精确的结果,而且是单实例场景,那么就使用Summary;
  • Summary对quantile的计算是在client端通过第三方库perks做的;而Histogram对quantile的计算则是server端完成的;
  • Summary计算出的quantile值是基于进程开始运行至今的所有采样值计算出来的;而Histogram则是基于最近的一段时间的采样值计算出来的,更符合monitoring系统的本质。

参考: 使用Prometheus计算百分位数值