Prometheus Metrics 設計的最佳實踐和應用例項,看這篇夠了!

騰訊雲原生發表於2020-09-28

Prometheus 是一個開源的監控解決方案,部署簡單易使用,難點在於如何設計符合特定需求的 Metrics 去全面高效地反映系統實時狀態,以助力故障問題的發現與定位。本文即基於最佳實踐的 Metrics 設計方法,結合具體的場景例項——TKE 的網路元件 IPAMD 的內部監控,以個人實踐經驗談一談如何設計和實現適合的、能夠更好反映系統實時狀態的監控指標(Metrics)。該篇內容適於 Prometheus 或相關監控系統的初學者(可無任何基礎瞭解),以及近期有 Prometheus 監控方案搭建和維護需求的系統開發管理者。通過這篇文章,可以加深對 Prometheus Metrics 的理解,並能針對實際的監控場景提出更好的指標(Metrics)設計。

1 引言

Prometheus 是一個開源的監控解決方案,它能夠提供監控指標資料的採集、儲存、查詢以及監控告警等功能。作為雲原生基金會(CNCF)的畢業專案,Prometheus 已經在雲原生領域得到了大範圍的應用,並逐漸成為了業界最流行的監控解決方案之一。

Prometheus 的部署和使用可以說是簡單易上手,但是如何針對實際的問題和需求設計適宜的 Metrics 卻並不是那麼直接可行,反而需要優先解決暴露出來的諸多不確定問題,比如何時選用 Vector,如何設計適宜的 buckets,Summary 和 Histogram 指標型別的取捨等。然而,要想有效助力故障及問題的發現與定位,必須要有一個合理有效的 Metrics 去全面高效地反映系統實時狀態。

本文將介紹基於最佳實踐的 Metrics 設計方法,並結合具體的場景例項——TKE 的網路元件 IPAMD 的內部監控,以個人實踐經驗談一談如何設計和實現適合的、能夠更好反映系統實時狀態的監控指標(Metrics)。

本文之後的第 2 節將對 Prometheus 的 Metrics 做簡單的介紹,對此已有了解的讀者可跳過。之後第 3 節將介紹 Metrics 設計的最佳實踐。第 4 節將結合具體的例項應用相關設計方法。第 5 節將介紹 Golang 上指標收集的實現方案。

2 Prometheus Metrics Type 簡介

Prometheus Metrics 是整個監控系統的核心,所有的監控指標資料都由其記錄。Prometheus 中,所有 Metrics 皆為時序資料,並以名字作區分,即每個指標收集到的樣本資料包含至少三個維度的資訊:名字、時刻和數值。

而 Prometheus Metrics 有四種基本的 type:

  • Counter: 只增不減的單變數
  • Gauge:可增可減的單變數
  • Histogram:多桶統計的多變數
  • Summary:聚合統計的多變數

此外,Prometheus Metrics 中有一種將樣本資料以標籤(Label)為維度作切分的資料型別,稱為向量(Vector)。四種基本型別也都有其 Vector 型別:

  • CounterVec
  • GaugeVec
  • HistogramVec
  • SummaryVec

Vector 相當於一組同名同型別的 Metrics,以 Label 做區分。Label 可以有多個,Prometheus 實際會為每個 Label 組合建立一個 Metric。Vector 型別記錄資料時需先打 Label 才能呼叫 Metrics 的方法記錄資料。

如對於 HTTP 請求延遲這一指標,由於 HTTP 請求可在多個地域的伺服器處理,且具有不同的方法,於是,可定義名為 http_request_latency_seconds 的 SummaryVec,標籤有regionmethod,以此表示不同地域伺服器的不同請求方法的請求延遲。

以下將對每個型別做詳細的介紹。

2.1 Counter

  • 定義:是單調遞增的計數器,重啟時重置為0,其餘時候只能增加。

  • 方法:

    type Counter interface {  Metric  Collector  // 自增1  Inc()  // 把給定值加入到計數器中. 若值小於 0 會 panic  Add(float64)}
    
  • 常測量物件:

    • 請求的數量
    • 任務完成的數量
    • 函式呼叫次數
    • 錯誤發生次數
    • ...

2.2 Gauge

  • 定義:表示一個可增可減的數字變數,初值為0

  • 方法:

    type Gauge interface {  Metric  Collector  Set(float64)    // 直接設定成給定值  Inc()   // 自增1  Dec()   // 自減1  Add(float64)     // 增加給定值,可為負  Sub(float64)    // 減少給定值,可為負  // SetToCurrentTime 將 Gauge 設定成當前的 Unix 時間戳  SetToCurrentTime()}
    
  • 常測量物件:

    • 溫度
    • 記憶體用量
    • 併發請求數
    • ...

2.3 Histogram

  • 定義:Histogram 會對觀測資料取樣,然後將觀測資料放入有數值上界的桶中,並記錄各桶中資料的個數,所有資料的個數和資料數值總和。

  • 方法:

    type Histogram interface {  Metric  Collector  // Observe 將一個觀測到的樣本資料加入 Histogram 中,並更新相關資訊  Observe(float64)}
    
  • 常測量物件:

    • 請求時延
    • 回覆長度
    • ...各種有樣本資料
  • 具體實現:Histogram 會根據觀測的樣本生成如下資料:

    inf 表無窮值,a1,a2,...是單調遞增的數值序列。

    • [basename]_count: 資料的個數,型別為 counter
    • [basename]_sum: 資料的加和,型別為 counter
    • [basename]_bucket{le=a1}: 處於[-inf,a1]的數值個數
    • [basename]_bucket{le=a2}: 處於[-inf,a2]的數值個數
    • ...
    • [basename]_bucket{le=<+inf>}:處於[-inf,+inf]的數值個數,prometheus預設額外生成,無需使用者定義
  • Histogram 可以計算樣本資料的百分位數,其計算原理為:通過找特定的百分位數值在哪個桶中,然後再通過插值得到結果。比如目前有兩個桶,分別儲存了[-inf, 1]和[-inf, 2]的資料。然後現在有20%的資料在[-inf, 1]的桶,100%的資料在[-inf, 2]的桶。那麼,50%分位數就應該在[1, 2]的區間中,且處於(50%-20%) / (100%-20%) = 30% / 80% = 37.5% 的位置處。Prometheus計算時假設區間中資料是均勻分佈,因此直接通過線性插值可以得到 (2-1)*3/8+1 = 1.375.

2.4 Summary

  • 定義:Summary 與 Histogram 類似,會對觀測資料進行取樣,得到資料的個數和總和。此外,還會取一個滑動視窗,計算視窗內樣本資料的分位數。

  • 方法:

    type Summary interface {  Metric  Collector  // Observe 將一個觀測到的樣本資料加入 Summary 中,並更新相關資訊  Observe(float64)}
    
  • 常測量物件:

    • 請求時延
    • 回覆長度
    • ...各種有樣本資料
  • 具體實現:Summary 完全是在client端聚合資料,每次呼叫 obeserve 會計算出如下資料:

    • [basename]_count: 資料的個數,型別為 counter
    • [basename]_sum: 資料的加和,型別為 counter
    • [basename]{quantile=0.5}: 滑動視窗內 50% 分位數值
    • [basename]{quantile=0.9}: 滑動視窗內 90% 分位數值
    • [basename]{quantile=0.99}: 滑動視窗內 99% 分位數值
    • ...

實際分位數值可根據需求制定,且是對每一個 Label 組合做聚合。

2.5 Histogram 和 Summary 簡單對比

可以看出,Histogram 和 Summary 型別測量的物件是比較接近的,但根據其實現方式和其本身的特點,在效能耗費、適用場景等方面具有一定差別,本文總結如下:

3 Metrics 設計的最佳實踐

3.1 如何確定需要測量的物件

在具體設計 Metrics 之前,首先需要明確需要測量的物件。需要測量的物件應該依據具體的問題背景、需求和需監控的系統本身來確定。

思路1:從需求出發

Google 針對大量分散式監控的經驗總結出四個監控的黃金指標,這四個指標對於一般性的監控測量物件都具有較好的參考意義。這四個指標分別為:

  • 延遲:服務請求的時間。
  • 通訊量:監控當前系統的流量,用於衡量服務的容量需求。
  • 錯誤:監控當前系統所有發生的錯誤請求,衡量當前系統錯誤發生的速率。
  • 飽和度:衡量當前服務的飽和度。主要強調最能影響服務狀態的受限制的資源。例如,如果系統主要受記憶體影響,那就主要關注系統的記憶體狀態。

而筆者認為,以上四種指標,其實是為了滿足四個監控需求:

  • 反映使用者體驗,衡量系統核心效能。如:線上系統的時延,作業計算系統的作業完成時間等。
  • 反映系統的服務量。如:請求數,發出和接收的網路包大小等。
  • 幫助發現和定位故障和問題。如:錯誤計數、呼叫失敗率等。
  • 反映系統的飽和度和負載。如:系統佔用的記憶體、作業佇列的長度等。

除了以上常規需求,還可根據具體的問題場景,為了排除和發現以前出現過或可能出現的問題,確定相應的測量物件。比如,系統需要經常呼叫的一個庫的介面可能耗時較長,或偶有失敗,可制定 Metrics 以測量這個介面的時延和失敗數。

思路2:從需監控的系統出發

另一方面,為了滿足相應的需求,不同系統需要觀測的測量物件也是不同的。在 官方文件 的最佳實踐中,將需要監控的應用分為了三類:

  • 線上服務系統(Online-serving systems):需對請求做即時的響應,請求發起者會等待響應。如 web 伺服器。
  • 線下計算系統(Offline processing):請求發起者不會等待響應,請求的作業通常會耗時較長。如批處理計算框架 Spark 等。
  • 批處理作業(Batch jobs):這類應用通常為一次性的,不會一直執行,執行完成後便會結束執行。如資料分析的 MapReduce 作業。

對於每一類應用其通常情況下測量的物件是不太一樣的。其總結如下:

  • 線上服務系統:主要有請求、出錯的數量,請求的時延等。
  • 線下計算系統:最後開始處理作業的時間,目前正在處理作業的數量,發出了多少 items, 作業佇列的長度等。
  • 批處理作業:最後成功執行的時刻,每個主要 stage 的執行時間,總的耗時,處理的記錄數量等。

除了系統本身,有時還需監控子系統:

  • 使用的庫(Libraries): 呼叫次數,成功數,出錯數,呼叫的時延。
  • 日誌(Logging):計數每一條寫入的日誌,從而可找到每條日誌發生的頻率和時間。
  • Failures: 錯誤計數。
  • 執行緒池:排隊的請求數,正在使用的執行緒數,匯流排程數,耗時,正在處理的任務數等。
  • 快取:請求數,命中數,總時延等。
  • ...

最後的測量物件的確定應結合以上兩點思路確定。

3.2 如何選用 Vector

選用 Vec 的原則:

  • 資料型別類似但資源型別、收集地點等不同
  • Vec 內資料單位統一

例子:

  • 不同資源物件的請求延遲
  • 不同地域伺服器的請求延遲
  • 不同 http 請求錯誤的計數
  • ...

此外,官方文件 中建議,對於一個資源物件的不同操作,如 Read/Write、Send/Receive, 應採用不同的 Metric 去記錄,而不要放在一個 Metric 裡。原因是監控時一般不會對這兩者做聚合,而是分別去觀測。

不過對於 request 的測量,通常是以 Label 做區分不同的 action。

3.3 如何確定 Label

根據3.2,常見 Label 的選擇有:

  • resource
  • region
  • type
  • ...

確定 Label 的一個重要原則是:同一維度 Label 的資料是可平均和可加和的,也即單位要統一。如風扇的風速和電壓就不能放在一個 Label 裡。

此外,不建議下列做法:

my_metric{label=a} 1my_metric{label=b} 6my_metric{label=total} 7

即在 Label 中同時統計了分和總的資料,建議採用 PromQL 在伺服器端聚合得到總和的結果。或者用另外的 Metric 去測量總的資料。

3.4 如何命名 Metrics 和 Label

好的命名能夠見名知義,因此命名也是良好設計的一環。

Metric 的命名:

  • 需要符合 pattern: [a-zA-Z:][a-zA-Z0-9:]*

  • 應該包含一個單詞作為字首,表明這個 Metric 所屬的域。如:

    • prometheus_notifications_total
    • process_cpu_seconds_total
    • ipamd_request_latency
  • 應該包含一個單位的單位作為字尾,表明這個 Metric 的單位。如:

    • http_request_duration_seconds
    • node_memory_usage_bytes
    • http_requests_total (for a unit-less accumulating count)
  • 邏輯上與被測量的變數含義相同。

  • 儘量使用基本單位,如 seconds,bytes。而不是 Milliseconds, megabytes。

Label 的命名:

  • 依據選擇的維度命名,如:

    • region: shenzhen/guangzhou/beijing
    • owner: user1/user2/user3
    • stage: extract/transform/load

3.5 如何設計適宜的 Buckets

根據前述 histogram 的統計原理可知,適宜的 buckets 能使 histogram 的百分位數計算更加準確。

理想情況下,桶會使得資料分佈呈階梯狀,即各桶區間內資料個數大致相同。如圖1所示,是本人在實際場景下配置的buckets 資料直方圖,y 軸為 buckets 內的資料個數,x 軸是各 buckets,可以看出其近似成階梯狀。這種情況下,當前桶個數下對資料的解析度最大,各百分位數計算的準確率較高。

圖1 較為理想的桶資料分佈

而根據筆者實踐經驗,為了達成以上目標,buckets 的設計可遵從如下經驗:

  • 需要知道資料的大致分佈,若事先不知道可先用預設桶 ({.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10})或 2 倍數桶({1,2,4,8...})觀察資料分佈再調整 buckets。
  • 資料分佈較密處桶間隔制定的較窄一些,分佈稀疏處可制定的較寬一些。
  • 對於多數時延資料,一般具有長尾的特性,較適宜用指數形式的桶(ExponentialBuckets)。
  • 初始桶上界一般覆蓋10%左右的資料,若不關注頭部資料也可以讓初始上界更大一些。
  • 若為了更準確計算特定百分位數,如90%,可在90%的資料處加密分佈桶,即減少桶的間隔。

4 例項:TKE-ENI-IPAMD Metrics 設計與規劃

4.1 元件簡介

該元件用於支援騰訊雲 TKE 的策略路由網路方案。在這一網路方案中,每個 pod 的 IP 都是 VPC 子網的一個IP,且繫結到了所在節點的彈性網路卡上,通過策略路由連通網路,並且使得容器可以支援騰訊雲的 VPC 的所有特性。

其中,在 2.0.0 版本以前,tke-eni-ipamd 元件是一個 IP 分配管理的 GRPC Server,其主要職責為:

  • cni IP 真正分配/刪除的 GRPC Server,分配/釋放 IP 會呼叫騰訊雲彈性網路卡介面執行相應的 IP 繫結/解綁操作
  • Node 控制器(用於給 Node 繫結/解綁彈性網路卡)
  • Stateulfset 控制器(用於給 Statefulset 預留 IP 資源)

其工作原理和流程如圖 2 所示:

圖2 tke-eni-ipamd(v2.0.0-) 工作原理和流程

4.2 IPAMD 的使用場景和我們的要求

背景:

  • ip 分配/釋放對時延比較敏感,為了方便確定 ip 分配/釋放過程中效能瓶頸是由我們自身程式碼造成的還是底層模組造成的(如 ipamd 呼叫的 vpc 介面等)。同時也方便對我們的程式碼和推進底層模組的效能優化。
  • ipamd 執行過程中可能會出現故障等問題,為了及時發現故障,定位問題,也需要有內部監控。

需求:

  • 需要能夠統計 ip 分配和釋放各個階段的時延,以確定效能瓶頸
  • 需要知道當前的併發請求數,以確定 IPAMD 負載
  • vpc 介面 ip分配/釋放,彈性網路卡建立/繫結/解綁/釋放耗時比較長,並且經常有失敗情況。需要能夠統計這些介面的時延和呼叫成功率,以定位效能瓶頸。
  • node controller,statefulset controller 進行 sync 階段會有一系列流程,希望能清楚主要流程耗時,方便定位瓶頸
  • 彈性網路卡的建立/刪除等過程中容易產生髒資料,需要能夠統計髒資料的個數,以發現髒資料問題。
  • 需要有較強的實時性,能夠清楚的看到最近(~分鐘級別)系統的執行狀態

我們的場景:

  • ipamd 是部署在每個使用者叢集中的一個元件
  • 每個使用者叢集內有 prometheus server 做聚合,然後每個 region 也有 server 去拉取資料

4.3 總體設計

因此,需要以下幾類 Metric:

  • ip alloc/free 各階段時延
  • 基本執行資訊:請求併發數、記憶體用量、goroutine 數,執行緒數
  • vpc 介面時延
  • vpc 介面呼叫成功率
  • controller sync 時延
  • 髒資料計數

4.4 Histogram vs. Summary

時延可選擇 Histogram 或 Summary 進行測量,如何選擇?

基於 2.5 節的兩者對比,有如下分析:

Summary:

  • 優點:

    1. 能夠非常準確的計算百分位數
    2. 不需要提前知道資料的分佈
  • 缺點:

    1. 靈活性不足,實時性需要通過 maxAge 來保證,寫死了後靈活性就不太夠(比如想知道更長維度的百分位數)
    2. 在 client 端已經做了聚合,即在各個使用者叢集的 ipamd 中已經聚合了,我們如果需要觀察全部 user 下的百分位數資料是不行的(只能看均值)
    3. 使用者叢集的 ipamd 的呼叫頻率可能很低(如小叢集或者穩定叢集),這種情況下 client 端聚合計算百分位數值失去意義(資料太少不穩定),如果把 maxAge 增大則失去實時性

Histogram:

  • 優點:

    1. 兼具靈活性和實時性
    2. 可以靈活的聚合資料,觀察各個尺度和維度下的資料
  • 缺點:

    1. 需要提前知道資料的大致分佈,並以此設計出合適而準確的桶序列
    2. 難以通過 Label 串聯多種 Metrics,因為各個 Metrics 的資料分佈可能差異較大,如果都只用一種桶序列的話會導致百分位數計算差異較大

Summary 的缺點過於致命,難以迴避。Histogram 的缺點可以通過增加工作量(即通過測試環境中的實驗來確定各 Metrics 的大致分佈)和增加 Metrics(不用 Label 區分)來較好解決。

所以傾向於使用 Histogram。

4.5 Metrics 規劃示例

詳細的 Metrics 規劃內容較多,這裡選取了一些代表性的樣例,列舉如下:

注1:DefBuckets指預設桶 ({.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10})。

注2:以上 buckets 持續微調中。

5 指標收集的 Golang 實現方案

5.1 總體實現思路

  • 利用 prometheus 的 golang client 實現自定義的 exporter(包括自定義的 Metrics ),並嵌入到 ipamd 程式碼中,以收集資料
  • 所有的 Metrics 作為 Metrics 包的外部變數可供其他包使用,呼叫測量方法
  • 自定義 exporter 參考 prometheus client golang example
  • 將收集到的資料通過 http server 暴露出來

5.2 Metrics 收集方案

方案1:非侵入式裝飾器模式

樣例: kubelet/kuberuntime/instrumented_services.go

type instrumentedRuntimeService struct {    service internalapi.RuntimeService}func recordOperation(operation string, start time.Time) {    metrics.RuntimeOperations.WithLabelValues(operation).Inc()    metrics.DeprecatedRuntimeOperations.WithLabelValues(operation).Inc()    metrics.RuntimeOperationsDuration.WithLabelValues(operation).Observe(metrics.SinceInSeconds(start))    metrics.DeprecatedRuntimeOperationsLatency.WithLabelValues(operation).Observe(metrics.SinceInMicroseconds(start))}func (in instrumentedRuntimeService) Status() (*runtimeapi.RuntimeStatus, error) {    const operation = "status"    defer recordOperation(operation, time.Now())    out, err := in.service.Status()    recordError(operation, err)    return out, err}

優點:

  • 上層呼叫函式處幾乎不用修改,只需修改呼叫的例項
  • 抽象較好,非侵入式設計,程式碼耦合度低

缺點:

  • 需單獨封裝每個呼叫函式,複用度低
  • 無法封裝內部函式,只能適用於測量對外服務函式的資料

方案2:defer 函式收集

樣例:

func test() (retErr error){    defer func(){        metrics.LatencySeconds.Observe(...)    }()    ...    func body    ...}

優點:

  • 上層呼叫函式處完全不用修改
  • 適用於所有函式的測量

缺點:

  • 有點濫用 defer
  • 侵入式設計,具有一定的耦合度

5.3 目前 IPAMD 的指標收集實現方案

  • 時延統計:通過 golang 的 time 模組計時,在函式中嵌入 time.Now 和並在其後 defer time.Since 來統計。
  • 呼叫成功率統計:呼叫次數在介面函式裡直接用 counter 進行統計,失敗次數在defer裡獲取命名返回值統計,最後在 prometheus server 端聚合的時候通過 PromQL 利用這兩個資料計算出呼叫成功率。
  • 併發請求數的統計:在最外層的 AddPodIP 和 DelPodIP 中,在函式中和 defer func 中分別呼叫Inc和Dec。

6 總結

本文介紹了 Prometheus Metrics 及最佳實踐的 Metrics 設計和收集實現方法,並在具體的監控場景—— TKE 的網路元件 IPAMD 的內部監控中應用了相關方法。

具體而言,本文基於最佳實踐,回答了 Prometheus Metrics 設計過程中的若干問題:

  • 如何確定需要測量的物件:依據需求(反映使用者體驗、服務量、飽和度和幫助發現問題等)和需監控的具體系統。
  • 何時選用 Vec:資料型別類似但資源型別、收集地點等不同,資料單位統一。
  • 如何確定 Label:可平均和可加和的,單位要統一;總和資料另外計。
  • 如何命名 Metrics 和 Label:見名知義,應包含監控的系統名/模組名,指標名,單位等資訊。
  • 如何設計適宜的 Buckets:依據資料分佈制定,密集部分桶區間較窄,總體桶分佈儘量接近階梯狀。
  • 如何取捨 Histogram 和 Summary:Histogram 計算誤差大,但靈活性較強,適用客戶端監控、或元件在系統中較多、或不太關心精確的百分位數值的場景;Summary 計算精確,但靈活性較差,適用服務端監控、或元件在系統中唯一或只有個位數、或需要知道較準確的百分位數值(如效能優化場景)的場景。

此外,Metrics 設計並不是一蹴而就的,需依據具體的需求的變化進行反覆迭代。比如需新增 Metrics 去發現定位可能出現的新問題和故障,再比如 Buckets 的設計也需要變化來適應測量資料分佈發生的變化,從而獲得更精確的百分位數測量值。

參考資料

  1. Prometheus 官方文件 :https://prometheus.io/docs/introduction/overview/Prometheus
  2. Go client library:https://github.com/prometheus/client_golang

【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公眾號,及時獲取更多幹貨!!

相關文章