原文来源: https://tidb.net/blog/c38dd8ac
一、背景简介
在学习prometheus时,会遇到一个histogram_quantile()函数,用于对histogram类型的指标进行分位数计算,实际上这个函数就是histogram这个指标类型最常用的函数。
此函数在tidb的监控图表中有一个比较明显地方使用:计算P99/P999 Duration等延迟指标。
新人们对此函数的理解是可能是一个较漫长的过程,而我观察到很多解释此函数的博主们在解释这个函数的应用场景和原理时非常容易陷入 “知识诅咒” 的陷阱,即,写作者自身非常了解这个领域的知识,但潜意识中很容易忽略读者可能对该主题或领域不了解的事实。于是在涉及某些较为简单的专有名词时会一笔带过,在涉及某些较为核心的逻辑时容易高屋建瓴的做出解释但读者却一头雾水。
不同的人认知水平不同,而即便是同一个人的认知水平也随时间增长,因此所有人都不可避免的陷入知识诅咒的陷阱,为此本文因此尝试从另一种通俗的角度来理解,希望对读者有所帮助,同时也可以极大加深自己的理解。
二、基础知识
在介绍这个函数之前,简单的介绍一下prometheus的几种指标类型: 官网: Prometheus指标类型
- Counter:理解为一个单调递增的数字即可,适合进行请求数、错误数等累计监控,例如t1时间request_count为0,t1+10s时间计数为100,则容易得到过去10s内的请求速度约为10个/s
- Gauge:理解为一个非单调递增的数字即可,适合进行瞬时值监控,例如当前温度、内存使用量等,由于上报频率不可能设置的很高,因此相比Counter可能会丢失一些信息,但应用场景也很广
- Histogram:直方图,可以搜一下网上的直方图图片,即prometheus将位于同一范围内的数字归类到一个柱状体(bucket)内进行计数,形成一个向量(线性代数名词,这里理解为数组)除此之外还会记录实际的指标总数和总值,即一个Histogram包含一个直方图向量和2个类似Counter的指标(Prometheus server并不区分指标类型,对server来说只有time series)
- Summary:略,Histogram的进阶版本,不过一些编程语言的驱动可能并没有很好的支持这种类型,使用Histogram可以覆盖他的使用场景。
上述简短的解释不能真正展示4种数据类型的实质,但他们不是本篇重点因此不多说了。
三、从P99计算开始
首先列出tidb计算P99的查询语句,可以从tidb grafana中获取到:
histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket{k8s_cluster="$k8s_cluster", tidb_cluster="$tidb_cluster"}[1m])) by (le))
因为同一个集群下的这俩标签都是一样的,我们将其简化为:
histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le))
使用postman可以快速查出他的结果:
GET /api/v1/query?query=histogram_quantile(0.99, sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le)) HTTP/1.1
Host: x.x.x.x:9090
{ "status": "success", "data": { "resultType": "vector", "result": [ { "metric": {}, "value": [ 1691657038.265, "0.051201495327102595" ] } ] }
}
可以知道当前集群的P99指标约为51ms。
但是这个计算过程看起来较为繁琐,先是使用rate对tidb_server_handle_query_duration_seconds_bucket计算了过去1min的速率,然后使用sum by (le)进行了求和,最后才使用histogram_quantile函数计算99分位数。
该如何理解这种计算方式?
四、流程解析
首先 思考一个问题,当我们想要获取一个P99值时,一个最直观的存储监控值的办法是什么?
当然是tidb每处理一个请求都把耗时以Gauge形式存起来,然后等prometheus来收集!
然后我们发现这种办法是在是太差了,qps高的时候监控代理和prometheus都容易爆炸。
再来分析一下实际需求,我们实质上并不是要看所有请求的耗时,只是想了解一下某个时间点(或某一段时间内)数据库中99%的请求都低于多少ms,即P99。
然后发现我们似乎不用存储具体的耗时值,我们只需要先创建一个直方图(一个包含多个bucket的容器),例如:
{"耗时低于1s": 0, // bucket 1, 标签 {le: 1}"耗时低于10s": 0, // bucket 2, 标签 {le: 10}"耗时低于100s": 0, // bucket 3, 标签 {le: 100}... // bucket n, 标签 {le: ......}
}
然后每当有请求耗时需要存储时,在对应的bucket中计数+1,这样只要bucket的设置合理一些我们就可以轻易获得一个执行耗时的分布图。在此基础上计算P99就比较容易了。
当然,除了上述的向量之外,histogram还会存储一个总耗时值和总请求次数的计数器,不过计算P99用不到。
直方图指标有了,我们把他叫做 tidb_server_handle_query_duration_seconds_bucket
,prometheus每个收集间隔过来取一次即可。
接下来呢?
可以看到每个bucket其实也都是一个计数器,计数器的瞬时值一般没有多少意义,因此我们需要使用rate()函数进行速率计算: rate(tidb_server_handle_query_duration_seconds_bucket[1m]
tidb_server_handle_query_duration_seconds_bucket如之前所说,他其实是一个向量值,向量值和标量值(60s)进行除法这个了解一些线代基础的可以知道结果是一个每个元素都除以60s的新向量。这个新向量的label和以前一样,都是{le: 1}, {le: 10}这种。
而tidb的请求类型很多,有select,update,insert还有analyze table,begin,commit等,我们希望统一进行计算,因此执行一个sum() group by le的操作,即: sum(rate(tidb_server_handle_query_duration_seconds_bucket[1m])) by (le)
同样的其结果依然是一个向量,我们只是按le进行了一次sql_type的聚合,至此我们终于可以使用 histogram_quantile(φ scalar, b instant-vector) 函数啦。这个函数接收2个参数,一个分位数值(例如0.99,0.999等),一个瞬时向量(这里就是rate处理之后的_bucket指标)。
至此 我们就得到了tidb的P99。至于histogram_quantile内部如何计算99分位数这里不再解释,因为为了更贴近真实结果有一些小的tricks解释起来需要多些几句,这不是本文重点。