最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go prometheus metrics條目自動回收與清理方法

 更新時間:2022年11月08日 11:35:30   作者:自在的LEE  
這篇文章主要為大家介紹了Go prometheus metrics條目自動回收與清理方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

事件背景

現(xiàn)網(wǎng)上運行著一個自己開發(fā)的 metrics exporter,它是專門來捕獲后端資源的運行狀態(tài),并生成對應的 prometheus metrics 供監(jiān)控報警系統(tǒng)使用。當然這個 exporter 只是負責遠程監(jiān)控資源,并不能實際控制后端的資源,也不能實時動態(tài)獲得被監(jiān)控的資源的變動事件。當我們的運維小伙伴手動錯誤刪除后端被監(jiān)控的資源,導致業(yè)務流量異常。此時也沒有報警出來,而這個報警卻是依賴這個 metrics exporter 所采集的數(shù)據(jù),導致了一次小型事件。因為這個事件,才有今天寫文章的動力,同時也分享下解決這個問題的方法。

現(xiàn)象獲取

架構(gòu)圖

問題定位

通過跟小伙伴們一起復盤,以及追查可能出現(xiàn)問題的位置后,大家都覺得沒有任何問題。在運維刪除對應的監(jiān)控資源后,同時沒有關閉報警規(guī)則的情況下,應該有大量的任何異常報警產(chǎn)生。但實際情況,沒有任何報警發(fā)出來。

當大家一籌莫展的時候,我突然說了一句,會不會是數(shù)據(jù)采集出現(xiàn)了問題?大家眼前一亮,趕緊拿出 metrics exporter 的代碼檢查。通過反復檢查,也沒有發(fā)現(xiàn)可疑的地方,于是大家又開始了思考。這時我打開了 metrics exporter 調(diào)試模式,打上斷點,然后請運維小伙伴刪除一個測試資源,觀察監(jiān)控數(shù)據(jù)的變化。果不其然,資源刪除了,對應監(jiān)控的 metrics 條目的值沒有變化(也就是說,還是上次資源的狀態(tài))。

這下破案了,搞了半天是因為 metrics 條目內(nèi)容沒有跟隨資源的刪除而被自動的刪除。導致了報警系統(tǒng)一直認為被刪除的資源還在運行,而且狀態(tài)正常。

原理分析

既然知道了原因,再回過頭看 metrics exporter 的代碼,代碼中有 prometheus.MustRegister、prometheus.Unregister 和相關的 MetricsVec 值變更的實現(xiàn)和調(diào)用。就是沒有判斷監(jiān)控資源在下線或者刪除的情況下,如何刪除和清理創(chuàng)建出來的 MetricsVec。

在我的印象中 MetricsVec 會根據(jù) labels 會自動創(chuàng)建相關的條目,從來沒有手動的添加和創(chuàng)建。根據(jù)這個邏輯我也認為,MetricsVec 中如果 labels 對應的值不更新或者處于不活躍的狀態(tài),應該自動刪除才是。

最后還是把 golang 的 github.com/prometheus/client_golang 這個庫想太完美了。沒有花時間對 github.com/prometheus/client_golang 內(nèi)部結(jié)構(gòu)、原理、處理機制充分理解,才導致這個事件的發(fā)生。

github.com/prometheus/client_golang 中的 metrics 主要是 4 個種類,這個可以 baidu 上搜索,很多介紹,我這里不詳細展開。這些種類的 metrics 又可以分為:一次性使用和多次使用

  • 一次性使用:當請求到達了 http 服務器,被 promhttp 中的 handler 處理后,返回數(shù)據(jù)給請求方。隨后 metrics 數(shù)據(jù)就失效了,不保存。下次再有請求到 http 接口查詢 metrics,數(shù)據(jù)重新計算生成,返回給請求方。
  • 多次性使用:當請求到達了 http 服務器,被 promhttp 中的 handler 處理后,返回數(shù)據(jù)給請求方。隨后 metrics 保存,并不會刪除,需要手動清理和刪除。 下次再有請求到 http 接口查詢 metrics,直接返回之前存儲過的數(shù)據(jù)給請求方。

注意這兩者的區(qū)別,他們有不同的應用場景。

  • 一次性使用:一次請求一次新數(shù)據(jù),數(shù)據(jù)與數(shù)據(jù)間隔時間由數(shù)據(jù)讀取者決定。 如果有多個數(shù)據(jù)讀取者,每一個讀取者讀取到的數(shù)據(jù)可能不會相同。每一個請求計算一次,如果采集請求量比較大,或者內(nèi)部計算壓力比較大,都會導致負載壓力很高。 計算和輸出是同步邏輯。 例如:k8s 上的很多 exporter 是這樣的方式。
  • 多次性使用:每次請求都是從 map 中獲得,數(shù)據(jù)與數(shù)據(jù)間隔時間由數(shù)據(jù)寫入者決定。如果有多個數(shù)據(jù)讀取者,每一個讀取者采集的數(shù)據(jù)相同(讀取的過程中沒有更新數(shù)據(jù)寫入)。每一個請求獲得都是相同的計算結(jié)果,1 次計算多數(shù)讀取。計算和輸出是異步邏輯。例如:http server 上 http 請求狀態(tài)統(tǒng)計,延遲統(tǒng)計,轉(zhuǎn)發(fā)字節(jié)匯總,并發(fā)量等等。

這次項目中寫的 metrics exporter 本應該是采用 “一次性使用” 這樣的模型來開發(fā),但是內(nèi)部結(jié)構(gòu)模型采用了 “多次性使用” 模型,因為指標數(shù)據(jù)寫入者和數(shù)據(jù)讀取者之間沒有必然聯(lián)系,不屬于一個會話系統(tǒng),所以之間是異步結(jié)構(gòu)。具體我們看下圖:

從圖中有 2 個身份說明下:

  • 數(shù)據(jù)讀取者:主要是 Prometheus 系統(tǒng)的采集器,根據(jù)配置的規(guī)則周期性的來 metrics 接口讀取數(shù)據(jù)。
  • 數(shù)據(jù)寫入者:開發(fā)的 scanner ,通過接口去讀遠程資源狀態(tài)信息和相關數(shù)據(jù),通過計算得到最后的結(jié)果,寫入指定的 metrics 條目內(nèi)。

在此次項目中 metrics 條目是用 prometheus.GaugeVec 作為采集數(shù)據(jù)計算后結(jié)果的存儲類型。

說了這么多,想要分析真正的原因,就必須深入 github.com/prometheus/client_golang 代碼中 GaugeVec 這個具體代碼實現(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ù)存儲位置
	desc      *Desc
	newMetric func(labelValues ...string) Metric
}

通過上面的代碼,一條 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ù)到通道
		}
	}
}

寫入數(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))  // 寫入數(shù)據(jù)到變量
}

看到上面的代碼,有的小伙伴就會說讀取和寫入的位置不一樣啊,沒有找到真正的位置。不要著急,后面還有。

// 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 實現(xiàn)這個接口
	// 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)".
}

看到這里就知道了寫入、存儲、讀取已經(jīng)連接到了一起。 同時如果沒有顯式的調(diào)用方法刪除 metricMap.metrics 的內(nèi)容,那么記錄的 metrics 條目的值就會一直存在,而原生代碼中只是創(chuàng)建和變更內(nèi)部值。正是因為這個邏輯才導致上面說的事情。

處理方法

既然找到原因,也找到對應的代碼以及對應的內(nèi)部邏輯,就清楚了 prometheus.GaugeVec 這個變量真正的使用方法。到此解決方案也就有了,找到合適的位置添加代碼,顯式調(diào)用 DeleteLabelValues 這個方法來刪除無效 metrics 條目。

為了最后實現(xiàn)整體效果,我總結(jié)下有幾個關鍵詞:“異步”、“多次性使用”、“自動回收”。

最后的改造思路

  • 創(chuàng)建一個 scanner 掃描結(jié)果存儲的狀態(tài)機 (status)
  • 每次 scanner 掃描結(jié)果會向這個狀態(tài)機做更新動作,并記錄對應的更新時間
  • 啟動一個 goroutine (cleaner) 定期掃描狀態(tài)機,然后遍歷分析記錄數(shù)據(jù)的更新時間。如果遍歷到對應數(shù)據(jù)的更新時間跟現(xiàn)在的時間差值超過一個固定的閾值,就主動刪除狀態(tài)機中對應的信息,同時刪除對應的 metrics 條目

通過這個動作就可以實現(xiàn)自動回收和清理無效的 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)
	// 寫入數(shù)據(jù)
	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}}) // 寫入狀態(tài)
	}
	// 創(chuàng)建退出 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)
	// 創(chuàng)建 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()
}

結(jié)果動畫:

以上就是Go prometheus metrics條目自動回收與清理方法的詳細內(nèi)容,更多關于Go prometheus metrics回收清理的資料請關注腳本之家其它相關文章!

相關文章

  • Golang簡單實現(xiàn)http的server端和client端

    Golang簡單實現(xiàn)http的server端和client端

    Http 服務是基于 Tcp 的應用層的實現(xiàn),也是最常見的網(wǎng)絡協(xié)議之一。本文主要介紹了Golang簡單實現(xiàn)http的server端和client端,感興趣的可以了解一下
    2021-06-06
  • golang中l(wèi)og包自定義輸出日志格式與寫入到文件

    golang中l(wèi)og包自定義輸出日志格式與寫入到文件

    這篇文章主要給大家介紹了關于golang中l(wèi)og包自定義輸出日志格式與寫入到文件的相關資料,日志輸出在任何項目中都極其重要,是有助于后續(xù)我們排查解決程序BUG,需要的朋友可以參考下
    2023-06-06
  • golang使用mTLS實現(xiàn)雙向加密認證http通信

    golang使用mTLS實現(xiàn)雙向加密認證http通信

    這篇文章主要為大家介紹了golang如何調(diào)用mTLS實現(xiàn)雙向加密認證http通信,文中的示例代碼講解詳細,具有一定的學習價值,需要的小伙伴可以參考下
    2023-08-08
  • Golang?WorkerPool線程池并發(fā)模式示例詳解

    Golang?WorkerPool線程池并發(fā)模式示例詳解

    這篇文章主要為大家介紹了Golang?WorkerPool線程池并發(fā)模式示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • 使用Go語言自制簡單易用的Web框架

    使用Go語言自制簡單易用的Web框架

    這篇文章主要為大家詳細介紹了如何使用Go語言實現(xiàn)自制簡單易用的Web框架,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下
    2024-01-01
  • go使用snmp庫查詢mib數(shù)據(jù)案例代碼

    go使用snmp庫查詢mib數(shù)據(jù)案例代碼

    go語言使用snmp庫中的 k-sone/snmpgo 實現(xiàn)相關mib查詢,本文通過實例代碼給大家介紹了go使用snmp庫查詢mib數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧
    2023-10-10
  • 一文帶你使用Golang實現(xiàn)SSH客戶端

    一文帶你使用Golang實現(xiàn)SSH客戶端

    SSH?全稱為?Secure?Shell,是一種用于安全地遠程登錄到網(wǎng)絡上的其他計算機的網(wǎng)絡協(xié)議,本文主要為大家詳細介紹了如何使用?Golang?實現(xiàn)?SSH?客戶端,需要的可以參考下
    2023-11-11
  • go寫文件后出現(xiàn)大量NUL字符問題解決

    go寫文件后出現(xiàn)大量NUL字符問題解決

    本文主要介紹了go寫文件后出現(xiàn)大量NUL字符問題解決,由于每次寫的時候設置的長度都是64,在某次不足64時,byte切片空余位置被填充為空字符,下面就來介紹一下如何解決
    2023-12-12
  • 總結(jié)Go語言中defer的使用和注意要點

    總結(jié)Go語言中defer的使用和注意要點

    Go語言中的defer關鍵字實現(xiàn)比較特殊的功能,這篇文章給大家總結(jié)了關于Go語言中defer的使用和注意要點,有需要的朋友們可以參考借鑒,下面來一起看看吧。
    2016-09-09
  • 從錯誤中學習改正Go語言六個壞習慣提高編程技巧

    從錯誤中學習改正Go語言六個壞習慣提高編程技巧

    這篇文章主要為大家介紹了從錯誤中學習改正Go語言五個壞習慣提高編程技巧示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-05-05

最新評論

宁阳县| 剑川县| 灌云县| 昌都县| 新津县| 汉沽区| 竹溪县| 大邑县| 齐齐哈尔市| 石门县| 竹北市| 广平县| 长岛县| 仁寿县| 广汉市| 建湖县| 独山县| 宝丰县| 东台市| 上林县| 合江县| 通辽市| 错那县| 肥城市| 广平县| 张家界市| 万安县| 辛集市| 梓潼县| 龙井市| 都安| 安龙县| 布尔津县| 河南省| 两当县| 阿鲁科尔沁旗| 阿瓦提县| 上杭县| 米脂县| 炉霍县| 九江县|