1. 程式人生 > 其它 >Prometheus原始碼分析:基於Go Client自定義的Exporter,是如何在Local儲存Metrics的?

Prometheus原始碼分析:基於Go Client自定義的Exporter,是如何在Local儲存Metrics的?

技術標籤:雲原生 & 微服務程式設計師日常gogogolang伺服器運維http

目錄

1 背景

2 什麼是Exporter?

3 Prometheus以輪詢的方式Pull拉取Metrics

4 Target是如何在本地儲存Metrics的?

4.1 基於Go Client開發的Exporter

4.2 Counter型別Metric原始碼分析

4.2.1 宣告Counter型別變數

4.2.2 Counter型別定義

4.2.3 counter.go WithLabelValues方法

4.2.4 counter.goGetMetricWithLabelValues方法

4.2.5 vec.goGetMetricWithLabelValues方法

4.2.6 metricMap的結構,Metric最終存到一個map裡,key=根據label值計算出的hash值,value=Metric元資訊

5 Prometheus拉取Exporter的哪些資料?


1 背景

我們想要提高微服務系統的可觀察性,因此在go語言寫的微服務中,使用Prometheus提供的go client實現上報metrics的功能。

2 什麼是Exporter?

廣義上講,所有可以向Prometheus提供監控樣本資料的程式都可以被稱為一個Exporter。

而Exporter的一個例項稱為target,如下所示,Prometheus通過輪詢的方式定期從這些target中獲取樣本資料。

例如我有個微服務是用go語言寫的,並且這個微服務部署了兩個例項,且在每個例項中都對外提供了一個HTTP介面"/Metrics",然後Prometheus可以通過這個HTTP介面訪問到該例項上的Metrics資訊。

在這個例子中,go程式碼裡的HTTP介面"/Metrics"的相關程式碼就是一個Exporter,而每個微服務例項中的這個HTTP介面就是一個target。

3 Prometheus以輪詢的方式Pull拉取Metrics

Prometheus如何獲取target裡的Metrics資訊?

Prometheus整體架構是以Pull的形式獲取Metrics資訊,因此它會以輪詢的方式,從target那獲取Metrics資訊,例如訪問target對外暴露的HTTP介面獲取Metrics資訊。

4 Target是如何在本地儲存Metrics的?

我們以Counter型別的Metric為例。

4.1 基於Go Client開發的Exporter

package main

import (
	"github.com/prometheus/client_golang/prometheus"
	"net/http"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"time"
	"math/rand"
	"fmt"
)

// Counter型別的Metric
var httpRequestCount = prometheus.NewCounterVec(
	prometheus.CounterOpts{
		Name: "http_request_count", // Metric的name
		Help: "http request count"}, // Metric的說明資訊
	[]string{"endpoint"}) // Metric有一個Label,名稱是endpoint,Metric形如 http_request_count(endpoint="")

// Gauge型別的Metric
var orderNum = prometheus.NewGauge(
	prometheus.GaugeOpts{
		Name: "order_num",
		Help: "order num"})

// Summary型別的Metric
var httpRequestDuration = prometheus.NewSummaryVec(
	prometheus.SummaryOpts{
		Name: "http_request_duration",
		Help: "http request duration",
	},
	[]string{"endpoint"},
)

// 將Metric註冊到本地的Prometheus
func init() {
	prometheus.MustRegister(httpRequestCount)
	prometheus.MustRegister(orderNum)
	prometheus.MustRegister(httpRequestDuration)
}


func main() {
    // Exporter
	http.Handle("/metrics", promhttp.Handler()) // 對外暴露metrics介面,等待Prometheus來拉取
	http.HandleFunc("/hello/", hello) // 處理業務請求,並變更Metric資訊
	ipport := "127.0.0.1:8888"
	fmt.Println("伺服器啟動%s", ipport)
	err := http.ListenAndServe(ipport, nil)
	if err != nil {
		fmt.Println(err)
	}
}

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("process one request = %s\n", r.URL.Path)
    // Counter型別的Metric只能增
	httpRequestCount.WithLabelValues(r.URL.Path).Inc()
	start := time.Now()
	n := rand.Intn(100)
    // Gauge型別的Metric可增可減
	if n >= 90 {
		orderNum.Dec()
		time.Sleep(100 * time.Millisecond)
	} else {
		orderNum.Inc()
		time.Sleep(50 * time.Millisecond)
	}
    // Summary型別Metric
	elapsed := (float64)(time.Since(start) / time.Millisecond)
	httpRequestDuration.WithLabelValues(r.URL.Path).Observe(elapsed)
	w.Write([]byte("ok"))
}

4.2 Counter型別Metric原始碼分析

4.2.1 宣告Counter型別變數

// Counter型別的Metric
var httpRequestCount = prometheus.NewCounterVec(
	prometheus.CounterOpts{
		Name: "http_request_count", // Metric的name
		Help: "http request count"}, // Metric的說明資訊
	[]string{"endpoint"}) // Metric有一個Label,名稱是endpoint,Metric形如 http_request_count(endpoint="")

4.2.2 Counter型別定義

// Counter定義
type CounterVec struct {
	*MetricVec
}

// MetricVec定義
type MetricVec struct {
	*metricMap // Metric最終是存在這裡

	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
}

4.2.3 counter.go WithLabelValues方法

重點

一個指標由Metric name + Labels共同確定。

若Metric name相同,但Label的值不同,則是不同的Metric。

例如:http_request_count(endpoint="hello"),http_request_count(endpoint="world")是兩個不同的指標

// @Param lvs 表示label values
func (v *CounterVec) WithLabelValues(lvs ...string) Counter {
	c, err := v.GetMetricWithLabelValues(lvs...) // 根據label的值來找對應的Metric
	if err != nil {
		panic(err)
	}
	return c
}

4.2.4 counter.goGetMetricWithLabelValues方法

// 根據label的值來找對應的Metric
// @Param lvs表示label value
func (v *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) {
	metric, err := v.MetricVec.GetMetricWithLabelValues(lvs...)
	if metric != nil {
		return metric.(Counter), err
	}
	return nil, err
}

4.2.5 vec.goGetMetricWithLabelValues方法

// 根據label值找對應的metric
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
	h, err := m.hashLabelValues(lvs) // 獲取label對應的hash值,非重點不展開講,這塊的核心是,若hash值一樣,則對應的Metric是同一個
	if err != nil {
		return nil, err
	}
    // 根據hash值從metricMap裡get對應的metric
    // 若不存在則新建立一個metric並放入到metricMap裡
	return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil
}

4.2.6 metricMap的結構,Metric最終存到一個map裡,key=根據label值計算出的hash值,value=Metric元資訊

// metricMap定義,Exporter的Metric都存在這個結構中
type metricMap struct {
	mtx       sync.RWMutex // Protects metrics.
	metrics   map[uint64][]metricWithLabelValues // Metric最終存到一個map裡,key=根據label值計算出的hash值,value=Metric元資訊
	desc      *Desc
	newMetric func(labelValues ...string) Metric
}

type metricWithLabelValues struct {
	values []string // label的值
	metric Metric // Metric的meta資訊
}

5 Prometheus拉取Exporter的哪些資料?

// Prometheus拉取的入口
http.Handle("/metrics", promhttp.Handler())

// http.go promhttp.Handler()
func Handler() http.Handler {
	return InstrumentMetricHandler(
		prometheus.DefaultRegisterer, HandlerFor(prometheus.DefaultGatherer, HandlerOpts{}),
	)
}


// http.go HandlerFor
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
	// 省略部分程式碼
	mfs, err := reg.Gather() // 收集Metric資訊
	// 省略部分程式碼
}

// prometheus.DefaultGatherer
// registry.go 
var (
	defaultRegistry              = NewRegistry() // DefaultGatherer就是defaultRegistry
	DefaultRegisterer Registerer = defaultRegistry
	DefaultGatherer   Gatherer   = defaultRegistry
)

// registry.go 
// Gather implements Gatherer. 負責收集metrics資訊
func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
	// 省略部分程式碼
    // 宣告Counter型別的Metric後,需要MustRegist註冊到Registry,最終就是儲存在collectorsByID裡
	for _, collector := range r.collectorsByID {
		checkedCollectors <- collector
	}
	// 省略部分程式碼
	collectWorker := func() {
		for {
			select {
			case collector := <-checkedCollectors:
				collector.Collect(checkedMetricChan) // 執行Counter的Collect,見下文
			case collector := <-uncheckedCollectors:
				collector.Collect(uncheckedMetricChan)
			default:
				return
			}
			wg.Done()
		}
	}

	// 省略部分程式碼
}


// vec.go
// Collect implements Collector.
func (m *MetricVec) Collect(ch chan<- Metric) { m.metricMap.Collect(ch) }

// vec.go
// Collect implements Collector.
// 返回metricMap裡所有的Metric
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
		}
	}
}


至此,可見Prometheus拉取的就是Counter型別的metricMap裡的 metric資料。

這裡有一點要注意:Prometheus拉取metric後並沒有刪除Local的metric資訊。