Skip to content

Metrics实战开发

可以Prometheus提供的SDK快速完成Metrics数据的暴露。

安装

go get -u github.com/prometheus/client_golang/prometheus/promhttp

默认指标

Prometheus提供的Client会暴露全局默认指标注册表,其中包含promhttp处理器和runtime相关的默认指标,根据不同指标名称的前缀区分:

  • go_*:以go_为前缀的指标是关于Go运行时相关的指标,比如垃圾回收时间、goroutine数量等,都是go客户端特有的,其他语言可能会暴露各自语言的运行时指标。
  • promhttp_*promhttp工具包的相关指标,用于跟踪指标请求的处理。
import (
	"fmt"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"net/http"
)

func main() {
	http.Handle("/metrics", promhttp.Handler())
	http.ListenAndServe(":8005", nil)
}

访问

# curl 127.0.0.1:8005/metrics
···
# HELP go_memstats_sys_bytes Number of bytes obtained from system.
# TYPE go_memstats_sys_bytes gauge
go_memstats_sys_bytes 8.784912e+06
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 7
# HELP promhttp_metric_handler_requests_in_flight Current number of scrapes being served.
# TYPE promhttp_metric_handler_requests_in_flight gauge
promhttp_metric_handler_requests_in_flight 1
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 0
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0

采集器

通过Prometheus提供的Collectors发现,直接把指标注册到Registry中的方式不太优雅,为了更好的模块化,需要把指标采集封装一个Collector对象,这也是很多第三方Collector的标准写法。

常用注册方法

  • MustNewConstMetric:注册Gauge、Counter类型指标
  • MustNewConstHistogram:注册Histogram类型指标
  • MustNewConstSummary:注册Summary类型指标

Collector接口声明

type Collector interface {
	// 指标的一些描述信息, 就是# 标识的那部分
	// 注意这里使用的是指针, 因为描述信息 全局存储一份就可以了
	Describe(chan<- *Desc)
	// 指标的数据, 比如 promhttp_metric_handler_errors_total{cause="gathering"} 0
	// 这里没有使用指针, 因为每次采集的值都是独立的
	Collect(chan<- Metric)
}

源码


// EmptyRegistry 空指标注册表
var (
	EmptyRegistry = prometheus.NewRegistry()
)

// Monitor 创建采集器
type Monitor struct {
	InterfaceStatusCode *prometheus.Desc
}

// NewMonitorMetrics 创建采集器指标注册规范
func NewMonitorMetrics() *Monitor {
	return &Monitor{
		InterfaceStatusCode: prometheus.NewDesc(
			"url_interface_status_code", // 指标名称
			"url 接口状态码",                 // 描述信息
			[]string{"app", "url"},      // 动态指标
			nil,                         // 静态指标
		),
	}
}

// Describe 收集描述信息
func (m Monitor) Describe(desc chan<- *prometheus.Desc) {
	desc <- m.InterfaceStatusCode
}

// Collect 收集指标数据
func (m Monitor) Collect(metrics chan<- prometheus.Metric) {
	metrics <- prometheus.MustNewConstMetric(
		m.InterfaceStatusCode,
		prometheus.GaugeValue,
		float64(100),
		"test",
		"http://url",
	)
}

func TestRegisterer() {
	// 注册采集器
	EmptyRegistry.MustRegister(NewMonitorMetrics())
	http.HandleFunc("/metrics", func(writer http.ResponseWriter, request *http.Request) {
		promhttp.HandlerFor(EmptyRegistry,
			promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(writer, request)
	})
	_ = http.ListenAndServe(":8001", nil)
}

func main() {

	TestRegisterer()

}

访问

% curl 127.0.0.1:8001/metrics
# HELP url_interface_status_code url 接口状态码
# TYPE url_interface_status_code gauge
url_interface_status_code{app="test",url="http://url"} 100

实战项目源码

问题

WithLabelValues和MustNewConst{Metric/Histogram/Summary}的区别

在开发exporter时遇到了一个问题,当我使用如下两种方式注册Metrics时,进程所消耗的资源是不一样的,WithLabelValues注册label消耗的资源特别高,使用MustNewConstMetric消耗就特别低,因此想看一下到底是怎么个事儿

WithLabelValuesMustNewConst{Metric/Histogram/Summary}都是 Prometheus 客户库中用于创建指标的方法。两者都支持动态标签,但它们在使用方式和适用场景上有所不同。

WithLabelValues

  • 使用方式:
    • 使用 WithLabelValues 方法直接传入动态标签值。
metric := pgc.HTTPLatencyHistogramCollect.WithLabelValues(name, strconv.Itoa(statusCode), requestPath)
metric.Observe(latency)

metric.Collect(ch)
  • 适用场景:
    • 适用于更复杂指标,具有动态值或大量标签。
    • 提供更大的灵活性,可以轻松捕获指标中的各种属性和变化。
    • 适用于需要根据特定标签组合进行细粒度分析的场景。

MustNewConstMetric

  • 使用方式:
    • 使用 Desc 结构定义指标名称、帮助文本、类型和标签。
    • 在 Desc 结构中指定动态标签名称。
    • 使用 MustNewConstMetric 方法创建指标,并传入动态标签值。
pid := strconv.Itoa(os.Getpid())
cmdline := os.Args[0]
user := os.Getenv("USER")
cpuUsage := 50.0 // Percentage

desc := prometheus.NewDesc(
    "cpu_usage",
    "CPU usage of process",
    []string{"pid", "cmdline", "user"},
    nil,
)

metric := prometheus.MustNewConstMetric(
    desc,
    prometheus.GaugeValue,
    cpuUsage,
    pid,
    cmdline,
    user,
)

metric.Collect(ch)
  • 适用场景:
    • 适用于简单指标,具有固定值和少量常量标签。
    • 创建指标的效率更高,特别是在频繁更新指标的情况下。
    • 适用于代码可读性要求较高的场景。

总结 建议根据您的具体需求选择合适的创建指标方法。

特性WithLabelValuesMustNewConstMetric
使用方式直接传入动态标签值使用 Desc 结构定义动态标签
适用场景复杂指标,动态值或大量标签简单指标,固定值和少量常量标签
优势更灵活,易于捕获各种属性和变化创建效率更高,代码可读性更好
劣势创建效率较低灵活度较低
  • 如果您需要创建更复杂指标,具有动态值或大量标签,并且需要根据特定标签组合进行细粒度分析,那么请使用 WithLabelValues。
  • 如果您需要创建简单指标,具有固定值和少量常量标签,并且更关心代码的可读性和创建效率,那么请使用 MustNewConstMetric。

分析MustNewConstMetric源码

处理较多的指标时,性能快,只做一些拼接的动作

MustNewConstMetric收集metric的思路想对简单

  • 校验label是否合法,收集的label与desc的label比对;
  • 声明空指标;
  • 制作metric格式的label并填充到声明的指标当中;
func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) {
	if desc.err != nil {
		return nil, desc.err
	}

  // 校验labelValues的长度和Desc中的labels是否一致,不一致则返回错误
	if err := validateLabelValues(labelValues, len(desc.variableLabels)); err != nil {
		return nil, err
	}

  // 声明空指标
	metric := &dto.Metric{}
  // 填充指标,首先制作desc中用于收集metrics的label,拼成一个完整的metric。
	if err := populateMetric(valueType, value, MakeLabelPairs(desc, labelValues), nil, metric); err != nil {
		return nil, err
	}

	return &constMetric{
		desc:   desc,
		metric: metric,
	}, nil
}

分析WithLabelValues源码

处理较多的指标时,性能低,需要做hash计算,消耗cpu;

收集metric相对复杂

  • 校验label是否合法,收集的label与desc的label比对;
  • 对label进行hash(建立label的唯一值)
  • 通过hash值
  • 获取label
func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge {
	g, err := v.GetMetricWithLabelValues(lvs...)
	if err != nil {
		panic(err)
	}
	return g
}

针对label value进行hash,返回hash值

将标签值转换为唯一的哈希值,以便高效地存储和检索指标。

func (m *MetricVec) hashLabelValues(vals []string) (uint64, error) {
	// 校验labelValues的长度和Desc中的labels是否一致,不一致则返回错误
  if err := validateLabelValues(vals, len(m.desc.variableLabels)-len(m.curry)); err != nil {
		return 0, err
	}

	var (
		h             = hashNew()
		curry         = m.curry
		iVals, iCurry int
	)

  // 遍历指标描述 (m.desc.variableLabels) 中定义的所有变量标签。
	for i := 0; i < len(m.desc.variableLabels); i++ {
  // 检查 curry 中是否存在与当前变量标签匹配的预定义标签值
		if iCurry < len(curry) && curry[iCurry].index == i {
  // 将当前哈希值 (h) 与标签值的哈希值相加。
			h = m.hashAdd(h, curry[iCurry].value)
			iCurry++
		} else {
			h = m.hashAdd(h, vals[iVals])
			iVals++
		}
  // 将当前哈希值 (h) 与分隔符字节的哈希值相加(用于区分标签)。
		h = m.hashAddByte(h, model.SeparatorByte)
	}
	return h, nil
}

根据hash值来获取metric

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,如xx{aa=bb...}
		metric = m.newMetric(inlinedLVs...)
    // 对hash后的label值做完索引进行赋值。
		m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric})
	}
	return metric
}

源码分析后的总结

  • MustNewConstMetric:无脑拼接metric,用于静态label,后续不会动态变化的;
  • WithLabelValues:会对label进行hash,作为索引 会将label存到哈希的数据结构,从哈希的数据结构中根据hash取出metirc,如果不存在则创建并返回。这意味着整个 metricMap 使用哈希映射进行组织以实现高效检索。

Released under the GPL License.