Go prometheus metrics條目自動(dòng)回收與清理方法
事件背景
現(xiàn)網(wǎng)上運(yùn)行著一個(gè)自己開(kāi)發(fā)的 metrics exporter,它是專(zhuān)門(mén)來(lái)捕獲后端資源的運(yùn)行狀態(tài),并生成對(duì)應(yīng)的 prometheus metrics 供監(jiān)控報(bào)警系統(tǒng)使用。當(dāng)然這個(gè) exporter 只是負(fù)責(zé)遠(yuǎn)程監(jiān)控資源,并不能實(shí)際控制后端的資源,也不能實(shí)時(shí)動(dòng)態(tài)獲得被監(jiān)控的資源的變動(dòng)事件。當(dāng)我們的運(yùn)維小伙伴手動(dòng)錯(cuò)誤刪除后端被監(jiān)控的資源,導(dǎo)致業(yè)務(wù)流量異常。此時(shí)也沒(méi)有報(bào)警出來(lái),而這個(gè)報(bào)警卻是依賴(lài)這個(gè) metrics exporter 所采集的數(shù)據(jù),導(dǎo)致了一次小型事件。因?yàn)檫@個(gè)事件,才有今天寫(xiě)文章的動(dòng)力,同時(shí)也分享下解決這個(gè)問(wèn)題的方法。
現(xiàn)象獲取
架構(gòu)圖

問(wèn)題定位
通過(guò)跟小伙伴們一起復(fù)盤(pán),以及追查可能出現(xiàn)問(wèn)題的位置后,大家都覺(jué)得沒(méi)有任何問(wèn)題。在運(yùn)維刪除對(duì)應(yīng)的監(jiān)控資源后,同時(shí)沒(méi)有關(guān)閉報(bào)警規(guī)則的情況下,應(yīng)該有大量的任何異常報(bào)警產(chǎn)生。但實(shí)際情況,沒(méi)有任何報(bào)警發(fā)出來(lái)。
當(dāng)大家一籌莫展的時(shí)候,我突然說(shuō)了一句,會(huì)不會(huì)是數(shù)據(jù)采集出現(xiàn)了問(wèn)題?大家眼前一亮,趕緊拿出 metrics exporter 的代碼檢查。通過(guò)反復(fù)檢查,也沒(méi)有發(fā)現(xiàn)可疑的地方,于是大家又開(kāi)始了思考。這時(shí)我打開(kāi)了 metrics exporter 調(diào)試模式,打上斷點(diǎn),然后請(qǐng)運(yùn)維小伙伴刪除一個(gè)測(cè)試資源,觀察監(jiān)控?cái)?shù)據(jù)的變化。果不其然,資源刪除了,對(duì)應(yīng)監(jiān)控的 metrics 條目的值沒(méi)有變化(也就是說(shuō),還是上次資源的狀態(tài))。
這下破案了,搞了半天是因?yàn)?metrics 條目?jī)?nèi)容沒(méi)有跟隨資源的刪除而被自動(dòng)的刪除。導(dǎo)致了報(bào)警系統(tǒng)一直認(rèn)為被刪除的資源還在運(yùn)行,而且狀態(tài)正常。
原理分析
既然知道了原因,再回過(guò)頭看 metrics exporter 的代碼,代碼中有 prometheus.MustRegister、prometheus.Unregister 和相關(guān)的 MetricsVec 值變更的實(shí)現(xiàn)和調(diào)用。就是沒(méi)有判斷監(jiān)控資源在下線或者刪除的情況下,如何刪除和清理創(chuàng)建出來(lái)的 MetricsVec。
在我的印象中 MetricsVec 會(huì)根據(jù) labels 會(huì)自動(dòng)創(chuàng)建相關(guān)的條目,從來(lái)沒(méi)有手動(dòng)的添加和創(chuàng)建。根據(jù)這個(gè)邏輯我也認(rèn)為,MetricsVec 中如果 labels 對(duì)應(yīng)的值不更新或者處于不活躍的狀態(tài),應(yīng)該自動(dòng)刪除才是。
最后還是把 golang 的 github.com/prometheus/client_golang 這個(gè)庫(kù)想太完美了。沒(méi)有花時(shí)間對(duì) github.com/prometheus/client_golang 內(nèi)部結(jié)構(gòu)、原理、處理機(jī)制充分理解,才導(dǎo)致這個(gè)事件的發(fā)生。
github.com/prometheus/client_golang 中的 metrics 主要是 4 個(gè)種類(lèi),這個(gè)可以 baidu 上搜索,很多介紹,我這里不詳細(xì)展開(kāi)。這些種類(lèi)的 metrics 又可以分為:一次性使用和多次使用。
- 一次性使用:當(dāng)請(qǐng)求到達(dá)了 http 服務(wù)器,被 promhttp 中的 handler 處理后,返回?cái)?shù)據(jù)給請(qǐng)求方。隨后 metrics 數(shù)據(jù)就失效了,不保存。下次再有請(qǐng)求到 http 接口查詢(xún) metrics,數(shù)據(jù)重新計(jì)算生成,返回給請(qǐng)求方。
- 多次性使用:當(dāng)請(qǐng)求到達(dá)了 http 服務(wù)器,被 promhttp 中的 handler 處理后,返回?cái)?shù)據(jù)給請(qǐng)求方。隨后 metrics 保存,并不會(huì)刪除,需要手動(dòng)清理和刪除。 下次再有請(qǐng)求到 http 接口查詢(xún) metrics,直接返回之前存儲(chǔ)過(guò)的數(shù)據(jù)給請(qǐng)求方。
注意這兩者的區(qū)別,他們有不同的應(yīng)用場(chǎng)景。
- 一次性使用:一次請(qǐng)求一次新數(shù)據(jù),數(shù)據(jù)與數(shù)據(jù)間隔時(shí)間由數(shù)據(jù)讀取者決定。 如果有多個(gè)數(shù)據(jù)讀取者,每一個(gè)讀取者讀取到的數(shù)據(jù)可能不會(huì)相同。每一個(gè)請(qǐng)求計(jì)算一次,如果采集請(qǐng)求量比較大,或者內(nèi)部計(jì)算壓力比較大,都會(huì)導(dǎo)致負(fù)載壓力很高。 計(jì)算和輸出是同步邏輯。 例如:k8s 上的很多 exporter 是這樣的方式。
- 多次性使用:每次請(qǐng)求都是從 map 中獲得,數(shù)據(jù)與數(shù)據(jù)間隔時(shí)間由數(shù)據(jù)寫(xiě)入者決定。如果有多個(gè)數(shù)據(jù)讀取者,每一個(gè)讀取者采集的數(shù)據(jù)相同(讀取的過(guò)程中沒(méi)有更新數(shù)據(jù)寫(xiě)入)。每一個(gè)請(qǐng)求獲得都是相同的計(jì)算結(jié)果,1 次計(jì)算多數(shù)讀取。計(jì)算和輸出是異步邏輯。例如:http server 上 http 請(qǐng)求狀態(tài)統(tǒng)計(jì),延遲統(tǒng)計(jì),轉(zhuǎn)發(fā)字節(jié)匯總,并發(fā)量等等。
這次項(xiàng)目中寫(xiě)的 metrics exporter 本應(yīng)該是采用 “一次性使用” 這樣的模型來(lái)開(kāi)發(fā),但是內(nèi)部結(jié)構(gòu)模型采用了 “多次性使用” 模型,因?yàn)橹笜?biāo)數(shù)據(jù)寫(xiě)入者和數(shù)據(jù)讀取者之間沒(méi)有必然聯(lián)系,不屬于一個(gè)會(huì)話系統(tǒng),所以之間是異步結(jié)構(gòu)。具體我們看下圖:

從圖中有 2 個(gè)身份說(shuō)明下:
- 數(shù)據(jù)讀取者:主要是 Prometheus 系統(tǒng)的采集器,根據(jù)配置的規(guī)則周期性的來(lái) metrics 接口讀取數(shù)據(jù)。
- 數(shù)據(jù)寫(xiě)入者:開(kāi)發(fā)的 scanner ,通過(guò)接口去讀遠(yuǎn)程資源狀態(tài)信息和相關(guān)數(shù)據(jù),通過(guò)計(jì)算得到最后的結(jié)果,寫(xiě)入指定的 metrics 條目?jī)?nèi)。
在此次項(xiàng)目中 metrics 條目是用 prometheus.GaugeVec 作為采集數(shù)據(jù)計(jì)算后結(jié)果的存儲(chǔ)類(lèi)型。
說(shuō)了這么多,想要分析真正的原因,就必須深入 github.com/prometheus/client_golang 代碼中 GaugeVec 這個(gè)具體代碼實(shí)現(xiàn)。
// 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 // 真正的數(shù)據(jù)存儲(chǔ)位置
desc *Desc
newMetric func(labelValues ...string) Metric
}
通過(guò)上面的代碼,一條 metric 條目是保存在 metricMap.metrics 下。 我們繼續(xù)往下看:
讀取數(shù)據(jù)
// 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 // 讀取數(shù)據(jù)到通道
}
}
}
寫(xiě)入數(shù)據(jù)
// 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)) // 寫(xiě)入數(shù)據(jù)到變量
}
看到上面的代碼,有的小伙伴就會(huì)說(shuō)讀取和寫(xiě)入的位置不一樣啊,沒(méi)有找到真正的位置。不要著急,后面還有。
// 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 { // 返回了一個(gè)接口
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}) // 這里寫(xiě)入 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 實(shí)現(xiàn)這個(gè)接口
// 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)".
}
看到這里就知道了寫(xiě)入、存儲(chǔ)、讀取已經(jīng)連接到了一起。 同時(shí)如果沒(méi)有顯式的調(diào)用方法刪除 metricMap.metrics 的內(nèi)容,那么記錄的 metrics 條目的值就會(huì)一直存在,而原生代碼中只是創(chuàng)建和變更內(nèi)部值。正是因?yàn)檫@個(gè)邏輯才導(dǎo)致上面說(shuō)的事情。
處理方法
既然找到原因,也找到對(duì)應(yīng)的代碼以及對(duì)應(yīng)的內(nèi)部邏輯,就清楚了 prometheus.GaugeVec 這個(gè)變量真正的使用方法。到此解決方案也就有了,找到合適的位置添加代碼,顯式調(diào)用 DeleteLabelValues 這個(gè)方法來(lái)刪除無(wú)效 metrics 條目。
為了最后實(shí)現(xiàn)整體效果,我總結(jié)下有幾個(gè)關(guān)鍵詞:“異步”、“多次性使用”、“自動(dòng)回收”。

最后的改造思路:
- 創(chuàng)建一個(gè) scanner 掃描結(jié)果存儲(chǔ)的狀態(tài)機(jī) (status)
- 每次 scanner 掃描結(jié)果會(huì)向這個(gè)狀態(tài)機(jī)做更新動(dòng)作,并記錄對(duì)應(yīng)的更新時(shí)間
- 啟動(dòng)一個(gè) goroutine (cleaner) 定期掃描狀態(tài)機(jī),然后遍歷分析記錄數(shù)據(jù)的更新時(shí)間。如果遍歷到對(duì)應(yīng)數(shù)據(jù)的更新時(shí)間跟現(xiàn)在的時(shí)間差值超過(guò)一個(gè)固定的閾值,就主動(dòng)刪除狀態(tài)機(jī)中對(duì)應(yīng)的信息,同時(shí)刪除對(duì)應(yīng)的 metrics 條目
通過(guò)這個(gè)動(dòng)作就可以實(shí)現(xiàn)自動(dòng)回收和清理無(wú)效的 metrics 條目,最后驗(yàn)證下來(lái)確實(shí)有效。
最終效果
通過(guò)測(cè)試代碼來(lái)驗(yàn)證這個(gè)方案的效果,具體如下演示:
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)
// 寫(xiě)入數(shù)據(jù)
for i := 0; i < 10; i++ {
labels := strconv.Itoa(i)
vec.WithLabelValues(labels).Set(1) // 寫(xiě)入 metric 條目
status.Store(labels, metricsMetaData{UpdatedAt: time.Now().Unix(), Labels: []string{labels}}) // 寫(xiě)入狀態(tài)
}
// 創(chuàng)建退出 ctx
stopCtx, stopCancel := context.WithCancel(context.Background())
// 啟動(dòng)清理器
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)
// 創(chuàng)建 http
http.Handle("/metrics", promhttp.Handler())
srv := http.Server{Addr: "0.0.0.0:8080"}
// 啟動(dòng) 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()
}
結(jié)果動(dòng)畫(huà):

以上就是Go prometheus metrics條目自動(dòng)回收與清理方法的詳細(xì)內(nèi)容,更多關(guān)于Go prometheus metrics回收清理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang語(yǔ)言的多種變量聲明方式與使用場(chǎng)景詳解
Golang當(dāng)中的變量類(lèi)型和C/C++比較接近,一般用的比較多的也就是int,float和字符串,下面這篇文章主要給大家介紹了關(guān)于Golang語(yǔ)言的多種變量聲明方式與使用場(chǎng)景的相關(guān)資料,需要的朋友可以參考下2022-02-02
Golang AGScheduler動(dòng)態(tài)持久化任務(wù)調(diào)度的強(qiáng)大庫(kù)使用實(shí)例
這篇文章主要為大家介紹了Golang AGScheduler動(dòng)態(tài)持久化任務(wù)調(diào)度的強(qiáng)大庫(kù)使用實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
golang基于websocket實(shí)現(xiàn)的簡(jiǎn)易聊天室程序
這篇文章主要介紹了golang基于websocket實(shí)現(xiàn)的簡(jiǎn)易聊天室,分析了websocket的下載、安裝及使用實(shí)現(xiàn)聊天室功能的相關(guān)技巧,需要的朋友可以參考下2016-07-07
Go語(yǔ)言到底有沒(méi)有引用傳參(對(duì)比 C++ )
這篇文章主要介紹了Go 到底有沒(méi)有引用傳參(對(duì)比 C++ ),需要的朋友可以參考下2017-09-09
詳解go語(yǔ)言 make(chan int, 1) 和 make (chan int) 的區(qū)別
這篇文章主要介紹了go語(yǔ)言 make(chan int, 1) 和 make (chan int) 的區(qū)別,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01

