Why prometheus

实时指标是监控中最重要的一环,用来尝出实时监控图表以及触发警报。传统的指标收集方式是通过采集的日志流处理写入时序数据库(比如Druid),这样做的问题一是流程比较长,任何一环出问题就会导致监控不可用,而警报对稳定性的要求是非常高得;二是存储成本和查询速度,比如我将每次的请求耗时保存下来,那么每次请求都是一条记录,一天下来可能就有千万级别的记录,查询时对机器的性能会有一定的要求。

Prometheus直接采集服务暴露的指标,少了中间流程能大大减少出问题的概率。另外通过提供client在采集端做了预聚合,虽然这样损失了精确度,但大大减少数据量以及提升查询速度。

Prometheus所做的预聚合使得其和传统的时序数据库查询完全不同,所以认识和理解指标是使用Prometheus的第一步。下面我会解释各种指标类型以及如何去使用。

Metric types

下面我主要会以Prometheus的自监控指标举例,你可以自己安装一个来尝试。

Counter

Counter可以简单理解为计数器,是个比较简单但又常用的类型。适用于生成请求次数、错误次数等指标。

比如prometheus暴露的http请求次数指标如下(打开/metrics查看)

# HELP prometheus_http_requests_total Counter of HTTP requests.
# TYPE prometheus_http_requests_total counter
prometheus_http_requests_total{code="200",handler="/api/v1/label/:name/values"} 7
prometheus_http_requests_total{code="200",handler="/api/v1/query"} 19
prometheus_http_requests_total{code="200",handler="/api/v1/query_range"} 27
prometheus_http_requests_total{code="200",handler="/graph"} 11
prometheus_http_requests_total{code="200",handler="/metrics"} 8929
prometheus_http_requests_total{code="200",handler="/static/*filepath"} 52
prometheus_http_requests_total{code="302",handler="/"} 1
prometheus_http_requests_total{code="400",handler="/api/v1/query_range"} 6

指标由指标名、花括号里的labels以及指标值组成,labels可以理解为维度,上面的数据写成表格就很好理解了。

code handler prometheus_http_requests_total
200 /api/v1/label/:name/values 7
200 /api/v1/query_range 27
200 /graph 11
200 /metrics 8929
200 /static/*filepath 52
302 / 1
400 /api/v1/query_range 6

我们可以通过/graph页面来查询这个指标,直接查询如下的instant query就可以看到过去一段时间每个采集点(默认15s)采集到的counter值。

prometheus_http_requests_total

用例

Counter类型直接查询没有什么意义,通常我们要看的是过去一段时间的统计数据,可以这么查

prometheus_http_requests_total - prometheus_http_requests_total offset 1h

打开Console Tab,就能看到过去一个小时内prometheus http请求的次数统计了。

时间粒度

如果熟悉传统的时序数据库查询的话,会更习惯指定时间粒度来聚合一个时间窗口内的数据。prometheus的counter指标里倒不需要做这样聚合,但为了使结果显示成每个时间粒度一个点而不是像现在的十几秒一个点,可以使用step来指定精度。

假设我们要查过去一小时内每5分钟有多少次请求可以这么写。这里使用更常用的increase函数和range query

increase(prometheus_http_requests_total[5m])

然后将Res里填上300就能得到想要的结果了。

按维度聚合

除了时间粒度外还有个很常用的参数叫group by,用来按某些维度来分组聚合指标。在prometheus里做起来差不多,不过稍有限制。我们先看看怎么计算所有http请求的和。

sum(increase(prometheus_http_requests_total[5m]))

这里将上节的promql加了一层sum函数,可以看到结果只有一条线了,这条线上的每一个点就是当前时间上所有codehandler对应指标的总和。

那么如何看每个code对应的请求数呢?我们可以通过by来实现,写法很像sql。

sum(increase(prometheus_http_requests_total[5m])) by (code)

要注意的是这里by只能跟在聚合函数后面,见文档

Gauge

Gauge是一个用来记录实时值的指标,常用于表示CPU使用率、内存使用率、线程数等指标。

比如prometheus暴露的go协程数指标

# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 40

用例

gauge类型指标最常见的就是用来标识服务是否存活的up指标了,这个指标在大多的exporter上都会有,属于一个可以建通用警报规则的指标。

大多数gauge指标用法差不多,我就拿go_goroutines来举例。查询go_goroutines我们可以看到一些自动生成的labels,主要关注instance,这个代表了我们的prometheus实例。假设我们有prometheus实例并且想查询所有实例的平均协程数。

avg(go_goroutines)

熟悉Prometheus后你会发现 avg 基本只对 gauge 指标有意义

按时间聚合

上面的结果看起来是满足要求,但是多查几次就发现好像结果有点不太一样?(如果你看不出来可以将Res调大)

原因是prometheus的精度问题,毕竟只有15s一个点,那么我要算某个时间点的平均值肯定是无法拿到准确值的。为了减小数据偏差,可以先横向将一段时间内的数据聚合,再纵向算平均值。比如先以1分组为粒度算出每分钟的平均数,再算出所有实例的平均数。

avg(avg_over_time(go_goroutines[1m]))

预测磁盘空间

这是个很有意思的使用场景,可以通过磁盘使用空间来预测。因为磁盘使用空间不像内存使用量和线程数那样变化频繁,具有一定的局部单调性,所以可以通过线性回归预测,prometheus提供了predict_linear来帮助计算。

# 利用过去30分钟的数据预测1小时后的数据
predict_linear(disk_used_bytes[30m], 3600)

Histogram

顾名思义该指标生成的是直方图数据。

prometheus暴露的请求耗时指标如下

# HELP prometheus_http_request_duration_seconds Histogram of latencies for HTTP requests.
# TYPE prometheus_http_request_duration_seconds histogram
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.1"} 60
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.2"} 63
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="0.4"} 64
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="1"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="3"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="8"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="20"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="60"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="120"} 65
prometheus_http_request_duration_seconds_bucket{handler="/api/v1/query_range",le="+Inf"} 65
prometheus_http_request_duration_seconds_sum{handler="/api/v1/query_range"} 2.5237541999999995
prometheus_http_request_duration_seconds_count{handler="/api/v1/query_range"} 65
# 其他labels下的都删掉了

prometheus_http_request_duration_seconds使用了[0.1,0.2,0.4,1,3,8,20,60,120,+Inf]这几个分桶来采样数据。

用例

通常我们会使用该数据来计算百分位数

比如我们可以通过以下的promql查询prometheus_http_request_duration_seconds在过去1小时内的P95(95%的请求耗时都小于等于这个值)。

histogram_quantile(0.95, rate(prometheus_http_request_duration_seconds_bucket[1h]))

如果出现了NaN数据代表数据量不够,无法计算。

如果有多个prometheus实例,同样可以聚合计算P95

# 注意group by le,防止把分桶信息也聚合了
histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[1h])) by (le))

Summary

Summary类型是在客户端直接聚合生成的百分位数。

比如Prometheus抓取间隔的百分位数指标

# HELP prometheus_target_interval_length_seconds Actual intervals between scrapes.
# TYPE prometheus_target_interval_length_seconds summary
prometheus_target_interval_length_seconds{interval="15s",quantile="0.01"} 14.9986389
prometheus_target_interval_length_seconds{interval="15s",quantile="0.05"} 14.9991762
prometheus_target_interval_length_seconds{interval="15s",quantile="0.5"} 15.0000115
prometheus_target_interval_length_seconds{interval="15s",quantile="0.9"} 15.0006213
prometheus_target_interval_length_seconds{interval="15s",quantile="0.99"} 15.0010902
prometheus_target_interval_length_seconds_sum{interval="15s"} 178380.0282111002
prometheus_target_interval_length_seconds_count{interval="15s"} 11892

虽然Histogram也能计算百分位数但精度受分桶影响很大,分桶少的话会使百分位数计算很不准确,而分桶多的话会使数据量成倍增加。Summary则是依靠原始数据计算出的百分位数,是很准确的值。

但是平时一般不用Summary,因为它无法聚合。想象一下,prometheus抓取了一个集群下多台机器的百分位数,我们怎么根据这些数据得到整个集群的百分位数呢?如果是P0(最小值)和P100(最大值)是可以计算,分别计算所有机器P0的最小值以及P100的最大值就行,但是其他百分位数就束手无策了。

所以除非你真的需要精确的百分位数,否则不建议使用Summary

Reference