程式碼插樁

本頁提供了一套關於如何對程式碼進行插樁的指導原則。

如何插樁

簡而言之,就是對一切進行插樁。每個庫、子系統和服務都應該至少有幾個指標,以便讓你對其效能有一個粗略的瞭解。

插樁應當是程式碼中不可分割的一部分。在你要使用指標的同一個檔案中例項化指標類。這樣在追蹤錯誤時,就可以輕鬆地從告警跳轉到控制檯,再跳轉到程式碼。

服務的三種類型

出於監控目的,服務通常可以分為三種類型:線上服務、離線處理和批處理作業。它們之間存在重疊,但每個服務往往都能很好地歸入其中一類。

線上服務系統

線上服務系統是指人類或其他系統期望得到即時響應的系統。例如,大多數資料庫和 HTTP 請求都屬於這一類。

此類系統中的關鍵指標是已執行的查詢數、錯誤數和延遲。正在處理的請求數也很有用。

關於計算失敗查詢的數量,請參閱下文的 失敗 一節。

線上服務系統應該在客戶端和伺服器端同時進行監控。如果兩端觀察到不同的行為,這對於除錯來說是非常有用的資訊。如果一個服務有許多客戶端,那麼由該服務單獨跟蹤每個客戶端是不現實的,因此它們必須依賴於自己的統計資料。

在計算查詢時,不論是在開始時還是結束時,請保持一致。建議在結束時進行計算,因為這會與錯誤和延遲統計資料相吻合,並且通常更容易編寫程式碼。

離線處理

對於離線處理,沒有人處於主動等待響應的狀態,且通常會採用批次處理。處理過程也可能包含多個階段。

對於每個階段,跟蹤輸入的專案、正在處理的專案、上一次處理的時間以及輸出的專案。如果是批次處理,還應該跟蹤輸入和輸出的批次。

瞭解系統上一次處理某些內容的時間有助於檢測系統是否已停滯,但這屬於非常區域性的侷限資訊。更好的方法是在系統內傳送心跳:某種會一直傳遞下去的虛擬專案,其中包含其被插入時的時間戳。每個階段都可以匯出其見過的最新心跳時間戳,從而讓你瞭解專案在系統中傳播所需的時間。對於沒有空閒期(即始終在進行處理)的系統,可能不需要顯式的心跳。

批處理作業

離線處理與批處理作業之間的界限較為模糊,因為離線處理可能會在批處理作業中完成。批處理作業的特點是它們並非連續執行,這使得對它們進行抓取變得很困難。

批處理作業的關鍵指標是上一次成功的執行時間。跟蹤作業的每個主要階段耗時、總執行時間以及上一次作業完成的時間(成功或失敗)也是很有用的。這些都是儀表盤(Gauge)指標,並且應當 推送到 PushGateway。通常還有一些特定於作業的整體統計資料也值得跟蹤,例如處理的記錄總數。

對於執行時間超過幾分鐘的批處理作業,使用基於拉取(pull)的監控對它們進行抓取也是很有用的。這使你能夠像對待其他型別的作業一樣,跟蹤這些指標隨時間的變化,例如資源使用情況以及與其他系統通訊時的延遲。如果作業開始變慢,這有助於進行除錯。

對於執行非常頻繁的批處理作業(例如,每 15 分鐘以上執行一次),你應該考慮將它們轉換為守護程序,並將其作為離線處理作業來處理。

子系統

除了三種主要服務型別之外,系統還包含也應當進行監控的子部件。

類庫應當直接提供插樁功能,無需使用者進行額外的配置。

如果該庫用於訪問程序外部的某些資源(例如網路、磁碟或程序間通訊 IPC),至少應跟蹤總體查詢次數、錯誤數(如果可能發生錯誤)以及延遲。

根據該庫的體量大小,跟蹤該庫本身的內部錯誤和延遲,以及你認為可能對分析有用的任何通用統計資料。

一個庫可能會被應用程式的多個獨立部分用於訪問不同的資源,因此請注意在適當的情況下使用標籤來區分不同的用途。例如,資料庫連線池應當區分它所連線的資料庫,而沒有必要區分 DNS 客戶端庫的各個使用者。

日誌記錄

作為一般規則,每有一行日誌記錄程式碼,你都應該對應有一個增加的計數器(Counter)。如果你發現一條有趣的日誌訊息,你肯定會希望能夠看到它發生的頻率以及持續了多久。

如果在同一個函式中有多個密切相關的日誌訊息(例如,if 或 switch 語句的不同分支),有時對所有這些訊息統一遞增一個相同的計數器也是合理的。

通常,將應用程式整體記錄的 info/error/warning 日誌行數總和匯出,並在釋出過程中檢查是否存在顯著差異,也是非常有用的。

失敗

失敗的處理方式應與日誌記錄類似。每當發生失敗時,都應該遞增一個計數器。與日誌記錄不同的是,根據程式碼結構的不同,錯誤可能還會向上丟擲到更通用的錯誤計數器中。

在報告失敗時,通常應該有另一個表示總嘗試次數的指標。這樣可以很容易地計算出失敗率。

執行緒池

對於任何型別的執行緒池,關鍵指標是排隊的請求數、正在使用的執行緒數、匯流排程數、已處理的任務數以及它們所花費的時間。跟蹤任務在佇列中等待的時間也很有用。

快取

快取的關鍵指標是總查詢量、命中率、整體延遲,以及該快取所服務的線上系統其本身的查詢次數、錯誤數和延遲。

收集器

在實現非簡易的自定義指標收集器時,建議匯出一個表示收集耗時(以秒為單位)的儀表盤(Gauge)指標,以及另一個表示所遇到錯誤數量的指標。

這是可以將持續時間匯出為儀表盤(Gauge)而不是彙總(Summary)或直方圖(Histogram)的兩種情況之一,另一種情況是批處理作業的持續時間。這是因為這兩者都只代表該特定推送/抓取的資訊,而不是跟蹤一段時間內的多次持續時間。

注意事項

在進行監控時,有一些通用的注意事項,也有一些特別針對 Prometheus 的注意事項。

使用標籤

很少有監控系統具有標籤的概念以及能利用它們的表示式語言,因此這需要一些時間來適應。

當你想要對多個指標進行累加/求平均/求和時,它們通常應該是一個帶有標籤的單一指標,而不是多個指標。

例如,不要建立 http_responses_500_totalhttp_responses_403_total,而是建立一個名為 http_responses_total 的單一指標,並帶有一個用於 HTTP 響應程式碼的 code 標籤。然後,你可以在規則和圖表中將整個指標作為一個整體進行處理。

根據經驗,絕不應該透過程式動態生成指標名稱的任何部分(而應使用標籤代替)。唯一的例外是在代理來自另一個監控/插樁系統的指標時。

另請參閱 命名 一節。

不要過度使用標籤

每個標籤集都是一個額外的時間序列,會產生記憶體、CPU、磁碟 and 網路開銷。通常,這些開銷是微不足道的,但在擁有大量指標、數百臺伺服器且帶有數百個標籤集的場景中,這些開銷會迅速累積。

作為一般準則,儘量將指標的基數保持在 10 以下,而對於超過該基數的指標,應儘量將其限制在整個系統中的少數幾個。你的絕大多數指標都不應該有標籤。

如果你的某個指標的基數超過 100,或者有增長到這麼大的潛力,請研究替代解決方案,例如減少維度,或者將分析從監控系統轉移到通用處理系統。

為了讓你對底層的具體數值有更直觀的瞭解,讓我們來看看 node_exporter。node_exporter 會暴露每個掛載檔案系統的指標。每個節點對於(例如)node_filesystem_avail 都會有幾十個時間序列。如果你有 10,000 個節點,你最終將得到大約 100,000 個 node_filesystem_avail 的時間序列,這對於 Prometheus 來說是完全可以輕鬆處理的。

如果你現在還要為每個使用者新增配額,那麼在 10,000 個節點上擁有 10,000 個使用者的情況下,時間序列的數量很快就會達到雙位數的百萬級別。這對於當前版本的 Prometheus 來說開銷太大了。即使數量較小,也會存在機會成本,因為你無法在這臺機器上保留其他可能更有用的指標了。

如果你不確定,可以先不使用標籤,隨著具體用例的出現,再逐步新增更多標籤。

計數器與儀表盤,彙總與直方圖

瞭解對於給定的指標應該使用四種主要指標型別中的哪一種非常重要。

在計數器(Counter)和儀表盤(Gauge)之間進行選擇時,有一個簡單的經驗法則:如果數值可以減少,那麼它就是儀表盤(Gauge)。

計數器(Counter)只能遞增(以及在程序重啟等情況下重置)。它們適用於累積事件發生的次數,或者每次事件中某種事物的累積量。例如,HTTP 請求的總數,或 HTTP 請求中傳送的總位元組數。原始計數器數值很少直接有用。請使用 rate() 函式來獲取它們每秒增長的速率。

儀表盤(Gauge)可以被設定,並可升可降。它們適用於狀態快照,例如正在處理的請求數、空閒/總記憶體或溫度。你絕不應該對儀表盤(Gauge)使用 rate()

彙總和直方圖是更復雜的指標型別,將在 它們專屬的章節 中討論。

使用時間戳,而不是“自某事發生以來的時間”

如果你想跟蹤自某事發生以來所經過的時間,請匯出該事件發生時的 Unix 時間戳,而不是自該事件發生以來所經過的時間。

匯出時間戳後,你可以使用表示式 time() - my_timestamp_metric 來計算自該事件發生以來的時間。這樣可以省去更新邏輯,並能防止更新邏輯卡死。

內層迴圈

通常,插樁帶來的額外資源開銷與其為運維和開發帶來的巨大收益相比,完全是微不足道的。

對於效能至關重要、或在給定程序中每秒呼叫超過 10 萬次的程式碼,你可能需要注意更新指標的頻次和數量。

根據競爭情況,遞增一個 Java 計數器需要 12-17ns  的時間。其他語言的效能也類似。如果這個時間對你的內層迴圈有顯著影響,請限制在內層迴圈中遞增的指標數量,並儘可能避免使用標籤(或者快取標籤查詢的結果,例如 Go 中的 With() 返回值或 Java 中的 labels() 返回值)。

還要注意涉及時間或持續時間的指標更新,因為獲取時間可能會涉及系統呼叫。與所有涉及效能敏感程式碼的情況一樣,基準測試是確定任何特定更改所帶來影響的最佳方法。

避免缺失指標

在發生某些事情之前一直不存在的時間序列很難處理,因為通常的簡單操作已不足以正確處理它們。為了避免這種情況,請為你預先知道可能存在的任何時間序列匯出一個預設值,例如 0

大多數 Prometheus 客戶端庫(包括 Go、Java 和 Python)會自動為沒有標籤的指標匯出 0

本頁內容