<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
現網上執行著一個自己開發的 metrics exporter,它是專門來捕獲後端資源的執行狀態,並生成對應的 prometheus metrics 供監控報警系統使用。當然這個 exporter 只是負責遠端監控資源,並不能實際控制後端的資源,也不能實時動態獲得被監控的資源的變動事件。當我們的運維小夥伴手動錯誤刪除後端被監控的資源,導致業務流量異常。此時也沒有報警出來,而這個報警卻是依賴這個 metrics exporter 所採集的資料,導致了一次小型事件。因為這個事件,才有今天寫文章的動力,同時也分享下解決這個問題的方法。
通過跟小夥伴們一起復盤,以及追查可能出現問題的位置後,大家都覺得沒有任何問題。在運維刪除對應的監控資源後,同時沒有關閉報警規則的情況下,應該有大量的任何異常報警產生。但實際情況,沒有任何報警發出來。
當大家一籌莫展的時候,我突然說了一句,會不會是資料採集出現了問題?大家眼前一亮,趕緊拿出 metrics exporter 的程式碼檢查。通過反覆檢查,也沒有發現可疑的地方,於是大家又開始了思考。這時我開啟了 metrics exporter 偵錯模式,打上斷點,然後請運維小夥伴刪除一個測試資源,觀察監控資料的變化。果不其然,資源刪除了,對應監控的 metrics 條目的值沒有變化(也就是說,還是上次資源的狀態)。
這下破案了,搞了半天是因為 metrics 條目內容沒有跟隨資源的刪除而被自動的刪除。導致了報警系統一直認為被刪除的資源還在執行,而且狀態正常。
既然知道了原因,再回過頭看 metrics exporter 的程式碼,程式碼中有 prometheus.MustRegister、prometheus.Unregister 和相關的 MetricsVec 值變更的實現和呼叫。就是沒有判斷監控資源在下線或者刪除的情況下,如何刪除和清理建立出來的 MetricsVec。
在我的印象中 MetricsVec 會根據 labels 會自動建立相關的條目,從來沒有手動的新增和建立。根據這個邏輯我也認為,MetricsVec 中如果 labels 對應的值不更新或者處於不活躍的狀態,應該自動刪除才是。
最後還是把 golang 的 github.com/prometheus/client_golang 這個庫想太完美了。沒有花時間對 github.com/prometheus/client_golang 內部結構、原理、處理機制充分理解,才導致這個事件的發生。
github.com/prometheus/client_golang 中的 metrics 主要是 4 個種類,這個可以 baidu 上搜尋,很多介紹,我這裡不詳細展開。這些種類的 metrics 又可以分為:一次性使用和多次使用。
注意這兩者的區別,他們有不同的應用場景。
這次專案中寫的 metrics exporter 本應該是採用 “一次性使用” 這樣的模型來開發,但是內部結構模型採用了 “多次性使用” 模型,因為指標資料寫入者和資料讀取者之間沒有必然聯絡,不屬於一個對談系統,所以之間是非同步結構。具體我們看下圖:
從圖中有 2 個身份說明下:
在此次專案中 metrics 條目是用 prometheus.GaugeVec 作為採集資料計算後結果的儲存型別。
說了這麼多,想要分析真正的原因,就必須深入 github.com/prometheus/client_golang 程式碼中 GaugeVec 這個具體程式碼實現。
// GaugeVec is a Collector that bundles a set of Gauges that all share the same // Desc, but have different values for their variable labels. This is used if // you want to count the same thing partitioned by various dimensions // (e.g. number of operations queued, partitioned by user and operation // type). Create instances with NewGaugeVec. type GaugeVec struct { *MetricVec } 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 is a helper for metricVec and shared between differently curried // metricVecs. type metricMap struct { mtx sync.RWMutex // Protects metrics. metrics map[uint64][]metricWithLabelValues // 真正的資料儲存位置 desc *Desc newMetric func(labelValues ...string) Metric }
通過上面的程式碼,一條 metric 條目是儲存在 metricMap.metrics 下。 我們繼續往下看:
讀取資料
// Collect implements Collector. func (m *metricMap) Collect(ch chan<- Metric) { m.mtx.RLock() defer m.mtx.RUnlock() // 遍歷 map for _, metrics := range m.metrics { for _, metric := range metrics { ch <- metric.metric // 讀取資料到通道 } } }
寫入資料
// To create Gauge instances, use NewGauge. type Gauge interface { Metric Collector // Set sets the Gauge to an arbitrary value. Set(float64) // Inc increments the Gauge by 1. Use Add to increment it by arbitrary // values. Inc() // Dec decrements the Gauge by 1. Use Sub to decrement it by arbitrary // values. Dec() // Add adds the given value to the Gauge. (The value can be negative, // resulting in a decrease of the Gauge.) Add(float64) // Sub subtracts the given value from the Gauge. (The value can be // negative, resulting in an increase of the Gauge.) Sub(float64) // SetToCurrentTime sets the Gauge to the current Unix time in seconds. SetToCurrentTime() } func NewGauge(opts GaugeOpts) Gauge { desc := NewDesc( BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, nil, opts.ConstLabels, ) result := &gauge{desc: desc, labelPairs: desc.constLabelPairs} result.init(result) // Init self-collection. return result } type gauge struct { // valBits contains the bits of the represented float64 value. It has // to go first in the struct to guarantee alignment for atomic // operations. http://golang.org/pkg/sync/atomic/#pkg-note-BUG valBits uint64 selfCollector desc *Desc labelPairs []*dto.LabelPair } func (g *gauge) Set(val float64) { atomic.StoreUint64(&g.valBits, math.Float64bits(val)) // 寫入資料到變數 }
看到上面的程式碼,有的小夥伴就會說讀取和寫入的位置不一樣啊,沒有找到真正的位置。不要著急,後面還有。
// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value // or creates it and returns the new one. // // This function holds the mutex. func (m *metricMap) getOrCreateMetricWithLabelValues(hash uint64, lvs []string, curry []curriedLabelValue,) Metric { // 返回了一個介面 m.mtx.RLock() metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs, curry) m.mtx.RUnlock() if ok { return metric } m.mtx.Lock() defer m.mtx.Unlock() metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs, curry) if !ok { inlinedLVs := inlineLabelValues(lvs, curry) metric = m.newMetric(inlinedLVs...) m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric}) // 這裡寫入 metricMap.metrics } return metric } // A Metric models a single sample value with its meta data being exported to // Prometheus. Implementations of Metric in this package are Gauge, Counter, // Histogram, Summary, and Untyped. type Metric interface { // 哦哦哦哦,是介面啊。Gauge 實現這個介面 // Desc returns the descriptor for the Metric. This method idempotently // returns the same descriptor throughout the lifetime of the // Metric. The returned descriptor is immutable by contract. A Metric // unable to describe itself must return an invalid descriptor (created // with NewInvalidDesc). Desc() *Desc // Write encodes the Metric into a "Metric" Protocol Buffer data // transmission object. // // Metric implementations must observe concurrency safety as reads of // this metric may occur at any time, and any blocking occurs at the // expense of total performance of rendering all registered // metrics. Ideally, Metric implementations should support concurrent // readers. // // While populating dto.Metric, it is the responsibility of the // implementation to ensure validity of the Metric protobuf (like valid // UTF-8 strings or syntactically valid metric and label names). It is // recommended to sort labels lexicographically. Callers of Write should // still make sure of sorting if they depend on it. Write(*dto.Metric) error // TODO(beorn7): The original rationale of passing in a pre-allocated // dto.Metric protobuf to save allocations has disappeared. The // signature of this method should be changed to "Write() (*dto.Metric, // error)". }
看到這裡就知道了寫入、儲存、讀取已經連線到了一起。 同時如果沒有顯式的呼叫方法刪除 metricMap.metrics 的內容,那麼記錄的 metrics 條目的值就會一直存在,而原生程式碼中只是建立和變更內部值。正是因為這個邏輯才導致上面說的事情。
既然找到原因,也找到對應的程式碼以及對應的內部邏輯,就清楚了 prometheus.GaugeVec 這個變數真正的使用方法。到此解決方案也就有了,找到合適的位置新增程式碼,顯式呼叫 DeleteLabelValues 這個方法來刪除無效 metrics 條目。
為了最後實現整體效果,我總結下有幾個關鍵詞:“非同步”、“多次性使用”、“自動回收”。
最後的改造思路:
通過這個動作就可以實現自動回收和清理無效的 metrics 條目,最後驗證下來確實有效。
通過測試程式碼來驗證這個方案的效果,具體如下演示:
package main import ( "context" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "net/http" "strconv" "sync" "time" ) type metricsMetaData struct { UpdatedAt int64 Labels []string } func main() { var wg sync.WaitGroup var status sync.Map vec := prometheus.NewGaugeVec( prometheus.GaugeOpts{ Namespace: "app", Name: "running_status", }, []string{"id"}, ) prometheus.MustRegister(vec) defer prometheus.Unregister(vec) // 寫入資料 for i := 0; i < 10; i++ { labels := strconv.Itoa(i) vec.WithLabelValues(labels).Set(1) // 寫入 metric 條目 status.Store(labels, metricsMetaData{UpdatedAt: time.Now().Unix(), Labels: []string{labels}}) // 寫入狀態 } // 建立退出 ctx stopCtx, stopCancel := context.WithCancel(context.Background()) // 啟動清理器 go func(ctx *context.Context, g *sync.WaitGroup) { defer g.Done() ticker := time.NewTicker(time.Second * 2) for { select { case <-ticker.C: now := time.Now().Unix() status.Range(func(key, value interface{}) bool { if now-value.(metricsMetaData).UpdatedAt > 5 { vec.DeleteLabelValues(value.(metricsMetaData).Labels...) // 刪除 metrics 條目 status.Delete(key) // 刪除 map 中的記錄 } return true }) break case <-(*ctx).Done(): return } } }(&stopCtx, &wg) wg.Add(1) // 建立 http http.Handle("/metrics", promhttp.Handler()) srv := http.Server{Addr: "0.0.0.0:8080"} // 啟動 http server go func(srv *http.Server, g *sync.WaitGroup) { defer g.Done() _ = srv.ListenAndServe() }(&srv, &wg) wg.Add(1) // 退出 time.Sleep(time.Second * 10) stopCancel() _ = srv.Shutdown(context.Background()) wg.Wait() }
結果動畫:
以上就是Go prometheus metrics條目自動回收與清理方法的詳細內容,更多關於Go prometheus metrics回收清理的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45