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接口收集指标?
-
-
步骤 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)
-
-
-
- Registry 是 Prometheus 中的具体组件(结构体),它的核心功能是:
- 管理所有注册的 Collector(指标收集器);
- 提供注册 / 注销 Collector 的方法(即实现了 Registerer 接口);
- 同时,它还实现了 Gatherer 接口的 Gather() 方法。
-
-
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 是 Prometheus 中的具体组件(结构体),它的核心功能是:
-
-
-
-
- 这意味着我们可以把 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 进行排序。这一行为在接口文档中有明确承诺,实际实现则通过排序辅助函数完成。

浙公网安备 33010602011771号