0%

[Prometheus] 詳解 histogram_quantile(q, sum(rate()) by (le)) 原理

前言

Prometheus 是監控與告警系統的工具集,將數據以時間序列的格式存在 TSDB (Time Series Database) 中,並提供 PromQL 來對資料做查詢。histogram_quantile 為其中一個聚合函式,讓我們能夠透過 histogram 類型的 metric 推算出指定的百分位數所對應的值,應用更靈活。在解讀數據之前,個人認為要先了解數據是如何得到的,因此這篇文章紀錄 histogram_quantile 的演算法原理。

histogram_quantile 原理

Prometheus 是開源專案,所有功能背後的底層實現都能在 Prometheus GitHub 找到,我們所在乎的 histogram_quantile 也不例外。原始碼是最直接理解運作原理的方式,因此直接為相關原始碼作註解,使讀者容易理解運算邏輯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
type bucket struct {
upperBound float64 // bucket metric 的 le 標籤
count float64 // bucket metric 的值
}

type buckets []bucket

// https://pkg.go.dev/sort#Interface
// 為了使用 sort.Sort 排序,必須為資料型態 buckets
// 定義 Len、Swap、Less 三個函式以滿足 Interface
func (b buckets) Len() int { return len(b) }
func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }

func bucketQuantile(q float64, buckets buckets) float64 {
// 三個 if 判斷 q 的合理性
// 若 q 為 NaN,回傳 NaN
if math.IsNaN(q) {
return math.NaN()
}
// 若 q 小於 0,回傳負無限大
if q < 0 {
return math.Inf(-1)
}
// 若 q 大於 1,回傳正無限大
if q > 1 {
return math.Inf(+1)
}
// 根據 bucket.upperBound 排序
// 小的在前,大的在後
sort.Sort(buckets)
// 判斷最後一個 bucket 的 upperBound (le) 是否為正無限大
// 若否,則結束運算並回傳 NaN
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
return math.NaN()
}

// 將 upperBound 一樣的 bucket 合併
buckets = coalesceBuckets(buckets)
// 確保 buckets 列表中的 count 為遞增
ensureMonotonic(buckets)

// buckets 長度小於 2 的情況為
// 1. buckets 為空
// 2. buckets[0].upperBound 為正無限大
// 滿足則回傳 NaN
if len(buckets) < 2 {
return math.NaN()
}
// 最後一個的 count 必定包含所有觀察數
observations := buckets[len(buckets)-1].count
// 若該組 buckets 沒有觀察數,則回傳 NaN
if observations == 0 {
return math.NaN()
}
// 百分位 q 在這組 buckets 中所對應的位置為 rank
rank := q * observations
// 尋找該 rank 所在的 bucket
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })

// 若 rank 所在為 upperBound 為無限大的 bucket
// 回傳倒數第二個 bucket 的 upperBound
if b == len(buckets)-1 {
return buckets[len(buckets)-2].upperBound
}
// 若 rank 所在為最低的 bucket 且該 upperBound 小於等於 0
// 則直接回傳該 upperBound
if b == 0 && buckets[0].upperBound <= 0 {
return buckets[0].upperBound
}
var (
bucketStart float64
bucketEnd = buckets[b].upperBound
count = buckets[b].count
)
if b > 0 {
bucketStart = buckets[b-1].upperBound
count -= buckets[b-1].count
rank -= buckets[b-1].count
}
// prometheus 假設一個前提,也就是在 bucket 中的
// 分佈情況是線性的,因此可以使用線性內差的方式計算出
// rank 對應的數值
return bucketStart + (bucketEnd-bucketStart)*(rank/count)
}

範例

histogram_quantile 通常通常其會搭配 sum 與 rate 來使用,因此常會看到 histogram_quantile(q, sum(rate()) by (le)) 這樣的 PromQL 出現。到目前為止,已經明白 histogram_quantile 背後的運作原理,再來就是以範例數據來強化記憶。

情境說明

情境與目標:監控的 Server 有兩個端點,分別為 /user//system/,我們要想要知道每 1 分鐘內 90 百分位的 http 處理時間 (不限定端點)。

Metric 定義

用來紀錄 http request 所需處理時間的 metric 的定義如下,其為 histogram 型態

1
2
3
4
5
prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets, // []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}
}, []string{"handler"})

結論

使用 PromQL 如下,即可得到想要的結果

1
histogram_quantile(0.90, sum(rate(http_request_seconds_bucket[1m])) by (le))

以實例說明運作原理

接著,以實例作為輔助來解析這段 PromQL 的含義

數據說明

假設在時間點 t - 1m 時,metrics 的數值如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
http_request_seconds_bucket{handler="/user/",le="0.005"} 5
http_request_seconds_bucket{handler="/user/",le="0.01"} 111
http_request_seconds_bucket{handler="/user/",le="0.025"} 491
http_request_seconds_bucket{handler="/user/",le="0.05"} 879
http_request_seconds_bucket{handler="/user/",le="0.1"} 1244
http_request_seconds_bucket{handler="/user/",le="0.25"} 1708
http_request_seconds_bucket{handler="/user/",le="0.5"} 1886
http_request_seconds_bucket{handler="/user/",le="1"} 1960
http_request_seconds_bucket{handler="/user/",le="2.5"} 2000
http_request_seconds_bucket{handler="/user/",le="5"} 2000
http_request_seconds_bucket{handler="/user/",le="10"} 2000
http_request_seconds_bucket{handler="/user/",le="+Inf"} 2000

http_request_seconds_bucket{handler="/system/",le="0.005"} 2
http_request_seconds_bucket{handler="/system/",le="0.01"} 38
http_request_seconds_bucket{handler="/system/",le="0.025"} 326
http_request_seconds_bucket{handler="/system/",le="0.05"} 692
http_request_seconds_bucket{handler="/system/",le="0.1"} 1083
http_request_seconds_bucket{handler="/system/",le="0.25"} 1598
http_request_seconds_bucket{handler="/system/",le="0.5"} 1842
http_request_seconds_bucket{handler="/system/",le="1"} 1940
http_request_seconds_bucket{handler="/system/",le="2.5"} 1994
http_request_seconds_bucket{handler="/system/",le="5"} 2000
http_request_seconds_bucket{handler="/system/",le="10"} 2000
http_request_seconds_bucket{handler="/system/",le="+Inf"} 2000

再假設在時間點 t 時,metrics 的數值如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
http_request_seconds_bucket{handler="/user/",le="0.005"} 6
http_request_seconds_bucket{handler="/user/",le="0.01"} 159
http_request_seconds_bucket{handler="/user/",le="0.025"} 835
http_request_seconds_bucket{handler="/user/",le="0.05"} 1592
http_request_seconds_bucket{handler="/user/",le="0.1"} 2391
http_request_seconds_bucket{handler="/user/",le="0.25"} 3315
http_request_seconds_bucket{handler="/user/",le="0.5"} 3676
http_request_seconds_bucket{handler="/user/",le="1"} 3844
http_request_seconds_bucket{handler="/user/",le="2.5"} 4000
http_request_seconds_bucket{handler="/user/",le="5"} 4000
http_request_seconds_bucket{handler="/user/",le="10"} 4000
http_request_seconds_bucket{handler="/user/",le="+Inf"} 4000

http_request_seconds_bucket{handler="/system/",le="0.005"} 7
http_request_seconds_bucket{handler="/system/",le="0.01"} 144
http_request_seconds_bucket{handler="/system/",le="0.025"} 844
http_request_seconds_bucket{handler="/system/",le="0.05"} 1628
http_request_seconds_bucket{handler="/system/",le="0.1"} 2400
http_request_seconds_bucket{handler="/system/",le="0.25"} 3318
http_request_seconds_bucket{handler="/system/",le="0.5"} 3720
http_request_seconds_bucket{handler="/system/",le="1"} 3901
http_request_seconds_bucket{handler="/system/",le="2.5"} 3981
http_request_seconds_bucket{handler="/system/",le="5"} 4000
http_request_seconds_bucket{handler="/system/",le="10"} 4000
http_request_seconds_bucket{handler="/system/",le="+Inf"} 4000

計算步驟頗析

首先計算 rate(http_request_seconds_bucket[1m]),即 t 時刻的 metric 的值減去 t - 1m 的 metric 的值

rate(http_request_seconds_bucket{handler="/user/"}[1m]) rate(http_request_seconds_bucket{handler="/system/"}[1m])
le=0.005 1 5
le=0.01 48 106
le=0.025 344 518
le=0.05 713 936
le=0.1 1147 1317
le=0.25 1607 1720
le=0.5 1790 1878
le=1 1884 1961
le=2.5 2000 1987
le=5 2000 2000
le=10 2000 2000
le=+Inf 2000 2000

sum(rate(http_request_seconds_bucket[1m])) by (le) 則是將兩個 rate 的結果,根據 le 把結果相加起來

sum(rate(http_request_seconds_bucket[1m])) by (le)
le=0.005 6
le=0.01 154
le=0.025 862
le=0.05 1649
le=0.1 2464
le=0.25 3327
le=0.5 3668
le=1 3845
le=2.5 3987
le=5 4000
le=10 4000
le=+Inf 4000

最後,在 golang 內部會將這些 buckets 轉為 go 的 buckets 物件並呼叫 bucketQuantile 函數計算出 90 百分位對應的值,可以將最後的 histogram_quantile 運算等效成以下 golang 程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// histogram_quantile(0.90, sum(rate(http_request_seconds_bucket[1m])) by (le))
bs := buckets{
{
upperBound: 0.005,
count: 6,
},
{
upperBound: 0.01,
count: 154,
},
{
upperBound: 0.025,
count: 862,
},
{
upperBound: 0.05,
count: 1649,
},
{
upperBound: 0.1,
count: 2464,
},
{
upperBound: 0.25,
count: 3327,
},
{
upperBound: 0.5,
count: 3668,
},
{
upperBound: 1,
count: 3845,
},
{
upperBound: 2.5,
count: 3987,
},
{
upperBound: 5,
count: 4000,
},
{
upperBound: 10,
count: 4000,
},
{
upperBound: math.Inf(+1),
count: 4000,
},
}
fmt.Printf("%f", bucketQuantile(0.90, bs))

舉一反三

這樣的 PromQL 是根據整體的數據來查看 90 百分位數的值

1
histogram_quantile(0.90, sum(rate(http_request_seconds_bucket[1m])) by (le))

但若我們想要查看指定端點的話呢?非常簡單,假設我們只想查看 /user/ 90 百分位數的值,只需加上篩選條件 {handler="/user/"} 即可

1
histogram_quantile(0.90, sum(rate(http_request_seconds_bucket{handler="/user/"}[1m])) by (le))

參考文獻

  1. Understanding histogram_quantile based on rate in Prometheus

  2. Prometheus Source Code

很高興能在這裡幫助到您,歡迎登入 Liker 為我鼓掌 5 次,或者成為我的讚賞公民,鼓勵我繼續創造優質文章。
以最優質的內容回應您的鼓勵