前言

下面我总结了一些对比点

对比点 histogram summary
查询表达式对比 histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) http_request_duration_seconds_summary{quantile="0.95"}
所需配置 选择合适的buckets 选择所需的φ分位数和滑动窗口。其他φ分位数和滑动窗口以后无法计算。
客户端性能开销 开销低,因为它们只需要增加计数器 开销高,由于流式分位数计算
服务端性能开销 开销高,因为需要在服务端实时计算(而且bucket值指标基数高) 开销低,可以看做是gauge指标上传,仅查询即可
分位值误差 随bucket精度变大而变大(线性插值法计算问题) 误差在φ维度上受可配置值限制
是否支持聚合 支持 不支持(配置sum avg等意义不大)
是否提供全局分位值 支持(根据promql匹配维度决定) 不支持(因为数据在每个实例/pod/agent侧已经算好,无法聚合)

histogram 线性插值法

histogram_quantile为何需要先算rate

  • 因为每个bucket都是counter型的,如果不算rate那么分位值的结果曲线是一条直线
  • 原理是因为counter型累加,不算rate并不知道当前bucket的增长情况,换句话说不知道这些bucket是多久积攒到现在这个值的

什么是线性插值法

  1. - 之前阅读很多文章都提到`histogram`采用`线性插值法`计算分位值会导致一定的误差
  2. - 对这个`线性插值法`总是理解的不到位
  3. - 在查看完代码之后明白了

代码分析

  1. - 代码位置:`D:\work\go_work\pkg\mod\github.com\prometheus\prometheus@v0.0.0-20201209205804-66f47e116e00\promql\quantile.go`
  2. ###

bucket数据结构

  1. - 其中`bucket` 代表事先定义好的bucket
  2. - `upperBound`代表这个bucket的上限值
  3. - `count` 代表这个小于等于这个`upperBound`的个数/次数
  4. - `workqueue_work_duration_seconds_bucket{name="crd_openapi_controller",le="10"} 65246 `
  5. - 所以上述表达式含义为 `workqueue_work_duration_seconds`小于`10`秒的有`65246 `
  1. type bucket struct {
  2. upperBound float64
  3. count float64
  4. }
  5. type buckets []bucket
  6. func (b buckets) Len() int { return len(b) }
  7. func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
  8. func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }

核心计算函数

  1. func bucketQuantile(q float64, buckets buckets) float64 {
  2. if q < 0 {
  3. return math.Inf(-1)
  4. }
  5. if q > 1 {
  6. return math.Inf(+1)
  7. }
  8. sort.Sort(buckets)
  9. if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
  10. return math.NaN()
  11. }
  12. buckets = coalesceBuckets(buckets)
  13. ensureMonotonic(buckets)
  14. if len(buckets) < 2 {
  15. return math.NaN()
  16. }
  17. observations := buckets[len(buckets)-1].count
  18. if observations == 0 {
  19. return math.NaN()
  20. }
  21. rank := q * observations
  22. b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
  23. if b == len(buckets)-1 {
  24. return buckets[len(buckets)-2].upperBound
  25. }
  26. if b == 0 && buckets[0].upperBound <= 0 {
  27. return buckets[0].upperBound
  28. }
  29. var (
  30. bucketStart float64
  31. bucketEnd = buckets[b].upperBound
  32. count = buckets[b].count
  33. )
  34. if b > 0 {
  35. bucketStart = buckets[b-1].upperBound
  36. count -= buckets[b-1].count
  37. rank -= buckets[b-1].count
  38. }
  39. sql:=fmt.Sprintf("%v+(%v-%v)*(%v/%v)",
  40. bucketStart,
  41. bucketEnd,
  42. bucketStart,
  43. rank,
  44. count,
  45. )
  46. log.Println(sql)
  47. return bucketStart + (bucketEnd-bucketStart)*(rank/count)
  48. }

我们现在有这些数据,然后求75分位值

  1. a := []bucket{
  2. {upperBound: 0.05, count: 199881},
  3. {upperBound: 0.1, count: 212210},
  4. {upperBound: 0.2, count: 215395},
  5. {upperBound: 0.4, count: 319435},
  6. {upperBound: 0.8, count: 419576},
  7. {upperBound: 1.6, count: 469593},
  8. {upperBound: math.Inf(1), count: 519593},
  9. }
  10. q75 := bucketQuantile(0.75, a)
  1. - 其计算逻辑为:根据记录总数和分位值求目标落在第几个bucket`b`
  2. - 根据`b`得到起始bucket大小`bucketStart`,终止bucket大小`bucketStart` ,本bucket宽度 ,本bucket记录数
  3. - 根据本段记录数和分位值算出目标分位数在本bucket排行`rank`
  4. - 最终的计算方式为`分位值=起始bucket大小+(本bucket宽度)*(目标分位数在本bucket排行/本bucket记录数)`
  5. - 换成本例中: `q75=0.4+(0.8-0.4)*(70259.75/100141) = 0.6806432929569308`
  6. 2021/02/02 19:08:55 记录总数 = 519593
  7. 2021/02/02 19:08:55 目标落在第几个bucket段= 4
  8. 2021/02/02 19:08:55 起始bucket大小= 0.4
  9. 2021/02/02 19:08:55 终止bucket大小= 0.8
  10. 2021/02/02 19:08:55 bucket宽度= 0.4
  11. 2021/02/02 19:08:55 bucket记录数= 100141
  12. 2021/02/02 19:08:55 目标分位数在本bucket排行= 70259.75
  13. 2021/02/02 19:08:55 分位值=起始bucket大小+(本bucket宽度)*(目标分位数在本bucket排行/本bucket记录数)
  14. 2021/02/02 19:08:55 0.4+(0.8-0.4)*(70259.75/100141) = 0.6806432929569308

那线性插值法的含义体现在哪里呢

  1. - 就是这里 `本bucket宽度*(目标分位数在本bucket排行/本bucket记录数)`
  2. - 有个假定:样本数据这个目标bucket中按照平均间隔均匀分布
  3. - 举例 100141个样本在0.4-0.8 bucket中均匀分布
  4. - 如果真实值分布靠近0.4一些,则计算出的值偏大
  5. - 如果真实值分布靠近0.8一些,则计算出的值偏小
  6. - 这就是线性插值法的含义

histogram 高基数问题

具体可以看我之前写的文章prometheus高基数问题和其解决方案

危害在哪里

  1. - 一个高基数的查询会把存储打挂
  2. - 一个50w基数查询1小时数据内存大概的消耗为1G,再叠加cpu等消耗
  3. ###

为何会出现

  • label乘积太多 ,比如bucket有50种,再叠加4个10种的业务标签,所以总基数为 5010101010=50w

summary 流式聚合

一个qps为1万的http服务接口的分位值如何计算

  • 假设以1秒为窗口拿到1万个请求的响应时间,排序算分位值即可
  • 但是1秒窗口期内,快的请求会多,慢的请求会少
  • 原理分析:说来惭愧,没看太明白,但是可以确定就是hold一段时间的点计算的
  • 使用库”github.com/beorn7/perks/quantile”
  • 感兴趣的可以自己研究下D:\work\go_work\pkg\mod\github.com\prometheus\client_golang@v1.9.0\prometheus\summary.go