k8s教程說明
- k8s底層原理和原始碼講解之精華篇
- k8s底層原理和原始碼講解之進階篇
- k8s純原始碼解讀課程,助力你變成k8s專家
- k8s-operator和crd實戰開發 助你成為k8s專家
- tekton全流水線實戰和pipeline執行原理原始碼解讀
prometheus全元件的教程
- 01_prometheus全元件配置使用、底層原理解析、高可用實戰
- 02_prometheus-thanos使用和原始碼解讀
- 03_kube-prometheus和prometheus-operator實戰和原理介紹
- 04_prometheus原始碼講解和二次開發
go語言課程
問題描述
- 比如對於1個構建的流水線指標 pipeline_step_duration ,會設定1個標籤是step
每次流水線包含的step可能不相同
# 比如 流水線a 第1次的step 包含clone 和build pipeline_step_duration{step="clone"} pipeline_step_duration{step="build"} # 第2次 的step 包含 build 和push pipeline_step_duration{step="build"} pipeline_step_duration{step="push"}
- 那麼問題來了:第2次的pipeline_step_duration{step="build"} 要不要刪掉?
- 其實在這個場景裡面是要刪掉的,因為已經不包含clone了
問題可以總結成:之前採集的標籤已經不存在了,資料要及時清理掉 --問題是如何清理?
討論這個問題前做個實驗:對比兩種常見的自打點方式對於不活躍指標的刪除處理
實驗手段:prometheus client-go sdk
- 啟動1個rand_metrics
包含rand_key,每次key都不一樣,測試請求metrics介面的結果
var ( T1 = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "rand_metrics", Help: "rand_metrics", }, []string{"rand_key"}) )
實現方式01 業務程式碼中直接實現打點:不實現Collector介面
程式碼如下,模擬極端情況,每0.1秒生成隨機key 和value設定metrics
package main import ( "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "math/rand" "net/http" "time" ) var ( T1 = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "rand_metrics", Help: "rand_metrics", }, []string{"rand_key"}) ) func init() { prometheus.DefaultRegisterer.MustRegister(T1) } func RandStr(length int) string { str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" bytes := []byte(str) result := []byte{} rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100))) for i := 0; i < length; i++ { result = append(result, bytes[rand.Intn(len(bytes))]) } return string(result) } func push() { for { randKey := RandStr(10) rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100))) T1.With(prometheus.Labels{"rand_key": randKey}).Set(rand.Float64()) time.Sleep(100 * time.Millisecond) } } func main() { go push() addr := ":8081" http.Handle("/metrics", promhttp.Handler()) srv := http.Server{Addr: addr} err := srv.ListenAndServe() fmt.Println(err) }
啟動服務之後請求 :8081/metrics介面發現 過期的 rand_key還會保留,不會清理
# HELP rand_metrics rand_metrics # TYPE rand_metrics gauge rand_metrics{rand_key="00DsYGkd6x"} 0.02229735291486387 rand_metrics{rand_key="017UBn8S2T"} 0.7192676436571013 rand_metrics{rand_key="01Ar4ca3i1"} 0.24131184816722678 rand_metrics{rand_key="02Ay5kqsDH"} 0.11462075954697458 rand_metrics{rand_key="02JZNZvMng"} 0.9874169937518104 rand_metrics{rand_key="02arsU5qNT"} 0.8552103362564516 rand_metrics{rand_key="02nMy3thfh"} 0.039571420204118024 rand_metrics{rand_key="032cyHjRhP"} 0.14576779289125183 rand_metrics{rand_key="03DPDckbfs"} 0.6106184905871918 rand_metrics{rand_key="03lbtLwFUO"} 0.936911945555629 rand_metrics{rand_key="03wqYiguP2"} 0.20167059771916385 rand_metrics{rand_key="04uG2s3X0C"} 0.3324314184499403
實現方式02 實現Collector介面
- 實現prometheus sdk中的collect 介面 :也就是給1個結構體 繫結Collect和Describe方法
- 在Collect中 實現設定標籤和賦值方法
在Describe中 傳入desc
package main import ( "fmt" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "log" "math/rand" "net/http" "time" ) var ( T1 = prometheus.NewDesc( "rand_metrics", "rand_metrics", []string{"rand_key"}, nil) ) type MyCollector struct { Name string } func (mc *MyCollector) Collect(ch chan<- prometheus.Metric) { log.Printf("MyCollector.collect.called") ch <- prometheus.MustNewConstMetric(T1, prometheus.GaugeValue, rand.Float64(), RandStr(10)) } func (mc *MyCollector) Describe(ch chan<- *prometheus.Desc) { log.Printf("MyCollector.Describe.called") ch <- T1 } func RandStr(length int) string { str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" bytes := []byte(str) result := []byte{} rand.Seed(time.Now().UnixNano() + int64(rand.Intn(100))) for i := 0; i < length; i++ { result = append(result, bytes[rand.Intn(len(bytes))]) } return string(result) } func main() { //go push() mc := &MyCollector{Name: "abc"} prometheus.MustRegister(mc) addr := ":8082" http.Handle("/metrics", promhttp.Handler()) srv := http.Server{Addr: addr} err := srv.ListenAndServe() fmt.Println(err) }
metrics效果測試 :請求:8082/metrics介面發現 rand_metrics總是隻有1個值
# HELP rand_metrics rand_metrics # TYPE rand_metrics gauge rand_metrics{rand_key="e1JU185kE4"} 0.12268247569586412
並且檢視日誌發現,每次我們請求/metrics介面時 MyCollector.collect.called會呼叫
2022/06/21 11:46:40 MyCollector.Describe.called 2022/06/21 11:46:44 MyCollector.collect.called 2022/06/21 11:46:47 MyCollector.collect.called 2022/06/21 11:46:47 MyCollector.collect.called 2022/06/21 11:46:47 MyCollector.collect.called 2022/06/21 11:46:47 MyCollector.collect.called
現象總結
- 實現Collector介面的方式 能滿足過期指標清理的需求,並且打點函式是伴隨/metrics介面請求觸發的
- 不實現Collector介面的方式 不能滿足過期指標清理的需求,指標會隨著業務打點堆積
原始碼解讀相關原因
01 兩種方式都是從web請求獲取的指標,所以得先從 /metrics介面看
- 入口就是 http.Handle("/metrics", promhttp.Handler())
- 追蹤後發現是 D:\go_path\pkg\mod\github.com\prometheus\client_golang@v1.12.2\prometheus\promhttp\http.go
主要邏輯為:
- 呼叫reg的Gather方法 獲取 MetricFamily陣列
- 然後編碼,寫到http的resp中
虛擬碼如下
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler { mfs, err := reg.Gather() for _, mf := range mfs { if handleError(enc.Encode(mf)) { return } } }
reg.Gather :遍歷reg中已註冊的collector 呼叫他們的collect方法
先呼叫他們的collect方法獲取metrics結果
collectWorker := func() { for { select { case collector := <-checkedCollectors: collector.Collect(checkedMetricChan) case collector := <-uncheckedCollectors: collector.Collect(uncheckedMetricChan) default: return } wg.Done() } }
然後消費chan中的資料,處理metrics
cmc := checkedMetricChan umc := uncheckedMetricChan for { select { case metric, ok := <-cmc: if !ok { cmc = nil break } errs.Append(processMetric( metric, metricFamiliesByName, metricHashes, registeredDescIDs, )) case metric, ok := <-umc: if !ok { umc = nil break } errs.Append(processMetric( metric, metricFamiliesByName, metricHashes, nil, ))
processMetric處理方法一致,所以方式12的不同就在 collect方法
02 不實現Collector介面的方式的collect方法追蹤
- 因為我們往reg中註冊的是 prometheus.NewGaugeVec生成的*GaugeVec指標
- 所以執行的是*GaugeVec的collect方法
而GaugeVec 又繼承了MetricVec
type GaugeVec struct { *MetricVec }
而MetricVec中有個metricMap物件, 所以最終是metricMap的collect方法
type MetricVec struct { *metricMap curry []curriedLabelValue // hashAdd and hashAddByte can be replaced for testing collision handling. hashAdd func(h uint64, s string) uint64 hashAddByte func(h uint64, b byte) uint64 }
觀察metricMap結構體和方法
- metricMap有個metrics的map
而它的Collect方法就是遍歷這個map內層的所有metricWithLabelValues介面,塞入ch中處理
// metricVecs. type metricMap struct { mtx sync.RWMutex // Protects metrics. metrics map[uint64][]metricWithLabelValues desc *Desc newMetric func(labelValues ...string) Metric } // Describe implements Collector. It will send exactly one Desc to the provided // channel. func (m *metricMap) Describe(ch chan<- *Desc) { ch <- m.desc } // Collect implements Collector. func (m *metricMap) Collect(ch chan<- Metric) { m.mtx.RLock() defer m.mtx.RUnlock() for _, metrics := range m.metrics { for _, metric := range metrics { ch <- metric.metric } } }
- 看到這裡就很清晰了,只要metrics map中的元素不被顯示的刪除,那麼資料就會一直存在
- 有一些exporter是採用這種顯式刪除的流派的,比如event_expoter
03 實現Collector介面的方式的collect方法追蹤
- 因為我們的collector 實現了collect方法
所以直接請求Gather會呼叫我們的collect方法 獲取結果
func (mc *MyCollector) Collect(ch chan<- prometheus.Metric) { log.Printf("MyCollector.collect.called") ch <- prometheus.MustNewConstMetric(T1, prometheus.GaugeValue, rand.Float64(), RandStr(10)) }
- 所以它不會往metricsMap中寫入,所以只有1個值
總結
- 兩種打點方式的collect方法是不一樣的
其實主流的exporter的效果也是不活躍的指標會刪掉:
- 比如 process-exporter監控程式,程式不存在指標曲線就會消失:從grafana圖上看就是斷點:不然採集一次會一直存在
- 比如 node-exporter 監控掛載點等,當掛載點消失相關曲線也會消失
- 因為主流的exporter採用都是 實現collect方法的方式:
- 還有k8s中kube-state-metrics採用的是 metrics-store作為informer的store 去watch etcd的delete 事件: pod刪除的時候相關的曲線也會消失
- 或者可以顯示的呼叫delete 方法,將過期的series從map中刪掉,不過需要hold中上一次的和這一次的diff
- 總之兩個流派:map顯式刪除VS實現collector介面