當前位置:
首頁 > 最新 > Prometheus原理和源碼分析

Prometheus原理和源碼分析

作者|楊諭黔

編輯|Cherry

Prometheus(下稱Prom)是一個基於 Metrics 的監控系統,與Kubernetes 同屬CNCF(Cloud Native Computing Foundation),它已經成為炙手可熱的Kubernetes生態圈中的核心監控系統,越來越多的項目(如Kubernetes和 etcd等)都加入了豐富的Prom原生支持,從側面體現了社區對它的認可。

Prom 提供了通用的數據模型和便捷的數據採集、存儲和查詢介面,同時基於 Go 實現也大大降低了服務端的運維成本,可以藉助一些優秀的圖形化工具(如 Grafana)可以實現友好的圖形化和報警。

實際使用中筆者發現工程人員中普遍存在對 Prom 中客戶端數據模型和 PromQL 計算邏輯的誤解,不但很難將數據中的價值發揮出來,還可能出現誤判。本文分析了客戶端和服務端的部分源碼實現,介紹了客戶端數據模型和 PromQL 計算邏輯,希望能為基於 Prom 的監控平台提供一些啟發。

Go 客戶端

Go 客戶端實現了 Prom 數據協議,定義了時序數據模型和採集監控數據的介面。

整體結構分析

無論是 Prom 拉取 (pull) 數據,還是客戶端主動推送 (push) 數據,都可以從 Collector 獲取 Metric 的定義,圖 1.1.1 中 UML 圖描述了 Go 客戶端中主要結構和介面之間的關係。

圖 1.1.1 Go 客戶端 UML 圖

先看 Collector 介面的定義,如圖 1.1.2 所示。

圖 1.1.2 Collector

Collector 中 Describe 和 Collect 方法都是無狀態的函數,其中 Describe 暴露全部可能的 Metric 描述列表,在註冊(Register)或註銷(Unregister)Collector 時會調用 Describe 來獲取完整的 Metric 列表,用以檢測 Metric 定義的衝突,另外在 github.com/prometheus/client_golang/prometheus/promhttp 下的 Instrument Handler 中,也會通過 Describe 獲取 Metric 列表,並檢查 label 列表(InstrumentHandler 中只支持 code 和 method 兩種自定義 label);而通過 Collect 可以獲取採樣數據,然後通過 HTTP 介面暴露給 Prom Server。另外,一些臨時性的進程,如批處理任務,可以把數據 push 到 Push Gateway,由 Push Gateway 暴露 pull 介面,此處不贅述。

客戶端對數據的收集大多是針對標準數據結構來進行的:

Counter:收集事件次數等單調遞增的數據

Gauge:收集當前的狀態,比如資料庫連接數

Histogram:收集隨機正態分布數據,比如響應延遲

Summary:收集隨機正態分布數據,和 Histogram 是類似的

每種標準數據結構還對應了 Vec 結構,通過 Vec 可以簡潔的定義一組相同性質的 Metric,在採集數據的時候傳入一組自定義的 Label/Value 獲取具體的 Metric(Counter/Gauge/Histogram/Summary),最終都會落實到基本的數據結構上,這裡不再贅述。

Counter 和 Gauge

Gauge 和 Counter 基本實現上看是一個進程內共享的浮點數,基於 value 結構實現,而 Counter 和 Gauge 僅僅封裝了對這個共享浮點數的各種操作和合法性檢查邏輯。

先看 Counter 中 Inc 函數的實現,圖 1.2.1 為 value 結構中 Inc 函數的實現。

圖 1.2.1 value.Inc

value.Add 中修改共享數據時採用了「無鎖」實現,相比「有鎖 (Mutex)」實現可以更充分利用多核處理器的並行計算能力,性能相比加 Mutex 的實現會有很大提升。圖 1.2.2 中是 Go Benchmark 的測試結果,對比了「有鎖」(用 defer 或不用 defer 來釋放鎖)和「無鎖」實現在多核場景下對性能的影響。

圖 1.2.2 Go Benchmark 測試結果

注意圖 1.2.2 中針對「有鎖」的實現,進行了兩組實驗,其中一組用 defer 來釋放鎖,可見在多核場景下「無鎖」實現的性能最好也最穩定。

Counter 和 Gauge 中的其他操作都很簡單,不贅述。

HistogramHistogram

HistogramHistogram 實現了 Observer 介面,用來獲取客戶端狀態初始化(重啟)到某個時間點的採樣點分布,監控數據常需要服從正態分布。

圖 1.3.1 Oberver 介面定義

先看通過 Histogram 採集一個 float64 數據的 Observe 方法實現(圖 1.3.2)。

圖 1.3.2 histogram.Observe

此處每個 bucket 對應的 count 是不互相包含的,bucket 的計數器之和應該等於全局計數器,即 h.count == sum(h.counts) 是成立的。然而為了便於服務端存儲和計算,最終服務端收集到的數據是向下包含的,這是在 histogram.Write(圖 1.3.3)中實現的。

圖 1.3.3 histogram.Write 實現

圖 1.3.4 中用表格形式給出了 Histogram 採集和整理數據的過程。

圖 1.3.4 Histogram 採集整理數據過程實例

Histogram 在客戶端也是無鎖的,因為每個採樣點只更新一個具體 bucket 內的 Counter(float64),因此客戶端性能開銷相比 Counter 和 Gauge 而言沒有明顯改變,適合高並發的數據收集。

圖 1.3.5 為 Go 客戶端的 Histogram 默認 bucket 設置,可以用來採集 Web 服務響應時間,實際應用中通常需要為監控對象選擇合理的 buckets,buckets 應設置為正態分布中常用的分位點。

圖 1.3.5 histogram 默認 buckets 設置

Summary

Summary 是標準數據結構中最複雜的一個,用來收集服從正態分布的採樣數據。在 Go 客戶端 Summary 結構和 Histogram 一樣,都實現了 Observer 介面(圖 1.3.1)。

Summary 中 quantile 實際上是正態分布中的分位點 ,如圖 1.4.1 所示,圖中的實心圓點分別代表 [0.025 0.25 0.50 0.75 0.975] 分位點,圖 2.1.10 中 0.5 分位點的採樣數據為 0,而 0.975 分位點的採樣值為 2,這說明採樣數據的絕大部分的峰值都在 2 附近。

圖 1.4.1 隨機正態分布數據的Quantile逼近模擬

由於 Summary 結構的客戶端實現相比其他幾個結構而言複雜一些,先看一下 summary 結構的定義(圖 1.4.2)。

圖 1.4.2 summary 定義

Summary 會將採集到的數據經過正態分布逼近得出對應分位點的採樣數據,數據流如圖 1.4.3 所示。

圖 1.4.3 Summary 數據流

接下來看 summary.Observe 實現,圖 1.4.4 和 1.4.5 中加入了代碼邏輯的註解。

圖 1.4.4 summary.Observe 實現

圖 1.4.5 summary.asyncFlush 的實現

再看 summary.Write 實現,圖 1.4.6 中加入了代碼邏輯的註解。

圖 1.4.6 summary.Write 實現

集成優化建議

客戶端集成時,需要關注採集監控數據對程序性能和可靠性的影響,同時也需要關注數據完備性,即採集到的數據應完整、正確地反映監控對象的狀態和變化,筆者提出以下兩點思路:

為監控對象定義「恰當」的監控數據集,「恰當」要求在詳細設計階段梳理並細化整個監控對象,不引入多餘的監控數據,也不應該出現監控盲點

根據每個監控數據的實際情況選擇合理的數據結構

Go 客戶端為 HTTP 層的集成提供了方便的 API,但使用中需要注意不要使用 github.com/prometheus/client_golang/prometheus 下定義的已經 deprecated 的 Instrument 函數(如圖 1.5.1 中注釋部分),除了會引入額外(通常不需要)的監控數據,不僅會對程序性能造成不利影響,而且可能存在危險的 race(如計算請求大小時存在 goroutine 並發地訪問 Header 邏輯)。

圖 1.5.1 InstrumentHandler(Deprecated)

Go 客戶端在後續的版本中給出了優化的 API,即 github.com/prometheus/client_golang/prometheus/promhttp 下的實現,為 HTTP Handler 的不同監控數據定義了獨立的 InstrumentHandlerXXX(圖 1.5.2),讓監控數據集保持靈活可控,完全規避了圖 1.5.1 中提到的幾個問題。

圖 1.5.2 promhttp 下的 InstrumentHandlerXXX

另外一個難點是根據實際使用場景,從 Histogram 和 Summary 中作出選擇以及給予合理的初始化配置。

Histogram 常使用 histogram_quantile 執行數據分析, histogram_quantile 函數通過分段線性近似模型逼近採樣數據分布的 UpperBound(如圖 1.5.3),誤差是比較大的,其中紅色曲線為實際的採樣分布(正態分布),而實心圓點是 Histogram 的 bucket(0.01 0.25 0.50 0.75 0.95),當求解 0.9 quantile 的採樣值時會用 (0.75, 0.95) 兩個相鄰的的 bucket 來線性近似。

圖 1.5.3 histogram_quantile 逼近正態分布

而 Summary 的分位點是客戶端預先定義好的,已知分位點可以求該分位點的採樣值,相比 Histogram 而言能更準確地獲取分位點的採樣值。

當然,Summary 精度高的代價是在客戶端增加了額外的計算開銷,而且 Summary 結構有頻繁的全局鎖操作,對高並發程序性能存在一定影響,圖 1.5.4 是對 Histogram 和 Summary 分析 Benchmark 的結果,Observe 和 Write 操作都有著指數級別的差異,需要結合實際應用場景作出選擇。

圖1.5.4 Histogram和Summary Benchmarking

PromQL

PromQL 是 Prom 中的查詢語言,提供了簡潔的、貼近自然語言的語法實現時序數據的分析計算。

表達式(Expression)是其中承載數據計算邏輯的部分,對表達式的準確理解有助於充分利用 promql 提供的計算和分析能力,本節先結合一個相對複雜的表達式來介紹 PromQL 的計算過程,然後對部分有代表性的函數實現進行了源碼分析。

計算過程

PromQL 表達式輸入是一段文本,Prom 會解析這段文本,將它轉化為一個結構化的語法樹對象,進而實現相應的數據計算邏輯,這裡選用一個相對比較複雜的表達式為例:

sum(avg_over_time(go_goroutines[5m]))by (instance)

上述表達式可以從外往內分解為三層:

sum(…) by (instance):序列縱向分組合併序列(包含相同的 instance 會分配到一組)

avg_over_time(…)

go_goroutines[5m]

調用 Prom Restful API 查詢表達式計算工作流如圖 2.1.1 所示,請求數據的時候給出的 step 參數就是這裡的 interval,它設定結果中相鄰兩個點的間隔,對 promql 的每次 evaluator 都是針對某個確定的時間點和 statement 來計算的,得到一個 vector(時間戳相同的向量)。Prom 可以將異構(時間戳不一致)的多維時間序列經過計算轉化為同構(時間戳一致)的多維時間序列。

圖 2.1.1 Restful API 查詢表達式計算工作流

先看 go_goroutines[5m] 的計算,這是一個某個時間點的 MatrixSelector 對象(圖 2.1.2)。

圖 2.1.2 MatrixSelector計算go_goroutines[5m]

此處 iterator 是序列篩選結果的順序訪問介面,圖 2.1.2 中獲取某個時間點往前的一段歷史數據,這是一個二維矩陣 (matrix),進而由外層函數將這段歷史數據匯總成一個 vector(圖 2.1.3)。

值得一提的是,很多函數(如 rate)都需要傳入 matrix,儘管如此,這些函數的輸出依然是針對某個時間點的 vector,它僅僅是在計算某個時間點的 vector 時考察了一部分歷史數據而已。

圖 2.1.3 avg_over_time 實現

最後來看關鍵字(keyword)sum 的實現,這裡注意 sum 不是函數(Function),圖 2.1.4 給出了所有關鍵字列表。

圖 2.1.4 關鍵字列表

sum 關鍵字的完整語法比較複雜,本文中只介紹例子中給出的 sum(…) by (instance)。

圖 2.1.5 sum(…) by (instance) 實現

至此輸出某個時間點的結果向量,整個表達式的計算過程在 Excel 中集中展示如圖 2.1.6 所示。

圖 2.1.6 sum(avg_over_time(go_goroutines[5m])) by (instance) 計算過程

PromQL 有三個很簡單的原則:

任意 PromQL 返回的結果都不是原始數據,即使查詢一個具體的 Metric(如 go_goroutines),結果也不是原始數據

任意 Metrics 經過 Function 計算後會丟失 name Label

子序列間具備完全相同的 Label/Value 鍵值對(可以有不同的 name)才能進行代數運算

特彆強調一些,如 2.1.1 所述,PromQL 在計算時使用的等距 interval 時間點,每個 interval 時間點的結果都是利用附近的採樣點經過某種形式的估算或近似得到的,所以在 Prom 中提諸如「1:28:07 AM 發生了 113 次某種事件」是不準確的,PromQL 所有計算結果都存在誤差。

有意思的是,在 Prom 中對多維時間序列進行代數運算時,不需要嚴格檢查兩邊的矩陣一致性,因為 PromQL 只會處理相同 Label/Value 的序列之間的代數運算,圖 2.1.7 中對兩個不相關的 Metric 進行了代數運算,來說明代數運算的基本原理,這在一些以「資料庫」為核心的系統中,如 influxdb,涉及跨表運算,無論是表達式複雜度還是計算性能都會有影響。

圖 2.1.7 序列的代數運算

最後需要特別提的一點是,PromQL 表達式計算的原始數據集是共享內存空間的,但計算的中間結果是不共享內存空間的,所以從優化內存佔用的角度來看,應該將常用的表達式持久化成 Metric,減少動態計算過程,讓內存使用做到可控,這可以藉助 Recording Rules 完成 。

部分函數(Function)實現

Prom 提供了豐富的函數(Function)庫來對數據做複雜分析,本節通過介紹幾個有代表性的函數實現來介紹其用途,希望能幫助讀者準確理解表達式計算結果背後的工程含義。

delta/rate/increase

delta/rate/increase 背後共享了相同的計算邏輯(圖 2.2.1),僅僅是參數不同。

圖 2.2.1 delta/rate/increase 函數入口

來看 extrapolatedRate 實現(圖 2.2.2),基於線性外插演算法估計了 interval 時間點的採樣值增量,Prom 實現中大量使用了線性插值。基本原理很簡單,計算 range 範圍內採樣點頭尾斜率,然後線性延伸至實際 interval 時間點。

圖 2.2.2 extrapolatedRate

特別提一下此處的兩個參數 isCounter 和 isRate,其中 isCounter=true 說明數據需要保證單調遞增,當 Counter 的客戶端重啟後,數據會歸零,出現非單調遞增的數據,那麼 isCounter 可以控制是否對該數據進行修正;isRate=true 用來對數據做採樣範圍內的均值,其結果表徵當前時間點一秒內的採樣值增量(秒級別增量)。

現在回頭看圖 2.2.1 中 delta/rate/increase 的參數就很明朗了(表 2.2.1)。

可見 delta 在處理數據時,不假設數據單調遞增(isCounter=false),適合用來處理 Gauge 數據;而 increase 適合處理 Counter 數據,並獲取 range 範圍內增量;rate 適合處理 counter 並獲取 range 範圍內的秒級增量。

XXX_over_time

XXX_over_time 實現 range 範圍內數據的橫向匯總,即採用 range 範圍內的一定量歷史數據估算當前時間點的值,其中 XXX 可以是 avg/sum/max/min 等動詞,圖 2.2.3 中為 XXX_over_time 中的函數 。

圖 2.2.3 XXX_over_time 涉及的函數

由於它們的區別僅僅是對 range 內數據進行橫向匯總時的計算方式不同,此處不做一一介紹,只關注其中的 avg_over_time 實現(圖 2.2.4)。

圖 2.2.4 avg_over_time

avg_over_time 的核心邏輯在 aggrOverTime 中實現,見圖 2.2.5。

圖 2.2.5 aggrOverTime

XXX_over_time 常用來做數據平滑,過濾數據中的異常點,其中 avg_over_time 就是常見的「滑動窗口平均」,在信號處理中為一種低通濾波器實現。

histogram_quantile

histogram_quantile(圖 2.2.6)是 Prom 中比較難以理解的函數之一,可以根據 Histogram 估計估算採樣數據在某個正態分布分位點的值(實際上估計的是 Upper Bound,即上限)。

圖 2.2.6 histogram_quantile

估算quantile採樣值邏輯在bucketQuantile(圖 2.2.7)函數中實現。

圖 2.2.7 bucketQuantile

總結

Prom 是一種典型的基於 Metric 的監控系統,Metric 是多維時序數據分析在工程中的一種表現形式。社區中常將 Kubernetes 和 Prometheus 放到一起討論,它的設計理念和 Kubernetes 也如出一轍:二者都為特定問題提出了標準或協議,為終端用戶提供了易用的介面,專註於提供領域價值。

Prom 數據採集主要是通過 pull 模型實現的,主動從客戶端拉取數據,減少了監控對象對外部系統的依賴,這種模型下監控對象只需維護少量客戶端數據,保持可控、簡單的實現,降低了維護複雜客戶端邏輯的風險。另外,Prom 為一些臨時存在的進程,如批處理任務,提供了 Push Gateway,這些客戶端可以將數據 push 到 Push Gateway 中,然後由 Push Gateway 提供 pull 介面將數據暴露給 Prom Server。

相比 Prom,常見的 Metric 監控方案(如 InfluxDB 的 metrics 客戶端實現 https://github.com/rcrowley/go-metrics )都是 push 模型,在客戶端需要維護採樣數據生命周期(如長時間沒有存儲成功的數據需要丟棄等),還需要避免客戶端在數據採集和存儲過程中可能出現的資源泄漏。

此外,PromQL 是 Prom 中一個爭議和亮點並存的點,它提供了友好的、貼近時序數據語義的語法,對時序數據分析有著豐富的支持,如 Prom 考慮了 Counter 這種單調遞增數據由於客戶端反覆重啟導致數據歸零的問題,Prom 中很多函數在計算的時候就對這樣的數據進行了容錯,對數據分析完全透明,極大地提升了易用性;同時 PromQL 提供了 histogram_quantile 根據 Histogram 來估算 quantile 值的計算支持,讓 quantile 在 Prom 端計算可以降低客戶端帶來的額外性能負擔。

總之,Prom 數據模型、分析計算介面的設計上都有著良好的一致性和擴展性。基於 pull 的數據採集模型一方面降低了客戶端複雜度和對外部系統的依賴,另一方面也讓客戶端實現自由擴展。反觀很多基於 push 模型的監控系統實現,瞬間擴展可能使監控系統服務端出現性能瓶頸,波及整個系統;還有 PromQL 簡潔的介面讓複雜的時序數據分析變得直觀,很多工程上需要處理的數據預處理 Prom 都已經內置了,減少了數據預處理成本。

作者介紹

楊諭黔,FreeWheel 基礎架構部 高級軟體工程師。 目前主要從事服務化框架、容器化平台相關的研發與推廣。關注和感興趣的技術主要有 Golang, Docker, Kubernetes 和它們周邊生態等。

細說雲計算


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 細說雲計算 的精彩文章:

一圖看懂全球雲計算生態——「雲計算周期表」

TAG:細說雲計算 |