為 Go 應用添加 Prometheus 監(jiān)控指標
前面我們了解了如何儀表化應用,接下來我們將學習使用 Prometheus 的 Go 客戶端庫來為一個 Go 應用程序添加和暴露監(jiān)控指標。
創(chuàng)建應用
我們首先從一個最簡單的 Go 應用程序開始,在端口 8080 的 /metrics 端點上暴露客戶端庫的默認注冊表,暫時還沒有跟蹤任何其他自定義的監(jiān)控指標。
先創(chuàng)建一個名為 instrument-demo 的目錄,在該目錄下面初始化項目:
- ☸ ➜ mkdir instrument-demo && cd instrument-demo
- ☸ ➜ go mod init github.com/cnych/instrument-demo
上面的命令會在 instrument-demo 目錄下面生成一個 go.mod 文件,在同目錄下面新建一個 main.go 的入口文件,內容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // Serve the default Prometheus metrics registry over HTTP on /metrics.
- http.Handle("/metrics", promhttp.Handler())
- http.ListenAndServe(":8080", nil)
- }
然后執(zhí)行下面的命令下載 Prometheus 客戶端庫依賴:
- ☸ ➜ export GOPROXY="https://goproxy.cn"
- ☸ ➜ go mod tidy
- go: finding module for package github.com/prometheus/client_golang/prometheus/promhttp
- go: found github.com/prometheus/client_golang/prometheus/promhttp in github.com/prometheus/client_golang v1.11.0
- go: downloading google.golang.org/protobuf v1.26.0-rc.1
然后直接執(zhí)行 go run 命令啟動服務:
- ☸ ➜ go run main.go
然后我們可以在瀏覽器中訪問 http://localhost:8080/metrics 來獲得默認的監(jiān)控指標數據:
- # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
- # TYPE go_gc_duration_seconds summary
- go_gc_duration_seconds{quantile="0"} 0
- go_gc_duration_seconds{quantile="0.25"} 0
- go_gc_duration_seconds{quantile="0.5"} 0
- go_gc_duration_seconds{quantile="0.75"} 0
- go_gc_duration_seconds{quantile="1"} 0
- go_gc_duration_seconds_sum 0
- go_gc_duration_seconds_count 0
- # HELP go_goroutines Number of goroutines that currently exist.
- # TYPE go_goroutines gauge
- go_goroutines 6
- ......
- # HELP go_threads Number of OS threads created.
- # TYPE go_threads gauge
- go_threads 8
- # HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
- # TYPE promhttp_metric_handler_requests_in_flight gauge
- promhttp_metric_handler_requests_in_flight 1
- # HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
- # TYPE promhttp_metric_handler_requests_total counter
- promhttp_metric_handler_requests_total{code="200"} 1
- promhttp_metric_handler_requests_total{code="500"} 0
- promhttp_metric_handler_requests_total{code="503"} 0
我們并沒有在代碼中添加什么業(yè)務邏輯,但是可以看到依然有一些指標數據輸出,這是因為 Go 客戶端庫默認在我們暴露的全局默認指標注冊表中注冊了一些關于 promhttp 處理器和運行時間相關的默認指標,根據不同指標名稱的前綴可以看出:
- go_*:以 go_ 為前綴的指標是關于 Go 運行時相關的指標,比如垃圾回收時間、goroutine 數量等,這些都是 Go 客戶端庫特有的,其他語言的客戶端庫可能會暴露各自語言的其他運行時指標。
- promhttp_*:來自 promhttp 工具包的相關指標,用于跟蹤對指標請求的處理。
這些默認的指標是非常有用,但是更多的時候我們需要自己控制,來暴露一些自定義指標。這就需要我們去實現自定義的指標了。
添加自定義指標
接下來我們來自定義一個的 gauge 指標來暴露當前的溫度。創(chuàng)建一個新的文件 custom-metric/main.go,內容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建一個沒有任何 label 標簽的 gauge 指標
- temp := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- })
- // 在默認的注冊表中注冊該指標
- prometheus.MustRegister(temp)
- // 設置 gauge 的值為 39
- temp.Set(39)
- // 暴露指標
- http.Handle("/metrics", promhttp.Handler())
- http.ListenAndServe(":8080", nil)
- }
上面文件中和最初的文件就有一些變化了:
- 我們使用 prometheus.NewGauge() 函數創(chuàng)建了一個自定義的 gauge 指標對象,指標名稱為 home_temperature_celsius,并添加了一個注釋信息。
- 然后使用 prometheus.MustRegister() 函數在默認的注冊表中注冊了這個 gauge 指標對象。
- 通過調用 Set() 方法將 gauge 指標的值設置為 39。
- 然后像之前一樣通過 HTTP 暴露默認的注冊表。
需要注意的是除了 prometheus.MustRegister() 函數之外還有一個 prometheus.Register() 函數,一般在 golang 中我們會將 Mustxxx 開頭的函數定義為必須滿足條件的函數,如果不滿足會返回一個 panic 而不是一個 error 操作,所以如果這里不能正常注冊的話會拋出一個 panic。
現在我們來運行這個程序:
- ☸ ➜ go run ./custom-metric
啟動后重新訪問指標接口 http://localhost:8080/metrics,仔細對比我們會發(fā)現多了一個名為 home_temperature_celsius 的指標:
- ...
- # HELP home_temperature_celsius The current temperature in degrees Celsius.
- # TYPE home_temperature_celsius gauge
- home_temperature_celsius 42
- ...
這樣我們就實現了添加一個自定義的指標的操作,整體比較簡單,當然在實際的項目中需要結合業(yè)務來確定添加哪些自定義指標。
自定義注冊表
前面我們是使用 prometheus.MustRegister() 函數來將指標注冊到全局默認注冊中,此外我們還可以使用 prometheus.NewRegistry() 函數來創(chuàng)建和使用自己的非全局的注冊表。
既然有全局的默認注冊表,為什么我們還需要自定義注冊表呢?這主要是因為:
- 全局變量通常不利于維護和測試,創(chuàng)建一個非全局的注冊表,并明確地將其傳遞給程序中需要注冊指標的地方,這也一種更加推薦的做法。
- 全局默認注冊表包括一組默認的指標,我們有時候可能希望除了自定義的指標之外,不希望暴露其他的指標。
下面的示例程序演示了如何創(chuàng)建、使用和暴露一個非全局注冊表對象,創(chuàng)建一個文件 custom-registry/main.go,內容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建一個自定義的注冊表
- registry := prometheus.NewRegistry()
- // 可選: 添加 process 和 Go 運行時指標到我們自定義的注冊表中
- registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
- registry.MustRegister(prometheus.NewGoCollector())
- // 創(chuàng)建一個簡單呃 gauge 指標。
- temp := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- })
- // 使用我們自定義的注冊表注冊 gauge
- registry.MustRegister(temp)
- // 設置 gague 的值為 39
- temp.Set(39)
- // 暴露自定義指標
- http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{Registry: registry}))
- http.ListenAndServe(":8080", nil)
- }
上面我們沒有使用全局默認的注冊表了,而是創(chuàng)建的一個自定義的注冊表:
- 首先使用 prometheus.NewRegistry() 函數創(chuàng)建我們自己的注冊表對象。
- 然后使用自定義注冊表對象上面的 MustRegister() 哈是來注冊 guage 指標,而不是調用 prometheus.MustRegister() 函數(這會使用全局默認的注冊表)。
- 如果我們希望在自定義注冊表中也有進程和 Go 運行時相關的指標,我們可以通過實例化 Collector 收集器來添加他們。
- 最后在暴露指標的時候必須通過調用 promhttp.HandleFor() 函數來創(chuàng)建一個專門針對我們自定義注冊表的 HTTP 處理器,為了同時暴露前面示例中的 promhttp_* 相關的指標,我們還需要在 promhttp.HandlerOpts 配置對象的 Registry 字段中傳遞我們的注冊表對象。
同樣我們重新運行上面的自定義注冊表程序:
- ☸ ➜ go run ./custom-metric
啟動后再次訪問指標接口 http://localhost:8080/metrics,可以發(fā)現和上面示例中的指標數據是相同的。
指標定制
Gauges
前面的示例我們已經了解了如何添加 gauge 類型的指標,創(chuàng)建了一個沒有任何標簽的指標,直接使用 prometheus.NewGauge() 函數即可實例化一個 gauge 類型的指標對象,通過 prometheus.GaugeOpts 對象可以指定指標的名稱和注釋信息:
- queueLength := prometheus.NewGauge(prometheus.GaugeOpts{
- Name: "queue_length",
- Help: "The number of items in the queue.",
- })
我們知道 gauge 類型的指標值是可以上升或下降的,所以我們可以為 gauge 指標設置一個指定的值,所以 gauge 指標對象暴露了 Set()、Inc()、Dec()、Add() 和 Sub() 這些函數來更改指標值:
- // 使用 Set() 設置指定的值
- queueLength.Set(0)
- // 增加或減少
- queueLength.Inc() // +1:Increment the gauge by 1.
- queueLength.Dec() // -1:Decrement the gauge by 1.
- queueLength.Add(23) // Increment by 23.
- queueLength.Sub(42) // Decrement by 42.
另外 gauge 儀表盤經常被用來暴露 Unix 的時間戳樣本值,所以也有一個方便的方法來將 gauge 設置為當前的時間戳:
- demoTimestamp.SetToCurrentTime()
最終 gauge 指標會被渲染成如下所示的數據:
- # HELP queue_length The number of items in the queue.
- # TYPE queue_length gauge
- queue_length 42
Counters
要創(chuàng)建一個 counter 類型的指標和 gauge 比較類似,只是用 prometheus.NewCounter() 函數來初始化指標對象:
- totalRequests := prometheus.NewCounter(prometheus.CounterOpts{
- Name: "http_requests_total",
- Help: "The total number of handled HTTP requests.",
- })
我們知道 counter 指標只能隨著時間的推移而不斷增加,所以我們不能為其設置一個指定的值或者減少指標值,所以該對象下面只有 Inc() 和 Add() 兩個函數:
- totalRequests.Inc() // +1:Increment the counter by 1.
- totalRequests.Add(23) // +n:Increment the counter by 23.
當服務進程重新啟動的時候,counter 指標值會被重置為 0,不過不用擔心數據錯亂,我們一般會使用的 rate() 函數會自動處理。
最終 counter 指標會被渲染成如下所示的數據:
- # HELP http_requests_total The total number of handled HTTP requests.
- # TYPE http_requests_total counter
- http_requests_total 7734
Histograms
創(chuàng)建直方圖指標比 counter 和 gauge 都要復雜,因為需要配置把觀測值歸入的 bucket 的數量,以及每個 bucket 的上邊界。Prometheus 中的直方圖是累積的,所以每一個后續(xù)的 bucket 都包含前一個 bucket 的觀察計數,所有 bucket 的下限都從 0 開始的,所以我們不需要明確配置每個 bucket 的下限,只需要配置上限即可。
同樣要創(chuàng)建直方圖指標對象,我們使用 prometheus.NewHistogram() 函數來進行初始化:
- requestDurations := prometheus.NewHistogram(prometheus.HistogramOpts{
- Name: "http_request_duration_seconds",
- Help: "A histogram of the HTTP request durations in seconds.",
- // Bucket 配置:第一個 bucket 包括所有在 0.05s 內完成的請求,最后一個包括所有在10s內完成的請求。
- Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
- })
這里和前面不一樣的地方在于除了指定指標名稱和幫助信息之外,還需要配置 Buckets。如果我們手動去枚舉所有的 bucket 可能很繁瑣,所以 Go 客戶端庫為為我們提供了一些輔助函數可以幫助我們生成線性或者指數增長的 bucket,比如 prometheus.LinearBuckets() 和 prometheus.ExponentialBuckets() 函數。
直方圖會自動對數值的分布進行分類和計數,所以它只有一個 Observe() 方法,每當你在代碼中處理要跟蹤的數據時,就會調用這個方法。例如,如果你剛剛處理了一個 HTTP 請求,花了 0.42 秒,則可以使用下面的代碼來跟蹤。
- requestDurations.Observe(0.42)
由于跟蹤持續(xù)時間是直方圖的一個常見用例,Go 客戶端庫就提供了輔助函數,用于對代碼的某些部分進行計時,然后自動觀察所產生的持續(xù)時間,將其轉化為直方圖,如下代碼所示:
- // 啟動一個計時器
- timer := prometheus.NewTimer(requestDurations)
- // [...在應用中處理請求...]
- // 停止計時器并觀察其持續(xù)時間,將其放進 requestDurations 的直方圖指標中去
- timer.ObserveDuration()
直方圖指標最終會生成如下所示的數據:
- # HELP http_request_duration_seconds A histogram of the HTTP request durations in seconds.
- # TYPE http_request_duration_seconds histogram
- http_request_duration_seconds_bucket{le="0.05"} 4599
- http_request_duration_seconds_bucket{le="0.1"} 24128
- http_request_duration_seconds_bucket{le="0.25"} 45311
- http_request_duration_seconds_bucket{le="0.5"} 59983
- http_request_duration_seconds_bucket{le="1"} 60345
- http_request_duration_seconds_bucket{le="2.5"} 114003
- http_request_duration_seconds_bucket{le="5"} 201325
- http_request_duration_seconds_bucket{le="+Inf"} 227420
- http_request_duration_seconds_sum 88364.234
- http_request_duration_seconds_count 227420
每個配置的存儲桶最終作為一個帶有 _bucket 后綴的計數器時間序列,使用 le(小于或等于) 標簽指示該存儲桶的上限,具有上限的隱式存儲桶 +Inf 也暴露于比最大配置的存儲桶邊界花費更長的時間的請求,還包括使用后綴 _sum 累積總和和計數 _count 的指標,這些時間序列中的每一個在概念上都是一個 counter 計數器(只能上升的單個值),只是它們是作為直方圖的一部分創(chuàng)建的。
Summaries
創(chuàng)建和使用摘要與直方圖非常類似,只是我們需要指定要跟蹤的 quantiles 分位數值,而不需要處理 bucket 桶,比如我們想要跟蹤 HTTP 請求延遲的第 50、90 和 99 個百分位數,那么我們可以創(chuàng)建這樣的一個摘要對象:
- requestDurations := prometheus.NewSummary(prometheus.SummaryOpts{
- Name: "http_request_duration_seconds",
- Help: "A summary of the HTTP request durations in seconds.",
- Objectives: map[float64]float64{
- 0.5: 0.05, // 第50個百分位數,最大絕對誤差為0.05。
- 0.9: 0.01, // 第90個百分位數,最大絕對誤差為0.01。
- 0.99: 0.001, // 第90個百分位數,最大絕對誤差為0.001。
- },
- },
- )
這里和前面不一樣的地方在于使用 prometheus.NewSummary() 函數初始化摘要指標對象的時候,需要通過 prometheus.SummaryOpts{} 對象的 Objectives 屬性指定想要跟蹤的分位數值。
同樣摘要指標對象創(chuàng)建后,跟蹤持續(xù)時間的方式和直方圖是完全一樣的,使用一個 Observe() 函數即可:
- requestDurations.Observe(0.42)
雖然直方圖桶可以跨維度匯總(如端點、HTTP 方法等),但這對于匯總 quantiles 分位數值來說在統(tǒng)計學上是無效的。例如,你不能對兩個單獨的服務實例的第 90 百分位延遲進行平均,并期望得到一個有效的整體第 90 百分位延遲。如果需要按維度進行匯總,那么我們需要使用直方圖而不是摘要指標。
摘要指標最終生成的指標數據與直方圖非常類似,不同之處在于使用 quantile 標簽來表示分位數序列,并且這些序列沒有擴展指標名稱的后綴:
- # HELP http_request_duration_seconds A summary of the HTTP request durations in seconds.
- # TYPE http_request_duration_seconds summary
- http_request_duration_seconds{quantile="0.5"} 0.052
- http_request_duration_seconds{quantile="0.90"} 0.564
- http_request_duration_seconds{quantile="0.99"} 2.372
- http_request_duration_seconds_sum 88364.234
- http_request_duration_seconds_count 227420
標簽
到目前為止,我們還沒有為指標對象添加任何的標簽,要創(chuàng)建具有標簽維度的指標,我們可以調用類似于 NewXXXVec() 的構造函數來初始化指標對象:
- NewGauge() 變成 NewGaugeVec()
- NewCounter() 變成 NewCounterVec()
- NewSummary() 變成 NewSummaryVec()
- NewHistogram() 變成 NewHistogramVec()
這些函數允許我們指定一個額外的字符串切片參數,提供標簽名稱的列表,通過它來拆分指標。
例如,為了按照房子以及測量溫度的房間來劃分我們早期的溫度表指標,可以這樣創(chuàng)建指標。
- temp := prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- },
- // 兩個標簽名稱,通過它們來分割指標。
- []string{"house", "room"},
- )
然后要訪問一個特有標簽的子指標,需要在設置其值之前,用 house 和 room 標簽的各自數值,對產生的 gauge 向量調用 WithLabelValues() 方法來處理下:
- // 為 home=ydzs 和 room=living-room 設置指標值
- temp.WithLabelValues("ydzs", "living-room").Set(27)
如果你喜歡在選擇的子指標中明確提供標簽名稱,可以使用效率稍低的 With() 方法來代替:
- temp.With(prometheus.Labels{"house": "ydzs", "room": "living-room"}).Set(66)
不過需要注意如果向這兩個方法傳遞不正確的標簽數量或不正確的標簽名稱,這兩個方法都會觸發(fā) panic。
下面是我們按照 house 和 room 標簽維度區(qū)分指標的完整示例,創(chuàng)建一個名為 label-metric/main.go 的新文件,內容如下所示:
- package main
- import (
- "net/http"
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promhttp"
- )
- func main() {
- // 創(chuàng)建帶 house 和 room 標簽的 gauge 指標對象
- temp := prometheus.NewGaugeVec(
- prometheus.GaugeOpts{
- Name: "home_temperature_celsius",
- Help: "The current temperature in degrees Celsius.",
- },
- // 指定標簽名稱
- []string{"house", "room"},
- )
- // 注冊到全局默認注冊表中
- prometheus.MustRegister(temp)
- // 針對不同標簽值設置不同的指標值
- temp.WithLabelValues("cnych", "living-room").Set(27)
- temp.WithLabelValues("cnych", "bedroom").Set(25.3)
- temp.WithLabelValues("ydzs", "living-room").Set(24.5)
- temp.WithLabelValues("ydzs", "bedroom").Set(27.7)
- // 暴露自定義的指標
- http.Handle("/metrics", promhttp.Handler())
- http.ListenAndServe(":8080", nil)
- }
上面代碼非常清晰了,運行下面的程序:
- ☸ ➜ go run ./label-metric
啟動完成后重新訪問指標端點 http://localhost:8080/metrics,可以找到 home_temperature_celsius 指標不同標簽維度下面的指標值:
- ...
- # HELP home_temperature_celsius The current temperature in degrees Celsius.
- # TYPE home_temperature_celsius gauge
- home_temperature_celsius{house="cnych",room="bedroom"} 25.3
- home_temperature_celsius{house="cnych",room="living-room"} 27
- home_temperature_celsius{house="ydzs",room="bedroom"} 27.7
- home_temperature_celsius{house="ydzs",room="living-room"} 24.5
- ...
注意:當使用帶有標簽維度的指標時,任何標簽組合的時間序列只有在該標簽組合被訪問過至少一次后才會出現在 /metrics 輸出中,這對我們在 PromQL 查詢的時候會產生一些問題,因為它希望某些時間序列一直存在,我們可以在程序第一次啟動時,將所有重要的標簽組合預先初始化為默認值。
同樣的方式在其他幾個指標類型中使用標簽的方法與上面的方式一致。