prometheus client用法与原理

前一阵看k8s的时候接触了prometheus,当时感觉它的查询语法promql还是挺难理解的,所以时隔一个月的时间决定再回头找找思路。

另外呢,我觉得prometheus非常实用,结合grafana展示炫酷的大盘报表,这是一个非常实在的技能。

下面拿python client为例,讲讲我对Prometheus的理解。

项目

python的代码放在这个项目里:https://github.com/owenliang/prometheus-py,下面是所有代码:

其中start_http_server在8000端口监听HTTP服务,供prometheus抓取metrics。

下面我们围绕代码来讲一下prometheus使用的关键概念。

客户端用法与原理

客户端可以使用4种metrics类型,我们一个一个说。

counter

一直累加的计数器,不可以减少。

定义它需要2个参数,第一个是metrics的名字,第二个是metrics的描述信息:

它的唯一方法就是inc,只允许增加不允许减少:

counter适合用来记录访问总次数之类的,通过promql可以计算counter的增长速率,即可以得到类似的QPS诸多指标。

调用的时候这样即可:

抓取到的metrics这样:

#HELP是cc的注释说明,我们刚才定义的时候指定的,#TYPE说明cc是一个counter。

其实cc这个counter被输出为cc_total,对应的累加值是46.0。

cc_created这个输出的TYPE是gauge类型,记录了cc这个metrics的创建时间,下面我们就说gauge类型是啥。

gauge

和counter略有不同。

gauge可增可减,可以任意设置,就代表了某个指标当前的值而已。

比如可以设置当前的CPU温度,内存使用量等等,它们都是上下浮动的,不是只增不减的。

定义和counter基本一样:

第一个是metrics的名字,第二个是描述。

它支持3个方法:

因为是任意的值,所以可以inc/dec,也可以set。

最后就是调用方法,我这里每次设置一个随机值,其值可以是任意浮点数:

输出也类似:

名字就是gg,TYPE是gauge。

我认为gauge的大部分用法就是直接拿来画图就好了,不需要做promql处理。

histogram

这种主要用来统计百分位的,什么是百分位?英文叫做quantiles。

比如你有100条访问请求的耗时时间,把它们从小到大排序,第90个时间是200ms,那么我们可以说90%的请求都小于200ms,这也叫做”90分位是200ms”,能够反映出服务的基本质量。当然,也许第91个时间是2000ms,这就没法说了。

实际情况是,我们每天访问量至少几个亿,不可能把所有访问数据都存起来,然后排序找到90分位的时间是多少。因此,类似这种问题都采用了一些估算的算法来处理,不需要把所有数据都存下来,这里面数学原理比较高端,我们就直接看看prometheus的用法好了。

首先定义histogram:

第一个是metrics的名字,第二个是描述,第三个是分桶设置,重点说一下buckets。

这里(-5,0,5)实际划分成了几种桶:<=-5,<=0,<=5,<=无穷大。

如果我们喂给它一个-8:

那么metrics会这样输出:

hh_sum记录了observe的总和,count记录了observe的次数,bucket就是各种桶了,le表示<=某值。

可见,值8<=无穷大,所以只有最后一个桶计数了1次(注意,桶只是计数,bucket作用相当于统计样本在不同区间的出现次数)。

bucket的划分需要我们根据数据的分布拍脑袋指定,合理的划分可以让promql估算百分位的时候更准确,我们使用histogram的时候只需要知道先分好桶,再不断的打点即可,最终百分位的计算可以基于histogram的原始数据完成。

我们不停的随机产生-5到5之间的打点给histogram:

每次prometheus来scrape抓走当前的metrics长相如下:

这些都叫做Instant-vector,瞬时向量。

到prometheus后台可以查出来最新的一组hh瞬时向量,只有标签不同(划分区间):

比无穷大小的有52次,说明一共就打点了52次。

比-5小的只有3次,比0小的29次,可以算出-5~0之间的是29-3=26次。

同样可以算出,0~5之间的是52-29=23次。

对这一组瞬时向量可以进行百分位的估算,比如我们要估算90分位的值是多少:

可见90分位的数值大概是3.9657534246575348,也就是90%的打点都比3.9小,感觉还是比较合理的哈,因为我们的随机数的上限就是5。

histogram这种metrics分桶计数的方式,在prometheus服务端做promql估算百分位,其估算准确度受限于分桶的合理性,如果桶分的不好,那估算的值就很不准了,这个大家慢慢摸索吧。

summary

因为histogram在客户端就是简单的分桶和分桶计数,在prometheus服务端基于这么有限的数据做百分位估算,所以的确不是很准确,summary就是解决百分位准确的问题而来的。

summary相当于把服务端的算法放在客户端实现,客户端打点的同时直接计算百分位。

这要求客户端提前定义好你想计算哪些百分位(就像histogram定义好每个桶的区间一样),这样客户端会直接算出精度很高的百分位值,直接给prometheus抓走使用即可。

python客户端没有完整实现summary算法,其实summary把算法搬到了客户端实现带来了很严重的性能问题,因为histogram仅仅是给每个桶做一个原子变量的计数就可以了,而summary要每次执行算法计算出最新的X分位value是多少,算法需要并发保护,所以对并发程序的性能影响就很大了,这可能也是python没实现它的原因之一吧。

另一个原因,可能是因为summary不灵活,因为百分位是提前在客户端里指定的,而histogram则可以通过promql随便指定,虽然计算的不如summary准确,但带来了灵活性。

所以summary就不展开说明了。

服务端用法与原理

接下来说说服务端,我觉得有几个理解意义重大。

关于instant-vector

这两种类型的vector什么时候用很容易乱,我说说我的理解。

最容易产生误解的地方就是:认为画graph需要用range-vector,这是第一次了解prometheus很大的误区。

我们以为画一条曲线需要很多很多的point才能串起来,所以理所当然认为画图应该是用range-vector来取N个point,所以很容易认为应该用cc[5m]这样的range-vector,这是完全错误的!

实际画曲线用的是instant-vector!

prometheus画图的时候,会往过去的时间重复的后退N秒,取每一次的instant-vector出来,最后用这些不同时间点的instant-vector来画线。

这是最新的一条cc_total instant-vector:

保持该promql不变直接切换到graph可以画出曲线,下方可以看到ajax请求,请求告知prometheus,每间隔14秒计算一次promsql得到instant-vector,整个时间窗口是start到end:

返回的数据长这样:

所以,prometheus的API可以按照一定的分辨率(也叫做resolution,这里是14秒),在某个时间区间内,多次执行你传入的promql,计算出一组instant-vector,画成graph。

关于range-vector

那么range-vector有啥用?根据我的了解,它存在的意义就是为了计算输出instant-vector,没有直接使用range-vector的场景。

我们打开https://prometheus.io/docs/prometheus/latest/querying/functions/, 然后搜索range-vector,你会发现只有极少数的promql函数支持range-vector。

比如rate的输入就是range-vector,它基于某个时间之前的N分钟的数据,基于这些数据计算出平均增长速率instant-vector:

既然rate输出的是Instant-vector,那么就可以基于不同的时间点多次执行该promql,得到多个时间点的平均速率,画出graph:

对应的ajax应答如下:

再说一个容易陷入误区的理解,就是想当然的认为avg,sum等函数是对range-vector作用的,这是完全错误的!

avg/sum等聚合函数,都是作用在instant-vector上的,它们在一组不同label的instant-vector之间求它们的sum或者求avg,而不是对同一个metrics的range-vector做avg/sum。

instant-vector向量之间运算

不同的instant-vector之间可以做加减乘除运算,比如:rate(cc_total[5m]) / rate(cc_total[6m]) 这样毫无意义的计算,这只是举个栗子。

向量之间运算分为2个过程,先匹配、后计算。

匹配只指通过on/ignoring/group_left/group_right语法,令左侧instant-vector的label和右侧instant-vector的可以基于同样的label以及label value而匹配。比如,左侧vector有一个label叫做port=8080,而右侧vector没有这个label或者label=9999,那么此时可以通过ignoring(port)来忽略掉port,从而实现左右两侧label的匹配。(仅仅语法是这样工作的,实际怎么匹配需要根据需求来定,不是为了匹配而匹配)

匹配的vector之间可以进行后续加减乘除计算,就和mysql的跨表join一样,如果一个左侧vector可以匹配多个右侧vector就用group_right,这叫做one-to-many;如果一个右侧vector可以匹配多个左侧vector就用group_right,这叫做many-to-one。如果一个左侧匹配一个右侧就不用group指令,直接加减乘除运算即可,这就是one-to-one。

prometheus不支持笛卡尔乘积的计算,即不支持many-to-many:

左侧hh_bucket有4个instant-vector,右侧是同样的hh_bucket的4个instant-vector。

如果按instantce标签匹配的话,左侧的4个vector和右侧的4个vector是many-to-many的匹配,所以无法计算。

但如果我让右侧通过avg把4个vector计算成1个vector,然后再用左侧的4个vector做many-to-one到右侧的1个vector,这样就不报错了:

当然,上面的例子没有任何意义,仅仅是语法正确。

最后

关于prometheus先总结这些,后续grafana通过调用prometheus API执行promql得到数据点,可以直接画出漂亮的图表,有了prometheus认识基础就会容易很多。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~