Prometheus 源码专题【左扬精讲】—— Prometheus Exporter 定制化开发:汇集器篇 —— 深入学习汇集器工作机制

Prometheus 源码专题【左扬精讲】—— Prometheus Exporter 定制化开发:汇集器篇 —— 深入学习汇集器工作机制

        在 Prometheus 监控体系中,汇集器(Collector)扮演着至关重要的角色,它处于采集器的下游,负责接收来自采集器的各种样本,并将这些样本进行处理和整合。本文将深入探讨汇集器的工作机制,从源码角度详细分析它如何决定接受哪些采集样本,以及接收和处理样本的具体过程。

一、汇集器概述

        汇集器的主要任务是接收来自采集器的样本,将这些样本转化为 protobuf 消息对象,然后按照一定规则排序,并将结果组合成一个大的 protobuf 消息对象。在 Prometheus 中,样本通过 metric 通道传递给汇集器,每个样本都具备转化为 protobuf 消息对象的能力。

二、客户端角度:Registry 和 Gatherer

在 Prometheus 客户端库中,Registry(百度翻译:英[ˈredʒɪstri] 美[ˈredʒɪstri]Gatherer(百度翻译:[ˈɡæðərə(r)] 美[ˈɡæðərər]) 是两个核心概念,它们与汇集器的工作密切相关。

2.1、Registry

Registry 是 Prometheus 客户端库中用于管理和注册指标的容器。它负责跟踪所有已注册的指标,并在需要时提供这些指标的信息。在源码中,Registry 是一个结构体,定义在 https://github.com/prometheus/client_golang/tree/main/prometheus 包中。

v1.22.0 版本为例,https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L260

// Registry 用于注册 Prometheus 收集器、收集它们的指标,并将这些指标整合为 MetricFamilies 以用于暴露(供外部抓取)
// 它实现了 Registerer、Gatherer 和 Collector 接口
// 注意:零值不可用,必须通过 NewRegistry 或 NewPedanticRegistry 函数创建实例
//
// Registry 实现了 Collector 接口,这使得它可以用于创建指标组
// 关于如何实现指标分组,可以参考 Grouping 示例
type Registry struct {
	mtx                   sync.RWMutex        // 读写互斥锁,用于保证 Registry 操作的并发安全性
	collectorsByID        map[uint64]Collector // 以 ID 为键存储收集器,其中 ID 是描述符 ID(descIDs)的哈希值,用于快速查找和去重
	descIDs               map[uint64]struct{}  // 存储所有已注册指标描述符的 ID,用于校验指标描述符的唯一性和一致性
	dimHashesByName       map[string]uint64    // 以指标名称为键存储维度哈希,用于检查同一指标名称下的维度(标签)是否一致
	uncheckedCollectors   []Collector          // 存储未校验的收集器(即 Describe 方法不返回任何描述符的收集器)
	pedanticChecksEnabled bool                 // 标识是否启用严格校验模式,启用后会对指标描述符进行更严格的检查
} 

关键说明:

    • 这个 Registry 结构体是 Prometheus 指标注册和管理的核心实现,通过内部数据结构维护了所有收集器和指标描述符的状态。
    • 字段设计体现了对并发安全(sync.RWMutex)、唯一性校验(descIDs、collectorsByID)、兼容性检查(dimHashesByName)的考虑。
    • pedanticChecksEnabled 字段支持两种校验模式:普通模式(默认)和严格模式(通过 NewPedanticRegistry 创建),严格模式会对指标描述符进行更细致的合法性校验(如标签格式、命名规范等)。
    • uncheckedCollectors 单独存储未校验的收集器,这类收集器因缺乏描述符无法参与正常的注册校验和注销,需要使用者自行保证其安全性。

2.2、如何使用Registry接口收集指标?

      • Registry:Prometheus 的核心组件,负责管理和收集所有注册的指标。
      • Collector:实现了Describe和Collect方法的对象,用于生成和提供指标数据。
      • Metric:具体的指标数据,如 Counter、Gauge、Histogram 等。
    • 步骤 1:创建自定义 Registry

      import (
          "github.com/prometheus/client_golang/prometheus"
          "github.com/prometheus/client_golang/prometheus/promhttp"
          "net/http"
      )
      
      // 创建普通Registry
      registry := prometheus.NewRegistry()
      
      // 或创建严格模式Registry(校验更严格)
      registry := prometheus.NewPedanticRegistry()
    • 步骤 2:创建并注册指标

      // 创建Counter指标
      requestsTotal := prometheus.NewCounterVec(
          prometheus.CounterOpts{
              Name: "http_requests_total",
              Help: "Total number of HTTP requests",
          },
          []string{"method", "path", "status"}, // 标签维度
      )
      
      // 创建Gauge指标
      inProgressRequests := prometheus.NewGauge(
          prometheus.GaugeOpts{
              Name: "http_requests_in_progress",
              Help: "Number of in-flight HTTP requests",
          },
      )
      
      // 注册指标到自定义Registry
      registry.MustRegister(requestsTotal, inProgressRequests)
    • 步骤 3:在代码中更新指标

      // HTTP处理函数示例
      func handler(w http.ResponseWriter, r *http.Request) {
          // 记录请求开始
          inProgressRequests.Inc()
          defer inProgressRequests.Dec()
      
          // 处理请求...
      
          // 记录请求完成
          requestsTotal.WithLabelValues(r.Method, r.URL.Path, "200").Inc()
      }
    • 步骤 4:暴露指标供 Prometheus 抓取

      // 创建HTTP处理函数,将自定义Registry的指标暴露
      http.Handle("/metrics", promhttp.HandlerFor(
          registry,
          promhttp.HandlerOpts{
              ErrorLog:      log.New(os.Stderr, "promhttp: ", log.LstdFlags),
              ErrorHandling: promhttp.ContinueOnError,
          },
      ))
      
      // 启动HTTP服务器
      log.Fatal(http.ListenAndServe(":8080", nil))

2.3、完整示例代码

package main

import (
    "log"
    "net/http"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    // 创建自定义Registry
    registry := prometheus.NewRegistry()

    // 创建指标
    requestsTotal := prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"},
    )

    inProgressRequests := prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "http_requests_in_progress",
            Help: "Number of in-flight HTTP requests",
        },
    )

    // 注册指标到自定义Registry
    registry.MustRegister(requestsTotal, inProgressRequests)

    // HTTP处理函数
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        inProgressRequests.Inc()
        defer inProgressRequests.Dec()

        w.Write([]byte("Hello, World!"))
        requestsTotal.WithLabelValues(r.Method, r.URL.Path, "200").Inc()
    })

    // 暴露自定义Registry的指标
    http.Handle("/metrics", promhttp.HandlerFor(
        registry,
        promhttp.HandlerOpts{
            ErrorLog:      log.New(os.Stderr, "promhttp: ", log.LstdFlags),
            ErrorHandling: promhttp.ContinueOnError,
        },
    ))

    // 启动服务器
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

2.4、关键细节说明

2.4.1、指标注册方式

      • registry.Register(collector):返回 error,需手动检查处理。注册失败(如指标名称冲突)时不会中断程序,但需显式处理错误(如记录日志、回退策略)。适合需要精细控制或在运行时动态注册的场景(https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L270)
      • registry.MustRegister(collector):注册失败时直接触发 panic,强制程序崩溃。适合应用初始化阶段(如 init() 函数或 main() 开头),确保指标注册成功,避免因错误配置导致监控失效。(https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L403)

2.4.2、自定义 Collector

若需要复杂指标逻辑,可实现自定义 Collector:

// 定义自定义收集器类型,实现 prometheus.Collector 接口
// 用于收集和暴露自定义业务指标,适用于标准指标类型(Counter/Gauge等)无法满足的场景
type CustomCollector struct {
    metric *prometheus.Desc  // 存储指标的元数据描述符(名称、帮助信息、标签等)
    // 可扩展:根据业务需求添加更多字段,例如数据库连接、缓存客户端等
    // 例:db *sql.DB  // 用于从数据库查询指标数据
}

// Describe 实现 prometheus.Collector 接口的 Describe 方法
// 作用:向传入的通道提供当前收集器所有指标的描述符(Desc)
// 这是注册阶段进行校验的关键步骤(确保指标唯一性、格式合法性)
// 参数 ch:用于传递描述符的通道,由 Registry 内部创建并传入
func (c *CustomCollector) Describe(ch chan<- *prometheus.Desc) {
    // 将收集器持有的指标描述符发送到通道
    // 若有多个指标,需依次发送所有描述符(例如:ch <- c.metric1; ch <- c.metric2)
    ch <- c.metric
}

// Collect 实现 prometheus.Collector 接口的 Collect 方法
// 作用:在指标抓取阶段(如Prometheus拉取/metrics时),实时计算并提供指标数据
// 调用时机:每次有请求访问/metrics端点时,Registry会调用所有注册的Collector的Collect方法
// 参数 ch:用于传递具体指标数据(Metric)的通道
func (c *CustomCollector) Collect(ch chan<- prometheus.Metric) {
    // 计算并发送指标值(此处为简化示例,实际应根据业务逻辑动态获取数据)
    // 可在此处添加复杂逻辑:如查询数据库、调用API、解析日志等
    // 例:从数据库获取值:value, err := queryMetricFromDB(c.db); if err != nil { ... }
    
    // 使用 MustNewConstMetric 创建一个常量指标(适用于瞬时值)
    // 若需动态更新,可在每次调用时重新计算value
    // 参数说明:
    // 1. c.metric:指标描述符(与Describe中一致,确保元数据匹配)
    // 2. prometheus.GaugeValue:指标类型(此处为 gauge,可根据需求改为 CounterValue 等)
    // 3. 42.0:指标的实际数值(示例固定值,实际应替换为业务计算结果)
    ch <- prometheus.MustNewConstMetric(
        c.metric,
        prometheus.GaugeValue,
        42.0,  // 实际场景中可能是:当前在线用户数、队列长度、任务成功率等
    )
    
    // 扩展:若有多个指标,需创建并发送对应Metric
    // 例:ch <- prometheus.MustNewConstMetric(c.metric2, ...)
}

// 注册自定义收集器到注册表
// registry.MustRegister 会触发以下流程:
// 1. 调用 CustomCollector 的 Describe 方法获取描述符
// 2. 校验描述符合法性(名称格式、与已有指标冲突等)
// 3. 若校验通过,将收集器加入Registry管理,等待Collect调用
registry.MustRegister(&CustomCollector{
    // 初始化指标描述符(Desc),通过 prometheus.NewDesc 创建
    // 参数说明:
    // 1. "custom_metric":指标名称(需符合Prometheus命名规范:小写字母、下划线分隔)
    // 2. "A custom metric":指标帮助信息(说明指标含义,便于监控理解)
    // 3. nil:标签列表(若有标签,需指定标签名数组,如 []string{"env", "service"})
    // 4. nil:指标的常量标签(固定附加到指标的标签键值对,如 prometheus.Labels{"team": "backend"})
    metric: prometheus.NewDesc(
        "custom_metric",
        "A custom metric",
        nil, nil,
    ),
    // 扩展:初始化其他字段,如:db: dbConn(传入数据库连接)
})

关键点说明:

        1. 接口实现要求:必须同时实现 Describe 和 Collect 方法,缺一不可
          • Describe 关注 "指标是什么"(元数据)
          • Collect 关注 "指标值是多少"(实时数据)
        2. 适用场景:
          • 指标值需要通过复杂计算或外部系统获取(如数据库查询、第三方 API 调用)
          • 需封装业务逻辑的指标(如根据多个原始指标计算得出的聚合指标)
          • 动态生成标签或指标的场景(如根据配置文件自动创建指标)
        3. 注意事项:
          • Collect 方法应设计为幂等、高效(避免长时间阻塞,影响指标抓取)
          • 若 Collect 可能产生错误,需使用 prometheus.NewInvalidMetric 发送错误信息,而非直接 panic
          • 多指标场景:一个 Collector 可管理多个指标,只需在 Describe 和 Collect 中处理所有指标即可

2.4.3、自定义 Collector(带标签和动态数据)

type UserCollector struct {
    userCountDesc *prometheus.Desc
    db            *sql.DB
}

func (u *UserCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- u.userCountDesc
}

func (u *UserCollector) Collect(ch chan<- prometheus.Metric) {
    // 从数据库查询不同用户类型的数量
    rows, err := u.db.Query("SELECT user_type, count(*) FROM users GROUP BY user_type")
    if err != nil {
        // 发送错误指标(会被Prometheus记录为invalid metric)
        ch <- prometheus.NewInvalidMetric(u.userCountDesc, err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        var userType string
        var count float64
        if err := rows.Scan(&userType, &count); err != nil {
            ch <- prometheus.NewInvalidMetric(u.userCountDesc, err)
            continue
        }
        // 发送带标签的指标值
        ch <- prometheus.MustNewConstMetric(
            u.userCountDesc,
            prometheus.GaugeValue,
            count,
            userType,  // 对应标签列表中的"user_type"
        )
    }
}

2.4.4、避免与默认 Registry 冲突

若使用自定义 Registry,需确保不与全局默认 Registry(prometheus.DefaultRegisterer)混用,防止指标重复注册。

2.5、验证指标

启动服务后,访问 http://localhost:8080/metrics 查看指标输出:

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/",status="200"} 1

# HELP http_requests_in_progress Number of in-flight HTTP requests
# TYPE http_requests_in_progress gauge
http_requests_in_progress 0  

3.1、Gatherer

Gatherer 是 Prometheus 中负责"收集指标数据"的标准接口,而"Registry(注册表)"作为 Prometheus 的核心组件,已经实现了这个接口,所以 Registry 可以直接当作 Gatherer 来用。 

3.1.1、什么是 Gatherer 接口?

Gatherer 是 Prometheus 定义的一个抽象接口,它只规定了一个核心方法 Gather(),作用是:

      • 把所有已注册的指标数据收集起来,整理成可暴露给外部(如 Prometheus 服务器抓取)的格式(MetricFamily)。

简单说,Gatherer 的职责就是 “汇总指标数据”,它的接口定义大概长这样(简化版):

type Gatherer interface {
    // Gather 收集所有指标,返回整理后的指标家族列表,可能返回错误(如收集失败)
    Gather() ([]*dto.MetricFamily, error)
}
  • Gatherer 接口只有一个方法 Gather,该方法返回一个 dto.MetricFamily 切片和一个错误。
  • dto.MetricFamily 是 protobuf 消息对象,用于表示一组相关的指标。
  • 当需要收集指标时,可以调用 Gatherer 的 Gather 方法。例如:
    metricFamilies, err := registry.Gather()
    if err != nil {
        log.Fatalf("Failed to gather metrics: %v", err)
    }

3.1.2、什么是 Registry?

      • Registry 是 Prometheus 中的具体组件(结构体),它的核心功能是:
        • 管理所有注册的 Collector(指标收集器);
        • 提供注册 / 注销 Collector 的方法(即实现了 Registerer 接口);
        • 同时,它还实现了 Gatherer 接口的 Gather() 方法。
      • Q1、为什么说 Registry 实现了 Registerer 接口?

        • Registerer 接口的作用是定义“注册 / 注销指标收集器”的规范,它包含 3 个核心方法:

          type Registerer interface {
              Register(Collector) error       // 注册一个收集器
              MustRegister(...Collector)     // 强制注册(失败则 panic)
              Unregister(Collector) bool     // 注销一个收集器
          }
        • 而 Registry 结构体(Prometheus 的核心组件)中,实实在在地实现了这 3 个方法。

          • Registry 有一个 Register 方法,参数是 Collector,返回值是 error,和接口要求完全一致;
          • 有 MustRegister 方法,参数是 ...Collector,无返回值,符合接口定义;
          • 有 Unregister 方法,参数是 Collector,返回值是 bool,也符合接口定义。
        • 这种“结构体的方法完全匹配接口方法”的情况,在 Go 中就叫做“Registry 实现了 Registerer 接口”。
      • Q2、这样实现的意义是啥?

        • 这意味着我们可以把 Registry 实例赋值给 Registerer 类型的变量,比如:
var reg prometheus.Registerer  // 声明一个 Registerer 类型的变量
reg = prometheus.NewRegistry() // 赋值一个 Registry 实例(合法,因为 Registry 实现了 Registerer)
        • 这样做的好处是“解耦”:比如你写一个函数时,参数可以声明为 Registerer 类型,而不是具体的 Registry 类型,这样调用时既可以传 Registry 实例,也可以传其他实现了 Registerer 接口的自定义类型(比如测试用的 mock 类型)。
      • Q3、为什么说 Registry 实现了 Gatherer 接口?

        • Gatherer 接口的作用是定义“收集指标数据”的规范,它只包含 1 个核心方法:

          type Gatherer interface {
              Gather() ([]*dto.MetricFamily, error)  // 收集所有指标,返回整理后的数据
          }
        • 这个 Gather() 方法的功能是:遍历所有已注册的 Collector(指标收集器),调用它们的 Collect 方法获取实时指标值,最后把这些值整理成 Prometheus 服务器能识别的格式(MetricFamily)。

        • 而 Registry 结构体中,也实实在在地实现了 Gather() 方法:Registry 的 Gather() 方法会内部遍历自己管理的所有 Collector,收集它们的指标数据,处理格式后返回 []*dto.MetricFamily 和可能的错误,完全符合 Gatherer 接口的要求。

        • 因此,Registry 也实现了 Gatherer 接口。
      • Q4、这样实现的意义是啥?

        • 同样可以把 Registry 实例当作 Gatherer 类型使用,比如 Prometheus 暴露指标的 promhttp.HandlerFor 函数,参数就需要一个 Gatherer:
// 因为 Registry 实现了 Gatherer,所以可以直接传 Registry 实例
handler := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
        • 这里的 registry 是 Registry 实例,但因为它实现了 Gatherer,所以能被 HandlerFor 接受,用于提供指标数据。
      • Q5、Registry 实现这两个接口的好处?

        • Registry 是 Prometheus 中管理指标全生命周期的核心组件:

          • 它通过实现 Registerer 接口,拥有了“注册 / 注销收集器”的能力(管理指标的“存在”);

          • 它通过实现 Gatherer 接口,拥有了“收集所有指标数据”的能力(管理指标的“数据”)。

          • 这种“一个结构体实现多个接口”的设计,让Registry成为了一个“多功能组件”:既负责指标的注册管理,又负责指标的数据收集,同时还能通过接口类型被灵活使用(比如替换成测试用的 mock 实现)。

四、汇集器决定接受哪些采集样本

汇集器通过 Registry 来决定接受哪些采集样本。在注册指标时,Registry 会记录所有已注册的指标信息。当调用 Gatherer 的 Gather 方法时,Registry 会遍历所有已注册的 Collector,调用它们的 Collect 方法来收集指标。

4.1、Collector 接口

Collector 是一个接口,定义了收集指标的方法。每个自定义的指标类型都需要实现 Collector 接口。

// Collector is the interface implemented by anything that can be used by
// Prometheus to collect metrics.
type Collector interface {
    Describe(chan<- *Desc)
    Collect(chan<- Metric)
}
    • Collector 接口包含两个方法:

      • Describe:用于描述指标的元信息,如指标名称、帮助信息等。

      • Collect:用于收集指标的实际值。

4.2、决定接受哪些样本

当调用 Gatherer (百度翻译:[ˈɡæðərə(r)] 美[ˈɡæðərər]) 的 Gather(百度翻译:[ˈɡæðə(r)] 美[ˈɡæðər]) 方法时,Registry(百度翻译:[ˈredʒɪstri] 美[ˈredʒɪstri]) 会遍历所有已注册的 Collector,调用它们的 Collect 方法。Collect 方法会将收集到的指标样本发送到一个 Metric 通道中。汇集器会从这个通道中接收样本,并将其转化为 protobuf 消息对象。

https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L412

// Gather implements Gatherer.
// Gather 方法实现了 Gatherer 接口,用于收集注册的指标并将其转换为 MetricFamily 切片
func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
	// 以只读模式锁定注册表,防止在收集指标时其他 goroutine 修改注册表
	r.mtx.RLock()

	// 检查注册表中是否没有已注册的检查型收集器和未检查型收集器
	if len(r.collectorsByID) == 0 && len(r.uncheckedCollectors) == 0 {
		// 如果没有收集器,这是一个快速路径,直接返回 nil 和 nil 错误
		r.mtx.RUnlock()
		return nil, nil
	}

	// 定义所需的变量
	var (
		// 创建一个有缓冲的通道,用于接收检查型收集器收集的指标
		checkedMetricChan   = make(chan Metric, capMetricChan)
		// 创建一个有缓冲的通道,用于接收未检查型收集器收集的指标
		uncheckedMetricChan = make(chan Metric, capMetricChan)
		// 用于记录已处理指标的哈希值,避免重复处理
		metricHashes        = map[uint64]struct{}{}
		// 用于等待所有收集器完成收集工作
		wg                  sync.WaitGroup
		// 用于存储收集过程中发生的错误
		errs                MultiError          // The collected errors to return in the end.
		// 仅在启用严格检查时使用,用于存储已注册描述符的 ID
		registeredDescIDs   map[uint64]struct{} // Only used for pedantic checks
	)

	// 计算需要启动的 goroutine 数量,即检查型收集器和未检查型收集器的总数
	goroutineBudget := len(r.collectorsByID) + len(r.uncheckedCollectors)
	// 创建一个映射,用于按名称存储 MetricFamily,初始容量为已注册指标的数量
	metricFamiliesByName := make(map[string]*dto.MetricFamily, len(r.dimHashesByName))
	// 创建一个有缓冲的通道,用于传递检查型收集器
	checkedCollectors := make(chan Collector, len(r.collectorsByID))
	// 创建一个有缓冲的通道,用于传递未检查型收集器
	uncheckedCollectors := make(chan Collector, len(r.uncheckedCollectors))
	// 将所有检查型收集器放入 checkedCollectors 通道
	for _, collector := range r.collectorsByID {
		checkedCollectors <- collector
	}
	// 将所有未检查型收集器放入 uncheckedCollectors 通道
	for _, collector := range r.uncheckedCollectors {
		uncheckedCollectors <- collector
	}
	// 如果启用了严格检查,需要复制已注册描述符的 ID 到 registeredDescIDs 中
	if r.pedanticChecksEnabled {
		registeredDescIDs = make(map[uint64]struct{}, len(r.descIDs))
		for id := range r.descIDs {
			registeredDescIDs[id] = struct{}{}
		}
	}
	// 释放只读锁,允许其他 goroutine 修改注册表
	r.mtx.RUnlock()

	// 增加等待组的计数,以便等待所有收集器完成工作
	wg.Add(goroutineBudget)

	// 定义一个收集工作函数,用于从通道中获取收集器并调用其 Collect 方法
	collectWorker := func() {
		for {
			select {
			case collector := <-checkedCollectors:
				// 从 checkedCollectors 通道获取检查型收集器并调用其 Collect 方法
				collector.Collect(checkedMetricChan)
			case collector := <-uncheckedCollectors:
				// 从 uncheckedCollectors 通道获取未检查型收集器并调用其 Collect 方法
				collector.Collect(uncheckedMetricChan)
			default:
				// 如果两个通道都没有收集器了,退出循环
				return
			}
			// 完成一个收集器的工作,减少等待组的计数
			wg.Done()
		}
	}

	// 立即启动第一个工作 goroutine,确保至少有一个工作 goroutine 在运行
	go collectWorker()
	// 减少 goroutine 预算
	goroutineBudget--

	// 启动一个 goroutine 来等待所有收集器完成工作,然后关闭两个指标通道
	go func() {
		wg.Wait()
		close(checkedMetricChan)
		close(uncheckedMetricChan)
	}()

	// 定义一个延迟执行的函数,用于在函数返回时排空两个指标通道
	defer func() {
		if checkedMetricChan != nil {
			for range checkedMetricChan {
			}
		}
		if uncheckedMetricChan != nil {
			for range uncheckedMetricChan {
			}
		}
	}()

	// 复制通道引用,以便在后续的 select 语句中可以将其置为 nil
	cmc := checkedMetricChan
	umc := uncheckedMetricChan

	// 开始一个无限循环,从通道中接收指标并处理
	for {
		select {
		case metric, ok := <-cmc:
			// 从 cmc 通道接收检查型指标
			if !ok {
				// 如果通道已关闭,将 cmc 置为 nil
				cmc = nil
				break
			}
			// 处理接收到的指标,并将可能的错误添加到 errs 中
			errs.Append(processMetric(
				metric, metricFamiliesByName,
				metricHashes,
				registeredDescIDs,
			))
		case metric, ok := <-umc:
			// 从 umc 通道接收未检查型指标
			if !ok {
				// 如果通道已关闭,将 umc 置为 nil
				umc = nil
				break
			}
			// 处理接收到的指标,并将可能的错误添加到 errs 中
			errs.Append(processMetric(
				metric, metricFamiliesByName,
				metricHashes,
				nil,
			))
		default:
			// 如果没有指标可接收
			if goroutineBudget <= 0 || len(checkedCollectors)+len(uncheckedCollectors) == 0 {
				// 如果 goroutine 预算用完或所有收集器都已开始工作
				// 再次尝试从通道中接收指标,没有 default 分支,防止忙等待
				select {
				case metric, ok := <-cmc:
					if !ok {
						cmc = nil
						break
					}
					errs.Append(processMetric(
						metric, metricFamiliesByName,
						metricHashes,
						registeredDescIDs,
					))
				case metric, ok := <-umc:
					if !ok {
						umc = nil
						break
					}
					errs.Append(processMetric(
						metric, metricFamiliesByName,
						metricHashes,
						nil,
					))
				}
				break
			}
			// 如果还有 goroutine 预算,启动更多的工作 goroutine
			go collectWorker()
			// 减少 goroutine 预算
			goroutineBudget--
			// 让出 CPU 时间片,允许其他 goroutine 运行
			runtime.Gosched()
		}
		// 当两个指标通道都关闭并排空后,退出循环
		if cmc == nil && umc == nil {
			break
		}
	}
	// 对收集到的 MetricFamily 进行规范化处理,并返回可能的错误
	return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap()
}

这段代码的主要功能是从注册表中收集所有已注册收集器的指标,并将其转换为 MetricFamily 切片。它通过多个通道和 goroutine 并行收集指标,以提高收集效率。同时,它还处理了可能出现的错误,并在必要时进行严格检查。最后,它对收集到的指标进行规范化处理,确保返回的 MetricFamily 切片是有效的。

关键点说明:

在 Prometheus 的 client_golang 库中,收集器(Collector)是用于收集指标数据的核心组件

已注册的检查型收集器(checked collectors)和未检查型收集器(unchecked collectors)是两种不同类型的收集器,它们在使用和验证方面存在一些差异,下面为你详细介绍: 

4.2.1、检查型收集器(Checked Collectors)

概念:检查型收集器是经过严格注册和验证的收集器。当你向注册表(Registry)注册一个检查型收集器时,注册表会对收集器的描述符(Descriptor)进行检查,确保其符合一定的规则,比如描述符的名称、帮助信息等是否合法。

用途:通常用于收集那些需要严格验证和标准化的指标,例如系统级别的指标、业务关键指标等。这样可以保证收集到的指标数据具有较高的质量和一致性。

代码体现:在 Registry 结构体中,collectorsByID 存储的就是检查型收集器。这些收集器在注册时会经过严格的验证过程。

// 示例:注册检查型收集器
r := prometheus.NewRegistry()
counter := prometheus.NewCounter(prometheus.CounterOpts{
    Name: "my_counter",
    Help: "This is a sample counter",
})
r.MustRegister(counter) // 这里 counter 就是一个检查型收集器

4.2.2、未检查型收集器(Unchecked Collectors)

概念:未检查型收集器是一种相对宽松的收集器类型。当你向注册表注册未检查型收集器时,注册表不会对其描述符进行严格的检查。这意味着你可以使用一些自定义的、可能不符合标准规则的描述符。
用途:适用于一些临时的、实验性的或者特殊需求的指标收集场景,你可能不希望受到严格的验证限制。
代码体现:在 Registry 结构体中,uncheckedCollectors 存储的就是未检查型收集器。

// 示例:注册未检查型收集器
r := prometheus.NewRegistry()
customCollector := &MyCustomCollector{}
r.Register(customCollector) // 如果 MyCustomCollector 没有经过严格验证,它就是一个未检查型收集器 

五、接收和处理样本的具体过程

5.1、接收样本

https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L412

在 Registry 的 Gather() 方法中,会为每个已注册的 Collector 启动独立的 goroutine 并发调用其 Collect() 方法,采集到的 Metric 通过 metricCh 通道发送。主流程会等待所有采集完成后关闭通道,保证所有样本被收集和处理。整个采集过程通过读锁保护,防止采集期间注册表发生变更,确保数据一致性。

5.2、转化为 protobuf 消息对象

从 https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L32 看到 dto.MetricFamily 和 dto.Metric 是 Protobuf 生成的结构体,定义在 https://github.com/prometheus/client_model/blob/v0.6.2/go/metrics.pb.go#L926

type MetricFamily struct {
    state         protoimpl.MessageState  // 消息状态(用于协议缓冲区内部管理)
    sizeCache     protoimpl.SizeCache      // 大小缓存(优化序列化性能)
    unknownFields protoimpl.UnknownFields // 未知字段(用于向前兼容)

    // 指标族名称(例如:http_requests_total)
    Name   *string     `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
    // 指标族帮助文本(描述指标含义)
    Help   *string     `protobuf:"bytes,2,opt,name=help" json:"help,omitempty"`
    // 指标类型(枚举值:COUNTER、GAUGE、HISTOGRAM等)
    Type   *MetricType `protobuf:"varint,3,opt,name=type,enum=io.prometheus.client.MetricType" json:"type,omitempty"`
    // 指标样本列表(包含多个同类型指标实例)
    Metric []*Metric   `protobuf:"bytes,4,rep,name=metric" json:"metric,omitempty"`
    // 指标单位(例如:seconds、bytes等)
    Unit   *string     `protobuf:"bytes,5,opt,name=unit" json:"unit,omitempty"`
}

https://github.com/prometheus/client_model/blob/v0.6.2/go/metrics.pb.go#L831

// Metric 结构体用于表示 Prometheus 中的一个指标,包含标签、不同类型的指标值(如 Gauge、Counter 等)以及时间戳信息
type Metric struct {
    //  protoimpl.MessageState 是 protobuf 生成代码中的一个内部类型,用于管理消息的状态
    // 它记录了消息的序列化和反序列化状态等信息
    state         protoimpl.MessageState
    //  protoimpl.SizeCache 是 protobuf 生成代码中的一个内部类型,用于缓存消息的大小
    // 避免每次计算消息大小时都进行复杂的计算,提高性能
    sizeCache     protoimpl.SizeCache
    //  protoimpl.UnknownFields 是 protobuf 生成代码中的一个内部类型,用于存储未知的字段
    // 当反序列化时遇到未知的字段,会将这些字段存储在这里,以保持兼容性
    unknownFields protoimpl.UnknownFields

    // Label 是一个切片,包含了该指标的所有标签对
    // 每个标签对由一个名称和一个值组成,用于标识和区分不同的指标实例
    // protobuf:"bytes,1,rep,name=label" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、重复规则和名称
    // json:"label,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Label       []*LabelPair `protobuf:"bytes,1,rep,name=label" json:"label,omitempty"`
    // Gauge 是一个指针,指向一个 Gauge 类型的结构体
    // Gauge 类型用于表示可以任意上下波动的值,如当前的内存使用量、并发连接数等
    // protobuf:"bytes,2,opt,name=gauge" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"gauge,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Gauge       *Gauge       `protobuf:"bytes,2,opt,name=gauge" json:"gauge,omitempty"`
    // Counter 是一个指针,指向一个 Counter 类型的结构体
    // Counter 类型用于表示单调递增的值,如请求总数、错误总数等
    // protobuf:"bytes,3,opt,name=counter" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"counter,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Counter     *Counter     `protobuf:"bytes,3,opt,name=counter" json:"counter,omitempty"`
    // Summary 是一个指针,指向一个 Summary 类型的结构体
    // Summary 类型用于统计样本的分布情况,如请求的响应时间分布
    // protobuf:"bytes,4,opt,name=summary" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"summary,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Summary     *Summary     `protobuf:"bytes,4,opt,name=summary" json:"summary,omitempty"`
    // Untyped 是一个指针,指向一个 Untyped 类型的结构体
    // Untyped 类型用于表示没有明确类型的指标值
    // protobuf:"bytes,5,opt,name=untyped" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"untyped,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Untyped     *Untyped     `protobuf:"bytes,5,opt,name=untyped" json:"untyped,omitempty"`
    // Histogram 是一个指针,指向一个 Histogram 类型的结构体
    // Histogram 类型用于统计样本的分布情况,与 Summary 不同的是,Histogram 可以在服务器端进行聚合
    // protobuf:"bytes,7,opt,name=histogram" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"histogram,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    Histogram   *Histogram   `protobuf:"bytes,7,opt,name=histogram" json:"histogram,omitempty"`
    // TimestampMs 是一个指针,指向一个 int64 类型的值
    // 该值表示该指标的时间戳,单位为毫秒
    // protobuf:"varint,6,opt,name=timestamp_ms,json=timestampMs" 是 protobuf 的标签,指定了该字段在序列化时的类型、编号、可选规则和名称
    // json:"timestamp_ms,omitempty" 是 JSON 标签,指定了该字段在 JSON 序列化时的名称,omitempty 表示如果该字段为空则不包含在 JSON 中
    TimestampMs *int64       `protobuf:"varint,6,opt,name=timestamp_ms,json=timestampMs" json:"timestamp_ms,omitempty"`
}

在 https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L412 的 Gather() 方法中,采集到的 Metric 会被聚合到 MetricFamily,最后返回 []*dto.MetricFamily。

https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/promhttp/http.go#L108 HandlerFor 相关方法会将 []*dto.MetricFamily 通过expfmt.Encoder(https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/promhttp/http.go#L217) 编码为 Protobuf 或 OpenMetrics 格式输出。

var enc expfmt.Encoder
if opts.EnableOpenMetricsTextCreatedSamples {
	enc = expfmt.NewEncoder(w, contentType, expfmt.WithCreatedLines())
} else {
	enc = expfmt.NewEncoder(w, contentType)
}

注意,关于序列化:

      • expfmt.MetricFamilyToDelimited(已废弃)和 proto.Marshal 都可以将 dto.MetricFamily 转为字节切片。
      • 推荐用 expfmt.Encoder,它会根据 content-type 自动选择 Protobuf 或文本格式。

收集到的 Metric 会被聚合到 dto.MetricFamily 对象中,最终形成 []*dto.MetricFamily。在 HTTP Handler 中,会通过 expfmt.Encoder 根据客户端请求的格式(如 Protobuf 或 OpenMetrics)将这些 MetricFamily 编码为字节流输出。通常无需手动逐步构造 dto.MetricFamily,除非在自定义 Collector 或测试场景下。

https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/promhttp/http.go#L108

// HandlerFor 为提供的 Gatherer 返回一个未经过仪表化(即没有添加监控指标)的 HTTP 处理器(http.Handler)。
// 该处理器的行为由提供的 HandlerOpts 定义。
// 因此,HandlerFor 可用于为自定义的 Gatherer 创建 HTTP 处理器,使用非默认的 HandlerOpts,
// 以及应用自定义的(或不使用)仪表化。
// 若要应用与 Handler 函数相同类型的仪表化,请使用 InstrumentMetricHandler 函数。
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
    // 调用 HandlerForTransactional 函数,将传入的 Gatherer 转换为支持事务操作的 Gatherer
    return HandlerForTransactional(prometheus.ToTransactionalGatherer(reg), opts)
}

这段注释和代码描述了 HandlerFor 函数的用途和工作方式。它主要用于创建一个 HTTP 处理器,用于处理来自自定义 Gatherer 的指标收集请求,并且可以自定义处理器的选项。函数内部调用了 HandlerForTransactional 函数,将传入的 Gatherer 转换为支持事务操作的 Gatherer。

https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/promhttp/http.go#L248

// 在 HandlerForTransactional 中,采集到的 mfs 是 []*dto.MetricFamily
for _, mf := range mfs {
	if handleError(enc.Encode(mf)) {
		return
	}
}

5.3、排序和组合

Prometheus 的汇集器在收集所有样本并转化为 dto.MetricFamily 后,会对这些 MetricFamily 进行排序,排序规则就是按指标名称的字典序。这个排序的目的是保证每次抓取的指标顺序一致,方便 Prometheus 服务端做去重和对比。

虽然MultiTRegistry.Gather 也显式做了排序,用的是 sort.Slice,但是我一时没有找到源码实现的部分,只在 Gatherer 接口的注释中,有明确写到:https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L140 

// Gatherer 是一个接口,用于将注册中心中收集到的指标聚合为多个 MetricFamily(指标族)。
// Gatherer 接口与 Registerer 接口有着相同的通用含义,即对操作的结果有一定的预期和约束。
type Gatherer interface {
    // Gather 方法会调用已注册的 Collectors(指标收集器)的 Collect 方法,
    // 然后将收集到的指标聚合为一个按字典序排序的、具有唯一名称的 MetricFamily 协议缓冲区消息(protobuf)切片。
    // Gather 方法会确保返回的切片是有效的且自洽的,以便用于有效的指标展示。
    // 作为对 metric.Desc 严格一致性要求的一个例外,Gather 方法允许同一指标族下的不同指标具有不同的标签名集合。
    //
    // 即使在收集过程中发生错误,Gather 方法也会尝试收集尽可能多的指标。
    // 因此,如果返回一个非空的错误,返回的 MetricFamily 切片可能为 nil(如果发生了严重错误,导致无法进行有意义的指标收集),
    // 或者包含一些 MetricFamily 协议缓冲区消息,其中一些可能是不完整的,还有一些可能完全缺失。
    // 返回的错误(可能是一个 MultiError,即包含多个错误的错误对象)会详细说明具体情况。
    // 请注意,这主要用于调试目的。如果收集到的协议缓冲区消息要用于实际监控中的指标展示,
    // 那么在返回的错误非空的情况下,通常最好不要展示不完整的结果,而是忽略返回的 MetricFamily 协议缓冲区消息。
    Gather() ([]*dto.MetricFamily, error)
} 

Maybe,在这里实现了排序:https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/internal/metric.go#L85,它的作用就是把 map 转成 slice,并按指标名称字典序排序。

// NormalizeMetricFamilies 返回一个 MetricFamily 切片,其中空的 MetricFamily 会被移除,
// 剩余的 MetricFamily 会在切片中按名称排序,并且每个 MetricFamily 内包含的 Metrics 也会被排序。
func NormalizeMetricFamilies(metricFamiliesByName map[string]*dto.MetricFamily) []*dto.MetricFamily {
    // 遍历 metricFamiliesByName 这个映射中的每个 MetricFamily
    for _, mf := range metricFamiliesByName {
        // 对每个 MetricFamily 中的 Metrics 进行排序。
        // MetricSorter 应该是一个实现了 sort.Interface 接口的类型,
        // 用于定义 Metrics 的排序规则
        sort.Sort(MetricSorter(mf.Metric))
    }
    // 创建一个字符串切片 names,初始容量为 metricFamiliesByName 的长度,
    // 用于存储非空 MetricFamily 的名称
    names := make([]string, 0, len(metricFamiliesByName))
    // 再次遍历 metricFamiliesByName 映射
    for name, mf := range metricFamiliesByName {
        // 检查当前的 MetricFamily 中是否包含 Metrics
        if len(mf.Metric) > 0 {
            // 如果包含 Metrics,则将该 MetricFamily 的名称添加到 names 切片中
            names = append(names, name)
        }
    }
    // 对存储非空 MetricFamily 名称的 names 切片进行排序,排序规则为字典序
    sort.Strings(names)
    // 创建一个 *dto.MetricFamily 类型的切片 result,初始容量为 names 切片的长度,
    // 用于存储最终排序好的非空 MetricFamily
    result := make([]*dto.MetricFamily, 0, len(names))
    // 遍历排序好的 names 切片
    for _, name := range names {
        // 根据名称从 metricFamiliesByName 映射中获取对应的 MetricFamily,
        // 并将其添加到 result 切片中
        result = append(result, metricFamiliesByName[name])
    }
    // 返回最终排序好的非空 MetricFamily 切片
    return result
}
这段代码的主要功能是对 metricFamiliesByName 映射中的 MetricFamily 进行规范化处理。具体步骤包括对每个 MetricFamily 内的 Metrics 进行排序,移除空的 MetricFamily,对剩余的 MetricFamily 按名称进行排序,最后返回排序好的 MetricFamily 切片。

       https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/  在 Registry 的 Gather 方法中,先将所有采集到的指标聚合到 map,再通过 internal.NormalizeMetricFamilies 函数将其转为 []dto.MetricFamily,并按指标名称字典序排序,保证每次抓取顺序一致。MultiTRegistry 也会对合并后的 MetricFamily 进行排序。这一行为在接口文档中有明确承诺,实际实现则通过排序辅助函数完成。 

posted @ 2025-07-24 11:21  左扬  阅读(35)  评论(0)    收藏  举报