本篇要來繼續講述不同 prometheus client 寫入流程細節上的不同處。
總的來說這兩個函式庫分別是把用既有的 http 和 sarama Client,定時抓取目標資料後。將資料轉換成 Prometheus Client 的資料型態。等待 Prometheus Server 的抓取。
需要特別注意的是 Prometheus Server 上的樣本時間是 Prometheus Server 的時間。而不是 Exporter 向 docker 或 kafka 請求資料的時間。雖然一般來說這兩個函式庫預設的抓取週期都比較短,但還是有為了省資源而調整的可能。
Histogram 可以提供數值分布的統計資料。使用上需先切分區間。在 Prometheus Client 中,Histogram
的每個區間各有一個計數器。每次有新數值時,由 atomic.Add
操作確保數值的正確性。
當 Prometheus Server 發送 http request 到 /metrics
時,Histogram
才會將各區間的計數器值累加,產生符合 OpenMetrics 格式-由 le
標籤區分的指標值。這個累加的過程是會對 histogram
上鎖的。確保在計算過程中,不會有新的數值被加入。
與 CounterVec 相同的是,HistogramVec
也會用 mutex 確保同一時間只能產生一個 Histogram。且不同 Histogram 之間的計數器值可以同時被增加。
更甚者,同 HistogramVec
內的兩個標籤不同的 Histogram
甲和乙。有可能甲先增加後乙才增加,http response 的數值卻只反應乙的增加。時間順序:
上述誤差在 Counter 和 Gauge 上也有可能發生。比方說我們的 http client 中間件,除了增加計數器外,還記錄 elapsed_time:
// 作一個 http client 的中間件,每次向下游發送請求後增加對應的計數器
type prometheusMiddleware struct{}
func (m *prometheusMiddleware) RoundTrip (next http.RoundTripper)
return func(req *http.Request) (*http.Response, error) {
start := time.Now()
// 發送請求到下游
resp, err := next.RoundTrip(req)
elapsed := time.Since(start)
requestDuration.WithLabelValues(req.URL.Path, resp.StatusCode()).Add(elapsed)
requestCount.WithLabelValues(req.URL.Path, resp.StatusCode()).Inc()
return resp, err
}
}
這時有極微小的機率,registry 讀取 requestDuration
和 requestCount
給 promethues server 時,只有其中一個增加。
反觀以下 PromQL 查詢:
http_responses_total{code!="2xx"} / http_responses_total
http_responses_total
是很多個不同時間序列的總和。總和的計算是在 Prometheus Server 上進行的。對於一個 code!="2xx"
的樣本。它在 PromQL 計算的時候,如果有被計入分子,那麼也一定會被計入分母。
相信從這個例子中,讀者可以看出為什麼 OpenMetrics 規範中,為什麼需要 StateSet
這個資料型態了。
在舉例常見的監控指標時,我們提到了用 Histogram 來記錄 http 延遲。再由 PromQL 計算 P99 延遲。然而既然要算 P99,為什麼不用 Summary 卻要用 Histogram 呢?照理來說 Histogram 經過了區間切分,對資訊進行了壓縮,算出 P99 需要內插,並不準確。在已知要算 P99,的情況下,使用 Summary 會更準確。然而這只限於單一標的的情況。萬一我們有十臺同樣的服務,每臺服務都有一個 Summary,各自回報該機器上的 P99 延遲。當我們想要知道的是所有服務的 P99 延遲時,依舊毫無頭緒-既不能取平均,也不能加權平均。這時 Histogram 至少可以給我們一個合理的數值。
於是正確的使用 Summary 的方式,就是不要做任何的加總或平均。一開始就確認所需的標籤,查詢時也要指定所有的標籤。