摘要:本文將介紹基於最佳實踐的 Metrics 設計方法,並結合具體的場景實例——TKE 的網絡組件 IPAMD 的內部監控,以個人實踐經驗談一談如何設計和實現適合的、能夠更好反映系統實時狀態的監控指標(Metrics)。Prometheus 的部署和使用可以說是簡單易上手,但是如何針對實際的問題和需求設計適宜的 Metrics 卻並不是那麼直接可行,反而需要優先解決暴露出來的諸多不確定問題,比如何時選用 Vector,如何設計適宜的 buckets,Summary 和 Histogram 指標類型的取捨等。

【編者的話】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,標籤有region和method,以此表示不同地域服務器的不同請求方法的請求延遲。

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

Counter

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

方法:

type Counter interface {

Metric

Collector



// 自增1

Inc()

// 把給定值加入到計數器中,若值小於 0 會 panic

Add(float64)

} 

常測量對象:

  • 請求的數量
  • 任務完成的數量
  • 函數調用次數
  • 錯誤發生次數
  • ……
    ####Gauge
    定義:表示一個可增可減的數字變量,初值爲0

方法:

type Gauge interface {

Metric

Collector

Set(float64)    // 直接設置成給定值

Inc()   // 自增1

Dec()   // 自減1

Add(float64)     // 增加給定值,可爲負

Sub(float64)    // 減少給定值,可爲負

// SetToCurrentTime 將 Gauge 設置成當前的 Unix 時間戳

SetToCurrentTime()

} 

常測量對象:

  • 溫度
  • 內存用量
  • 併發請求數
  • ……

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。

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 組合做聚合。

Histogram 和 Summary 簡單對比

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

3、Metrics 設計的最佳實踐

如何確定需要測量的對象

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

思路 1:從需求出發

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

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

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

  • 反映用戶體驗,衡量系統核心性能。如:在線系統的時延,作業計算系統的作業完成時間等。
  • 反映系統的服務量。如:請求數,發出和接收的網絡包大小等。
  • 幫助發現和定位故障和問題。如:錯誤計數、調用失敗率等。
  • 反映系統的飽和度和負載。如:系統佔用的內存、作業隊列的長度等。

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

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

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

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

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

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

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

  • 使用的庫(Libraries): 調用次數,成功數,出錯數,調用的時延。
  • 日誌(Logging):計數每一條寫入的日誌,從而可找到每條日誌發生的頻率和時間。
  • Failures: 錯誤計數。
  • 線程池:排隊的請求數,正在使用的線程數,總線程數,耗時,正在處理的任務數等。
  • 緩存:請求數,命中數,總時延等。
  • ……

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

如何選用 Vector

選用 Vec 的原則:

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

例子:

  • 不同資源對象的請求延遲
  • 不同地域服務器的請求延遲
  • 不同 http 請求錯誤的計數
  • ……

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

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

如何確定 Label

根據上文,常見 Label 的選擇有:

  • resource
  • region
  • type
  • ……

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

此外,不建議下列做法:

my_metric{label=a} 1

my_metric{label=b} 6

my_metric{label=total} 7

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

如何命名 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

如何設計適宜的 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 設計與規劃

組件簡介

該組件用於支持騰訊雲 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-)工作原理和流程

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

背景:

  • IP 分配/釋放對時延比較敏感,爲了方便確定 IP 分配/釋放過程中性能瓶頸是由我們自身代碼造成的還是底層模塊造成的(如 ipamD 調用的 VPC 接口等)。同時也方便對我們的代碼和推進底層模塊的性能優化。
  • ipamD 運行過程中可能會出現故障等問題,爲了及時發現故障,定位問題,也需要有內部監控。

需求:

  • 需要能夠統計 IP 分配和釋放各個階段的時延,以確定性能瓶頸
  • 需要知道當前的併發請求數,以確定 ipamD 負載
  • VPC 接口 IP 分配/釋放,彈性網卡創建/綁定/解綁/釋放耗時比較長,並且經常有失敗情況。需要能夠統計這些接口的時延和調用成功率,以定位性能瓶頸。
  • Node Controller,StatefulSet Controller 進行 sync 階段會有一系列流程,希望能清楚主要流程耗時,方便定位瓶頸
  • 彈性網卡的創建/刪除等過程中容易產生髒數據,需要能夠統計髒數據的個數,以發現髒數據問題。
  • 需要有較強的實時性,能夠清楚的看到最近(~分鐘級別)系統的運行狀態

我們的場景:

  • ipamD 是部署在每個用戶集羣中的一個組件
  • 每個用戶集羣內有 Prometheus Server 做聚合,然後每個 region 也有 Server 去拉取數據

總體設計

因此,需要以下幾類 Metric:

  • ip alloc/free 各階段時延
  • 基本運行信息:請求併發數、內存用量、Goroutine 數,線程數
  • VPC 接口時延
  • VPC 接口調用成功率
  • controller sync 時延
  • 髒數據計數

Histogram vs. Summary

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

基於【Histogram 和 Summary 簡單對比】兩者對比,有如下分析:

Summary:

  • 優點:
    • 能夠非常準確的計算百分位數
    • 不需要提前知道數據的分佈
  • 缺點:
    • 靈活性不足,實時性需要通過 maxAge 來保證,寫死了後靈活性就不太夠(比如想知道更長維度的百分位數)
    • 在 client 端已經做了聚合,即在各個用戶集羣的 ipamD 中已經聚合了,我們如果需要觀察全部 user 下的百分位數數據是不行的(只能看均值)
    • 用戶集羣的 ipamD 的調用頻率可能很低(如小集羣或者穩定集羣),這種情況下 client 端聚合計算百分位數值失去意義(數據太少不穩定),如果把 maxAge 增大則失去實時性

Histogram:

  • 優點:
    • 兼具靈活性和實時性
    • 可以靈活的聚合數據,觀察各個尺度和維度下的數據
  • 缺點:
    • 需要提前知道數據的大致分佈,並以此設計出合適而準確的桶序列
    • 難以通過 Label 串聯多種 Metrics,因爲各個 Metrics 的數據分佈可能差異較大,如果都只用一種桶序列的話會導致百分位數計算差異較大

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

所以傾向於使用 Histogram。

Metrics 規劃示例

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

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

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

5、指標收集的 Golang 實現方案

總體實現思路

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

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
  • 侵入式設計,具有一定的耦合度

目前 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:數據類型類似但資源 strong text 類型、收集地點等不同,數據單位統一。
  • 如何確定 Label:可平均和可加和的,單位要統一;總和數據另外計。
  • 如何命名 Metrics 和 Label:見名知義,應包含監控的系統名/模塊名,指標名,單位等信息。
  • 如何設計適宜的 Buckets:依據數據分佈制定,密集部分桶區間較窄,總體桶分佈儘量接近階梯狀。
  • 如何取捨 Histogram 和 Summary:Histogram 計算誤差大,但靈活性較強,適用客戶端監控、或組件在系統中較多、或不太關心精確的百分位數值的場景;Summary 計算精確,但靈活性較差,適用服務端監控、或組件在系統中唯一或只有個位數、或需要知道較準確的百分位數值(如性能優化場景)的場景。

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

原文鏈接: https://mp.weixin.qq.com/s/3aL9IiWYlp4H7k3bxrk_TQ

相關文章