編寫客戶端庫

本文件介紹了 Prometheus 客戶端庫應該提供哪些功能和 API,旨在保持各客戶端庫之間的一致性,使簡單的用例易於使用,並避擴音供可能引導使用者走入誤區的功能。

在撰寫本文時,已經支援了 10 種語言,因此我們對如何編寫客戶端庫已經有了很好的認識。這些準則旨在幫助新客戶端庫的作者開發出優秀的產品。

約定

MUST(必須)/ MUST NOT(絕不)/ SHOULD(應當)/ SHOULD NOT(不應當)/ MAY(可以)的含義與 https://www.ietf.org/rfc/rfc2119.txt  中的定義一致。

此外,ENCOURAGED(鼓勵)意味著該功能對於客戶端庫來說是理想的,但如果沒有也沒關係。換句話說,屬於錦上添花的功能。

注意事項

  • 充分利用每種語言的特性。

  • 常見的用例應當簡單易用。

  • 正確的方法應當是最簡單的方法。

  • 應當可以支援更復雜的用例。

常見的用例包括(按優先順序排序):

  • 不帶標籤的 Counter(計數器),廣泛分佈在庫/應用中。

  • 在 Summary/Histogram(直方圖)中為函式/程式碼塊進行耗時統計。

  • 使用 Gauge(儀表盤)來跟蹤事物的當前狀態(及其極限)。

  • 監控批處理作業。

整體結構

客戶端在內部必須(MUST)編寫為基於回撥的機制。客戶端通常應當(SHOULD)遵循這裡描述的結構。

核心類是 Collector。它有一個方法(通常稱為 'collect'),用於返回零個或多個指標及其樣本。Collector 會註冊到 CollectorRegistry 中。透過將 CollectorRegistry 傳遞給一個“橋接(bridge)”類/方法/函式來暴露資料,該橋接器會以 Prometheus 支援的格式返回指標。每次 CollectorRegistry 被抓取時,它必須回撥每個 Collector 的 collect 方法。

大多數使用者互動的介面是 Counter、Gauge、Summary 和 Histogram 的 Collector。這些代表單個指標,應該涵蓋使用者在對自己的程式碼進行插樁時絕大多數的用例。

更高階的用例(例如從另一個監控/插樁系統進行代理)需要編寫自定義的 Collector。也可能會有人想要編寫一個“橋接器”,該橋接器接收一個 CollectorRegistry 並以另一個監控/插樁系統能理解的格式產生資料,從而讓使用者只需考慮一套插樁系統。

CollectorRegistry 應當(SHOULD)提供 register()/unregister() 函式,並且應當(SHOULD)允許一個 Collector 被註冊到多個 CollectorRegistry。

客戶端庫必須(MUST)是執行緒安全的。

對於 C 語言等非面嚮物件語言,客戶端庫應當在切實可行的範圍內儘可能遵循這種結構的精神。

命名

客戶端庫應當(SHOULD)遵循本文件中提到的函式/方法/類名,同時兼顧它們所對應語言的命名規範。例如,set_to_current_time() 適合作為 Python 中的方法名,而在 Go 中 SetToCurrentTime() 更好,Java 中的規範則是 setToCurrentTime()。如果由於技術原因(例如不允許函式過載)導致名稱不同,文件/幫助字串應當(SHOULD)引導使用者瞭解其他名稱。

庫絕不(MUST NOT)提供與此處給出的名稱相同或相似,但語義不同的函式/方法/類。

指標

Counter、Gauge、Summary 和 Histogram 指標型別是使用者互動的主要介面。

Counter 和 Gauge 必須(MUST)是客戶端庫的一部分。Summary 和 Histogram 中至少必須(MUST)提供一個。

這些指標主要應當作為檔案靜態變數使用,即定義在被插樁程式碼所在的同一個檔案中的全域性變數。客戶端庫應當(SHOULD)支援這一點。常見的用例是對整段程式碼進行插樁,而不是在某個物件例項的上下文中對程式碼進行插樁。使用者不應該擔心在程式碼中到處傳遞他們的指標,客戶端庫應該為他們處理這些(如果它不這樣做,使用者就會圍繞該庫編寫一個封裝器使其更“容易”——但這很少會有好結果)。

必須(MUST)有一個預設的 CollectorRegistry,標準指標必須(MUST)預設隱式註冊到其中,無需使用者進行特殊操作。必須(MUST)提供一種方法讓指標不註冊到預設的 CollectorRegistry 中,以便在批處理作業和單元測試中使用。自定義 Collector 應當(SHOULD)也遵循這一點。

具體應該如何建立指標因語言而異。對於某些語言(Java、Go),建造者(builder)模式是最好的;而對於其他語言(Python),函式引數足夠豐富,可以在一次呼叫中完成。

例如,在 Java Simpleclient 中,我們有:

class YourClass {
  static final Counter requests = Counter.build()
      .name("requests_total")
      .help("Requests.").register();
}

這將向預設的 CollectorRegistry 註冊 requests。透過呼叫 build() 而不是 register(),指標將不會被註冊(便於單元測試),你也可以向 register() 傳遞一個 CollectorRegistry(便於批處理作業)。

Counter(計數器)

Counter 是單調遞增的計數器。它絕不(MUST NOT)允許值減少,但可以(MAY)被重置為 0(例如透過伺服器重啟)。

Counter 必須(MUST)擁有以下方法:

  • inc():將計數器增加 1
  • inc(double v):將計數器增加指定的量。必須(MUST)檢查 v >= 0。

鼓勵(ENCOURAGED)Counter 擁有:

一種用於統計在給定的程式碼塊中丟擲/引發的異常次數的方法,並且可以可選地僅限某些型別的異常。這在 Python 中是 count_exceptions。

Counter 必須(MUST)從 0 開始。

Gauge(儀表盤)

Gauge 代表一個可以上升和下降的值。

Gauge 必須(MUST)擁有以下方法:

  • inc():將儀表盤增加 1
  • inc(double v):將儀表盤增加指定的量
  • dec():將儀表盤減少 1
  • dec(double v):將儀表盤減少指定的量
  • set(double v):將儀表盤設定為指定的值

Gauge 必須(MUST)從 0 開始,你也可以(MAY)提供一種讓特定 Gauge 從其他數字開始的方法。

Gauge 應當(SHOULD)擁有以下方法:

  • set_to_current_time():將儀表盤設定為當前 Unix 時間(以秒為單位)。

鼓勵(ENCOURAGED)Gauge 擁有:

一種在某些程式碼/函式中跟蹤正在處理中的請求的方法。在 Python 中是 track_inprogress

一種對一段程式碼進行計時並將 Gauge 設定為其持續時間(以秒為單位)的方法。這對於批處理作業非常有用。在 Java 中是 startTimer/setDuration,在 Python 中是 time() 裝飾器/上下文管理器。這應當(SHOULD)與 Summary/Histogram 中的模式匹配(儘管是使用 set() 而不是 observe())。

總結

Summary 在滑動時間視窗內對觀測值(通常是請求持續時間等)進行取樣,並即時反映其分佈、頻率和總和。

Summary 絕不(MUST NOT)允許使用者將 "quantile" 設定為標籤名稱,因為該名稱在內部用於指定 Summary 分位數。鼓勵(ENCOURAGED)Summary 提供分位數作為匯出項,儘管這些分位數無法聚合且往往較慢。Summary 必須(MUST)允許不包含分位數,因為僅 _count/_sum 就已經非常有用,且這必須(MUST)作為預設設定。

Summary 必須(MUST)擁有以下方法:

  • observe(double v):觀測指定的數值

Summary 應當(SHOULD)擁有以下方法:

某種為使用者對程式碼進行計時(以秒為單位)的方法。在 Python 中是 time() 裝飾器/上下文管理器。在 Java 中是 startTimer/observeDuration。絕不(MUST NOT)提供秒以外的其他單位(如果使用者想要其他單位,他們可以手動實現)。這應當遵循與 Gauge/Histogram 相同的模式。

Summary 的 _count/_sum 必須(MUST)從 0 開始。

Histogram(直方圖)

Histogram 允許對事件進行可聚合的分佈統計,例如請求延遲。其核心是為每個分桶(bucket)設定一個計數器。

Histogram 絕不(MUST NOT)允許 le 作為使用者設定的標籤,因為 le 在內部用於指定分桶。

Histogram 必須(MUST)提供手動選擇分桶的方法。應當(SHOULD)提供以 linear(start, width, count)exponential(start, factor, count) 方式設定分桶的方法。Count 必須(MUST)包含 +Inf 分桶。

Histogram 應當(SHOULD)具有與其他客戶端庫相同的預設分桶。一旦建立了該指標,分桶絕不(MUST NOT)可更改。

Histogram 必須(MUST)具有以下方法:

  • observe(double v):觀測指定的數值

Histogram 應當(SHOULD)具有以下方法:

某種為使用者對程式碼進行計時(以秒為單位)的方法。在 Python 中是 time() 裝飾器/上下文管理器。在 Java 中是 startTimer/observeDuration。絕不(MUST NOT)提供秒以外的其他單位(如果使用者想要其他單位,他們可以手動實現)。這應該遵循與 Gauge/Summary 相同的模式。

Histogram 的 _count/_sum 以及分桶必須(MUST)從 0 開始。

關於指標的進一步考量

在符合特定語言特性的前提下,鼓勵(ENCOURAGED)在指標中提供超出上述記錄的附加功能。

如果你能讓一個常見用例變得更簡單,那就去做吧,只要它不會鼓勵不良行為(如次優的指標/標籤佈局,或者在客戶端中進行計算)。

標籤

標籤是 Prometheus 最強大的方面之一,但也是最容易被濫用的。因此,客戶端庫在如何向用戶提供標籤方面必須非常小心。

客戶端庫不應當(SHOULD NOT)允許使用者為 Gauge/Counter/Summary/Histogram 或該庫提供的任何其他 Collector 的同一個指標設定不同的標籤名稱。

自定義 Collector 的指標幾乎總是應該具有一致的標籤名稱。由於仍然存在少數但合理的、不屬於此種情況的用例,因此客戶端庫不應對此進行驗證。

儘管標籤功能強大,但大多數指標並不會帶有標籤。因此,API 應當支援標籤,但不能讓它占主導地位。

客戶端庫必須(MUST)允許在建立 Gauge/Counter/Summary/Histogram 時可選地指定標籤名稱列表。客戶端庫應當(SHOULD)支援任意數量的標籤名稱。客戶端庫必須(MUST)驗證標籤名稱是否符合文件規定的要求

訪問指標標籤維度的通用方法是透過 labels() 方法,該方法接收標籤值列表或從標籤名稱到標籤值的對映,並返回一個“子指標(Child)”。然後可以在該 Child 上呼叫常用的 .inc()/.dec()/.observe() 等方法。

labels() 返回的 Child 應當(SHOULD)對使用者而言是可快取的,以避免重複查詢——這在對延遲敏感的程式碼中非常重要。

帶有標籤的指標應當(SHOULD)支援與 labels() 具有相同簽名的 remove() 方法,該方法將從指標中移除一個 Child 且不再匯出它,以及支援一個從指標中移除所有 Child 的 clear() 方法。這些操作會使已快取的 Child 失效。

應當(SHOULD)有一種使用預設值初始化給定 Child 的方法,通常只需呼叫 labels()。不帶標籤的指標必須(MUST)始終進行初始化,以避免指標缺失的問題

指標名稱

指標名稱必須遵循規範。與標籤名稱一樣,在使用 Gauge/Counter/Summary/Histogram 以及該庫提供的任何其他 Collector 時,都必須(MUST)滿足此要求。

許多客戶端庫允許將名稱分為三部分設定:namespace_subsystem_name,其中只有 name 是必填的。

絕不鼓勵(MUST be discouraged)使用動態/生成的指標名稱或指標名稱的子部分,除非自定義 Collector 是在從其他插樁/監控系統進行代理。生成/動態的指標名稱表明你應當改用標籤。

指標描述和幫助

Gauge/Counter/Summary/Histogram 必須(MUST)要求提供指標描述/幫助資訊。

隨客戶端庫提供的任何自定義 Collector 其指標都必須(MUST)包含描述/幫助資訊。

建議將其作為必填引數,但不要檢查它是否達到了一定的長度,因為如果有人真的不想寫文件,我們也無法說服他們。隨客戶端庫提供的 Collector(以及整個生態系統中我們能做到的地方)應當(SHOULD)具有良好的指標描述,以起到模範作用。

暴露格式

客戶端必須(MUST)實現暴露格式文件中概述的基於文字的暴露格式。

如果可以在不消耗顯著資源成本的情況下實現,鼓勵(ENCOURAGED)暴露的指標具有可復現的順序(尤其是對於人類可讀的格式)。

標準和執行時 Collector

客戶端庫應當(SHOULD)儘可能提供如下文所述的標準(Standard)匯出項。

這些應當(SHOULD)作為自定義 Collector 實現,並在預設情況下注冊到預設的 CollectorRegistry。應當(SHOULD)提供一種停用它們的方法,因為在一些非常特殊的用例中它們會產生干擾。

程序指標

這些指標具有字首 process_。如果在使用特定語言或執行時獲取所需的值存在困難甚至無法獲取,客戶端庫應當選擇(SHOULD)忽略相應的指標,而不是匯出虛假的、不準確的或特殊的值(如 NaN)。所有記憶體值均以位元組(bytes)為單位,所有時間均以 Unix 時間戳/秒為單位。

指標名稱幫助字串單位
process_cpu_seconds_total累計消耗的使用者和系統 CPU 時間(秒)。
process_open_fds開啟的檔案描述符數量。檔案描述符
process_max_fds最大可開啟的檔案描述符數量。檔案描述符
process_virtual_memory_bytes虛擬記憶體大小(位元組)。位元組
process_virtual_memory_max_bytes最大可用虛擬記憶體量(位元組)。位元組
process_resident_memory_bytes常駐記憶體大小(位元組)。位元組
process_heap_bytes程序堆大小(位元組)。位元組
process_start_time_seconds從 Unix 紀元開始計算的程序啟動時間(秒)。
process_threads程序中的作業系統執行緒數。執行緒

執行時指標

此外,鼓勵(ENCOURAGED)客戶端庫根據其語言的執行時提供合理的指標(例如垃圾回收統計資訊),並帶有合適的字首,如 go_hotspot_ 等。

單元測試

客戶端庫應當(SHOULD)擁有覆蓋核心插樁庫和暴露格式的單元測試。

鼓勵(ENCOURAGED)客戶端庫提供方便使用者對其插樁程式碼的使用進行單元測試的方法。例如 Python 中的 CollectorRegistry.get_sample_value

打包和依賴

理想情況下,客戶端庫可以包含在任何應用程式中,以新增某些插樁而不會破壞應用程式。

因此,在向客戶端庫新增依賴時應保持謹慎。例如,如果你添加了一個庫,該庫使用的 Prometheus 客戶端需要庫的 x.y 版本,但應用程式在其他地方使用了 x.z 版本,這是否會對應用程式產生不利影響?

建議在可能出現這種情況時,將核心插樁部分與特定格式的橋接/指標暴露部分分離開來。例如,Java simpleclient 的 simpleclient 模組沒有任何依賴,而 simpleclient_servlet 則包含 HTTP 相關部分。

效能考量

由於客戶端庫必須是執行緒安全的,因此需要某種形式的併發控制,並且必須考慮多核機器和應用程式上的效能問題。

根據我們的經驗,效能最差的是互斥鎖(mutex)。

處理器的原子指令(atomic instructions)效能往往居中,通常是可以接受的。

避免不同 CPU 修改相同記憶體塊的方法效果最好,例如 Java simpleclient 中的 DoubleAdder。但這樣做會有記憶體開銷。

如上所述,labels() 的結果應當是可以快取的。通常支援帶有標籤的指標的併發對映(concurrent maps)速度相對較慢。針對不帶標籤的指標進行特例化處理,以避免類似於 labels() 的查詢,會有很大的幫助。

當指標被遞增/遞減/設定等操作時,應當(SHOULD)避免阻塞,因為不希望在進行抓取時阻礙整個應用程式的執行。

鼓勵(ENCOURAGED)對主要插樁操作(包括標籤)進行基準測試(benchmarks)。

在進行指標暴露時,應當將資源消耗(尤其是 RAM)牢記在心。考慮透過流式傳輸結果來減少記憶體佔用,並可能限制併發抓取的數量。

本頁內容