PromQL 保姆級中文簡明教程
這篇文章介紹如何使用 PromQL 查詢 Prometheus 裏面的數據。包括如何使用函數,理解這些函數,Metrics 的邏輯等等,因爲看了很多教程試圖學習 PromQL,發現這些教程都直說有哪些函數、語法是什麼,看完之後還是很難理解。
比如 [1m]
是什麼意思?爲什麼有的函數需要有的函數不需要?它對 Grafana 上面展示的數據有什麼影響?rate
和 irate
的區別是什麼?sum
和 rate
要先用哪個後用哪個?經過照葫蘆畫瓢地寫了很多 PromQL 來設置監控和告警規則,我漸漸對 PromQL 的邏輯有了一些理解。這篇文章從頭開始,通過介紹 PromQL 裏面的邏輯,來理解這些函數的作用。
本文不會一一回答上面這些問題,但是我的這些問題都是由於之前對 PromQL 裏面的邏輯和概念不瞭解,相信讀完本文之後,這些問題的答案就顯得不言而喻了。
本文不會深入講解 Prometheus 的數據存儲原理,Prometheus 對 metrics 的抓取原理等問題;也不會深入介紹 PromQL 中每一個 API 的實現。只會着重於介紹如何寫 PromQL 的原理,和它的設計邏輯。但是相信如果理解了本文這些概念,可以更透徹地理解和閱讀 Prometheus 官方的文檔。
Metric 類型
Prometheus 裏面其實只有兩種數據類型。Gauge 和 Counter。
Gauge
Gauge 是比較符合直覺的。它就是表示一個當前的 “狀態”,比如內存當前是多少,CPU 當前的使用率是多少。
Counter
Counter 有一些不符合直覺。我想了很久才理解(可能我有點鑽牛角尖了)。Counter 是一個永遠只遞增的 Metric 類型。
使用 Counter 計算得到的,每秒收到的 packet 數量
典型的 Counter 舉例:服務器服務的請求數,服務器收到了多少包(上圖)。這個數字是只增不減的,用 Counter 最合適了。因爲每一個時間點的總請求數都會包含之前時間點的請求數,所以可以理解成它是一個 “有狀態的”(非官方說法,我這麼說只是爲了方便讀者理解)。使用 Counter 記錄每一個時間點的 “總數”,然後除以時間,就可以得到 QPS,packets/s 等數據。
爲什麼需要 Counter 呢?先來回顧一下 Gauge,你可以將 Gauge 理解爲 “無狀態的”,即類型是 Gauge 的 metric 不需要關心歷史的值,只需要記錄當前的值是多少就可以了。比如當前的內存值,當前的 CPU 使用率。當然,如果你想要查詢歷史的值,依然是可以查到的。只不過對於每一個時間點的“內存使用量” 這個 Gauge,不包含歷史的數據。那麼可否用 Gauge 來代替 Counter 呢?
Prometheus 是一個抓取的模型:服務器暴露一個 HTTP 服務,Prometheus 來訪問這個 HTTP 接口來獲取 metrics 的數據。如果使用 Gauge 來表示上面的 pk/s 數據的話,只能使用這種方案:使用這個 Metric 記錄自從上次抓取過後收到的 Packet 總數(或者直接記錄 Packet/s ,原理是一樣的)。每次 Prometheus 來抓取數據之後,就將這個值重置爲 0. 這樣的實現就類似 Gauge 了。
Prometheus 的抓取模型,去訪問服務的 HTTP 來抓取 metrics
這種實現的缺點有:
-
抓取數據本質是 GET 操作,但是這個 GET 操作卻會修改數據(將 metric 重置爲 0),所以會帶來很多隱患,比如一個服務每次只能由一個 Prometheus 來抓取,不能擴展;不能 cURL 這個
/metrics
來進行 debug,因爲會影響真實的數據,等等。 -
如果服務器發生了重啓,數據將會清零,會丟失數據(雖然 Counter 也沒有從本質上解決這個問題)。
Counter 因爲是一個只遞增的值,所以它可以判斷數字下降的問題,比如現在請求的 Count 數是 1000,然後下次 Prometheus 來抓取發現變成了 20,那麼 Prometheus 就知道,真實的數據不可能是 20,因爲請求數是不可能下降的。所以它會將這個點認爲是 1020。
然後用 Counter 也可以解決多次讀的問題,服務器上的 /metrics
,可以使用 cURL 和 grep 等工具實時查看,不會改變數據。Counter 有關的細節可以參考下 How does a Prometheus Counter work?[1]
其實 Prometheus 裏面還有兩種數據類型,一種是 Histogram,另一種是 Summary.
但是這兩種類型本質上都是 Counter。比如,如果你要統計一個服務處理請求的平均耗時,就可以用 Summary。在代碼中只用一種 Summary 類型 [2],就可以暴露出收到的總請求數,處理這些請求花費的總時間數,兩個 Counter 類型的 metric。算是一個 “語法糖”。
Histogram 是由多個 Counter 組成的一組(bucket)metrics,比如你要統計 P99 的信息,使用 Histogram 可以暴露出 10 個 bucket 分別存放不同耗時區間的請求數,使用 histogram_quantile
函數就可以方便地計算出 P99(《P99 是如何計算的?[3]》). 本質上也是一個 “糖”。假如 Prometheus 沒有 Histogram 和 Summary 這兩種 Metric 類型,也是完全可以的,只不過我們在使用上就需要多做很多事情,麻煩一些。
講了這麼說,希望讀者已經明白 Counter 和 Gauge 了。因爲我們接下來的查詢會一直跟這兩種 Metric 類型打交道。
Selectors
下面這張圖簡單地表示了 Metric 在 Prometheus 中的樣子,以給讀者一個概念。
如果我們直接在 Grafana 中使用 node_network_receive_packets_total 來畫圖的話,就會得到 5 條線。
Counter 的值很大,並且此圖基本上看不到變化趨勢。因爲它們只增加,可以認爲是這個服務器自存在以來收到的所有的包的數量。
Metric 可以通過 label 來進行選擇,比如 node_network_receive_packets_total{device=”bond0″} 就會只查詢到 bond0 的數據,繪製 bond0 這個 device 的曲線。也支持正則表達式,可以通過 node_network_receive_packets_total{device=~”en.*”} 繪製 en0 和 en2 的曲線。
其實,metric name 也是一個 “label”, 所以 node_network_receive_packets_total{device="bond0"}
本質上是 {__name__="node_network_receive_packets_total", device="bond0"}
。但是因爲 metric name 基本上是必用的 label,所以我們一般用第一種寫法, 這樣看起來更易懂。
PromQL 支持很複雜的 Selector,詳細的用法可以參考文檔 [4]。值得一提的是,Prometheus 是圖靈完備 (Turing Complete)[5] 的(Surprise!)。
實際上,如果你使用下面的查詢語句,將會僅僅得到一個數字,而不是整個 metric 的歷史數據(node_network_receive_packets_total{device=~"en.*"}
得到的是下圖中黃色的部分。
這個就是 Instant Vector:只查詢到 metric 的在某個時間點(默認是當前時間)的值。
PromQL 語言的數據類型
爲了避免讀者混淆,這裏說明一下 Metric Type 和 PromQL 查詢語言中的數據類型的區別。很簡單,在寫 PromQL 的時候,無論是 Counter 還是 Gauge,對於函數來說都是一串數字,他們數據結構上沒有區別。我們說的 Instant Vector 還是 Range Vector, 指的是 PromQL 函數的入參和返回值的類型。
Instant Vector
Instant 是立即的意思,Instant Vector 顧名思義,就是當前的值。假如查詢的時間點是 t,那麼查詢會返回距離 t 時間點最近的一個值。
常用的另一種數據類型是 Range Vector。
Range Vector
Range Vector 顧名思義,返回的是一個 range 的數據。
Range 的表示方法是 [1m]
,表示 1 分鐘的數據。也可以使用 [1h]
表示 1 小時,[1d]
表示 1 天。支持的所有的 duration 表示方法可以參考文檔 [6]。
假如我們對 Prometheus 的採集配置是每 10s 採集一次,那麼 1 分鐘內就會有采集 6 次,就會有 6 個數據點。我們使用 node_network_receive_packets_total{device=~“.*”}[1m] 查詢的話,就可以得到以下的數據:兩個 metric,最後的 6 個數據點。
Prometheus 大部分的函數要麼接受的是 Instant Vector,要麼接受的是 Range Vector。所以要看懂這些函數的文檔,就要理解這兩種類型。
在詳細解釋之前,請讀者思考一個問題:在 Grafana 中畫出來一個 Metric 的圖標,需要查詢結果是一個 Instant Vector,還是 Range Vector 呢?
答案是 Instant Vector (Surprise!)。
爲什麼呢?要畫出一段時間的 Chart,不應該需要一個 Range 的數據嗎?爲什麼是 Instant Vector?
答案是:Range Vector 基本上只是爲了給函數用的,Grafana 繪圖只能接受 Instant Vector。Prometheus 的查詢 API 是以 HTTP 的形式提供的,Grafana 在渲染一個圖標的時候會向 Prometheus 去查詢數據。而這個查詢 API 主要有兩種:
第一種是 /query
:查詢一個時間點的數據,返回一個數據值,通過 ?time=1627111334
可以查詢指定時間的數據。
假如要繪製 1 個小時內的 Chart 的話,Grafana 首先需要你在創建 Chart 的時候傳入一個 step
值,表示多久查一個數據,這裏假設 step=1min
的話,我們對每分鐘需要查詢一次數據。那麼 Grafana 會向 Prometheus 發送 60 次請求,查詢 60 個數據點,即 60 個 Instant Vector,然後繪製出來一張圖表。
Grafana 的 step 設置
當然,60 次請求太多了。所以就有了第二種 API query_range
,接收的參數有 ?start=<start timestamp>&end=<end timestamp>&step=60
。但是這個 API 本質上,是一個語法糖,在 Prometheus 內部還是對 60 個點進行了分別計算,然後返回。當然了,會有一些優化。
然後就有了下一個問題:爲什麼 Grafana 偏偏要繪製 Instant Vector,而不是 Range Vector 呢?
Grafana 只接受 Instant Vector, 如果查詢的結果是 Range Vector, 會報錯
因爲這裏的 Range Vector 並不是一個 “繪製的時間”,而是函數計算所需要的時間區間。看下面的例子就容易理解了。
來解釋一下這個查詢:
rate(node_network_receive_packets_total{device=~”en.*”}[1m])
查詢每秒收到的 packet 數量
node_network_receive_packets_total 是一個 Counter,爲了計算每秒的 packet 數量,我們要計算每秒的數量,就要用到 rate 函數。
先來看一個時間點的計算,假如我們計算 t 時間點的每秒 packet 數量,rate 函數可以幫我們用這段時間([1m]
)的總 packet 數量,除以時間 [1m]
,就得到了一個 “平均值”,以此作爲曲線來繪製。
以這種方法就得到了一個點的數據。
然後我們對之前的每一個點,都以此法進行計算,就得到了一個 pk/s
的曲線(最長的那條是原始的數據,黃色的表示 rate
對於每一個點的計算過程,藍色的框爲最終的繪製的點)。
所以這個 PromQL 查詢最終得到的數據點是:… 2.2, 1.96, 2.31, 2, 1.71 (即藍色的點)。
這裏有兩個選中的 metric,分別是 en0
和 en2
,所以 rate
會分別計算兩條曲線,就得到了上面的 Chart,有兩條線。
rate, irate 和 increase
很多人都會糾結 irate
和 rate
有什麼區別。看到這裏,其實就很好解釋了。
以下來自官方的文檔:
irate() irate(v range-vector) calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points.
即,irate
是計算的最後兩個點之間的差值。可以用下圖來表示:
irate 的計算方式
自然,因爲只用最後兩個點的差值來計算,會比 rate
平均值的方法得到的結果,變化更加劇烈,更能反映當時的情況。那既然是使用最後兩個點計算,這裏又爲什麼需要 [1m]
呢?這個 [1m]
不是用來計算的,是用來限制找 t-2 個點的時間的,比如,如果中間丟了很多數據,那麼顯然這個點的計算會很不準確,irate
在計算的時候會最多向前在 [1m]
找點,如果超過 [1m]
沒有找到數據點,這個點的計算就放棄了。
在現實中的例子,可以將上面查詢的 rate
改成 irate
。
irate(node_network_receive_packets_total{device=~”en.*”}[1m])
對比與之前的圖,可以看到變化更加劇烈了。
那麼,是不是我們總是使用 irate
比較好呢?也不是,比如 requests/s 這種,如果變化太劇烈,從面板上你只能看到一條劇烈抖動導致看不清數值的曲線,而具體值我們是不太關心的,我們可能更關心一天中的 QPS 變化情況;但是像是 CPU,network 這種資源的變化,使用 irate
更加有意義一些。
還有一個函數叫做 increase
,它的計算方式是 end - start
,沒有除。計算的是每分鐘的增量。比較好理解,這裏就不畫圖了。
這三個函數接受的都是 Range Vector,返回的是 Instant Vector,比較常用。
另外需要注意的是,increase
和 rate
的 range 內必須要有至少 4 個數據點。詳細的解釋可以見這裏:What range should I use with rate()?[7]
介紹了這兩種類型,那麼其他的 Prometheus 函數 [8] 應該都可以看文檔理解了。Prometheus 的文檔中會將函數這樣標註:
changes() For each input time series, changes(v range-vector) returns the number of times its value has changed within the provided time range as an instant vector.
我們就知道,changes()
這個函數接受的是一個 range-vector, 所以要帶上類似於 [1m]
。不能傳入不帶類似 [1m]
的 metrics,類似於這樣的使用是不合法的:change(requests_count{server="server_a"}
,這樣就相當於傳入了一個 Instant Vector。
看到這裏,你應該已經成爲一隻在 Prometheus 裏面自由翱翔的鳥兒了。接下來可以抱着文檔 [9] 去寫查詢了,但是在這之前,讓我再介紹一點非常重要的誤區。
使用函數的順序問題
在計算 P99 的時候,我們會使用下面的查詢:
histogram_quantile(0.9,
sum by (le)
(rate(http_request_duration_seconds_bucket[10m]))
)
首先,Histogram 是一個 Counter,所以我們要使用 rate
先處理,然後根據 le
將 labels 使用 sum
合起來,最後使用 histogram_quantile
來計算。這三個函數的順序是不能調換的,必須是先 rate
再 sum
,最後 histogram_quantile
。
爲什麼呢?這個問題可以分成兩步來看:
rate
必須在 sum
之前。前面提到過 Prometheus 支持在 Counter 的數據有下降之後自動處理的,比如服務器重啓了,metric 重新從 0 開始。這個其實不是在存儲的時候做的,比如應用暴露的 metric 就是從 2033 變成 0 了,那麼 Prometheus 就會忠實地存儲 0. 但是在計算 rate
的時候,就會識別出來這個下降。但是 sum
不會,所以如果先 sum
再 rate
,曲線就會出現非常大的波動。詳細見這裏 [10]。
histogram_quantile
必須在最後。在《P99 是如何計算的?[11]》這篇文章中介紹了 P99 的原理。也就是說 histogram_quantile
計算的結果是近似值,去聚合(無論是 sum
還是 max
還是 avg
)這個值都是沒有意義的。
引用鏈接
[1]
How does a Prometheus Counter work?: https://www.robustperception.io/how-does-a-prometheus-counter-work
[2]
在代碼中只用一種 Summary 類型: https://github.com/prometheus/client_python#summary
[3]
P99 是如何計算的?: https://www.kawabangga.com/posts/4284
[4]
參考文檔: https://prometheus.io/docs/prometheus/latest/querying/basics/
[5]
圖靈完備 (Turing Complete): https://www.robustperception.io/conways-life-in-prometheus
[6]
參考文檔: https://prometheus.io/docs/prometheus/latest/querying/basics/#time-durations
[7]
What range should I use with rate()?: https://www.robustperception.io/what-range-should-i-use-with-rate
[8]
Prometheus 函數: https://prometheus.io/docs/prometheus/latest/querying/functions/
[9]
文檔: https://prometheus.io/docs/introduction/overview/
[10]
這裏: https://www.robustperception.io/rate-then-sum-never-sum-then-rate
[11]
P99 是如何計算的?: https://www.kawabangga.com/posts/4284
原文鏈接:https://www.kawabangga.com/posts/4408
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/DFCTY0KPYlxlZMGOHcUSvw