Prometheus源码专题【左扬精讲】—— Prometheus Exporter 定制化开发:架构设计篇​

Prometheus源码专题【左扬精讲】—— Prometheus Exporter 定制化开发:架构设计篇

        在 Prometheus 监控体系中,Exporter 作为指标采集与暴露的核心组件,其架构设计直接影响监控系统的灵活性、可扩展性和可靠性。本文将以 Go 语言客户端为例,从架构设计层面解析 Prometheus Exporter 的核心组件与工作原理,重点探讨汇集器(Gatherer)、采集器(Collector)的协作模式,以及多汇集器架构的设计思路。

一、Exporter 核心架构概览

Prometheus Exporter 的核心职责是采集目标系统的指标数据通过 HTTP 接口暴露给 Prometheus Server。在 Go 语言客户端(https://github.com/prometheus/client_golang)中,这一过程通过一套清晰的组件分层实现:

    • 采集器(Collector):指标数据的生产者,负责从特定数据源(如应用内状态、外部 API、日志文件等)采集原始指标,并按照 Prometheus 数据模型进行格式化。​
    • 汇集器(Gatherer):指标数据的汇总者,接收来自多个 Collector 的指标样本,进行校验、去重、排序后,转换为 Prometheus 协议兼容的 protobuf 格式。​
    • HTTP 服务:指标数据的暴露者,将 Gatherer 处理后的 protobuf 数据编码为文本或二进制格式,通过/metrics端点响应 Prometheus 的拉取请求。

三者的协作流程可概括为:Collector 生成样本 → 写入 Gatherer 的通道 → Gatherer 汇总处理 → 转换为 HTTP 响应。这种分层设计使数据采集与数据处理解耦,为定制化开发提供了灵活的扩展点。

二、单个汇集器构成的 Exporter

 单个 Gatherer 架构是 Exporter 的基础形态,适用于大多数简单场景。其核心特点是通过一个 Gatherer 实例管理所有 Collector,形成 "单调度中心" 模式。

1. 架构组成
  • Registry:作为 Collector 的注册中心,提供MustRegister等方法管理 Collector 实例,确保 Gatherer 能统一发现所有采集器。​
  • 默认 Gatherer:Registry 默认实现 Gatherer 接口,当调用handler.Handler()时,底层通过 Registry 的Gather方法触发指标汇总。
2. 工作流程
  1. 开发者定义多个 Collector,分别采集不同类型的指标(如系统 CPU、内存、业务接口 QPS 等)。​
  2. 通过prometheus.MustRegister将所有 Collector 注册到默认 Registry。​
  3. 启动 HTTP 服务,当 Prometheus 请求/metrics时,Registry(作为 Gatherer)调用所有 Collector 的Collect方法,收集指标样本。​
  4. Gatherer 对样本进行校验(如指标名格式、标签合法性)、去重(相同指标 + 标签组合仅保留最新值)和排序,生成 protobuf 格式的MetricFamily列表。​
  5. HTTP 处理器将 protobuf 数据编码为文本格式(默认),返回给 Prometheus。
这种架构的优势在于简单直观,适合指标来源单一、数量较少的场景(如单个应用的监控 Exporter)。

三、多个汇集器构成的 Exporter

当监控场景复杂化(如需要从多个独立系统采集指标、或部分指标需要特殊处理逻辑)时,多 Gatherer 架构成为更优选择。其核心设计是prometheus.Gatherers(复数形式)聚合多个独立 Gatherer 实例,实现指标的分层汇总。

1. 架构特点

    1. 独立 Gatherer 实例:每个 Gatherer 可关联不同的 Collector 集合,例如:​
    2. Gatherer A:负责采集主机硬件指标(CPU、磁盘 IO)。​
    3. Gatherer B:负责采集应用业务指标(订单量、支付成功率)。​
    4. Gatherer C:负责采集第三方服务调用指标(API 响应时间、错误率)。​
    5. Gatherers 集合:通过prometheus.Gatherers{A, B, C}将多个 Gatherer 打包,其Gather方法会按顺序调用每个 Gatherer 的Gather方法,合并所有指标结果。

2. 设计优势 

  1. 隔离性:不同 Gatherer 管理的指标相互独立,某类指标采集失败(如第三方 API 超时)不会影响其他 Gatherer 的正常工作。​
  2. 扩展性:可根据业务需求动态增减 Gatherer 实例,无需修改已有采集逻辑。​
  3. 定制化处理:针对特殊指标(如敏感数据),可在独立 Gatherer 中实现专属的过滤或转换逻辑。​

例如,在微服务架构中,一个 Exporter 可通过多 Gatherer 分别采集服务自身指标、依赖的数据库指标和消息队列指标,各 Gatherer 可独立配置超时时间和重试策略。

四、Registry、Gatherers 与 GathererFunc 解析

这三个概念是理解 Exporter 架构的核心,它们从不同层面支撑了指标的注册、汇总与扩展能力。

4.1、Registry:Collector 的管理中枢

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

核心作用:维护 Collector 的生命周期,提供注册、反注册接口,确保 Gatherer 能高效发现所有采集器。​
实现特性:

  • 内置DefaultRegisterer作为全局默认注册中心,简化简单场景的开发。​
  • 支持NewRegistry创建独立 Registry,实现多组 Collector 的隔离管理(例如生产环境与测试环境的指标分离)。​
  • 当注册重复的 Collector(同一指标名且标签规则冲突)时,会触发 panic,避免指标混乱。

Registry 的设计体现了依赖注入思想,通过统一注册机制解耦 Gatherer 与 Collector 的直接关联,使 Gatherer 无需关心具体的采集逻辑。

4.2、Gatherers:多 Gatherer 的聚合器

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

本质:Gatherers是一个实现了 Gatherer 接口的切片类型,可以将多个不同来源的指标聚合在一起,统一收集。

4.2.1、Gatherer 接口定义

// Gatherer is the interface for the part of a registry in charge of gathering
// the collected metrics into a number of MetricFamilies. The Gatherer interface
// comes with the same general implication as described for the Registerer
// interface.
type Gatherer interface {
	// Gather calls the Collect method of the registered Collectors and then
	// gathers the collected metrics into a lexicographically sorted slice
	// of uniquely named MetricFamily protobufs. Gather ensures that the
	// returned slice is valid and self-consistent so that it can be used
	// for valid exposition. As an exception to the strict consistency
	// requirements described for metric.Desc, Gather will tolerate
	// different sets of label names for metrics of the same metric family.
	//
	// Even if an error occurs, Gather attempts to gather as many metrics as
	// possible. Hence, if a non-nil error is returned, the returned
	// MetricFamily slice could be nil (in case of a fatal error that
	// prevented any meaningful metric collection) or contain a number of
	// MetricFamily protobufs, some of which might be incomplete, and some
	// might be missing altogether. The returned error (which might be a
	// MultiError) explains the details. Note that this is mostly useful for
	// debugging purposes. If the gathered protobufs are to be used for
	// exposition in actual monitoring, it is almost always better to not
	// expose an incomplete result and instead disregard the returned
	// MetricFamily protobufs in case the returned error is non-nil.
	Gather() ([]*dto.MetricFamily, error)
}

任何实现了 Gather() 方法的类型都可以作为 Gatherer。

4.2.2、Gatherers 类型定义

Gatherers 的定义如下(https://github.com/prometheus/client_golang/blob/v1.22.0/prometheus/registry.go#L743)

// Gatherers is a slice of Gatherer instances that implements the Gatherer
// interface itself. Its Gather method calls Gather on all Gatherers in the
// slice in order and returns the merged results. Errors returned from the
// Gather calls are all returned in a flattened MultiError. Duplicate and
// inconsistent Metrics are skipped (first occurrence in slice order wins) and
// reported in the returned error.
//
// Gatherers can be used to merge the Gather results from multiple
// Registries. It also provides a way to directly inject existing MetricFamily
// protobufs into the gathering by creating a custom Gatherer with a Gather
// method that simply returns the existing MetricFamily protobufs. Note that no
// registration is involved (in contrast to Collector registration), so
// obviously registration-time checks cannot happen. Any inconsistencies between
// the gathered MetricFamilies are reported as errors by the Gather method, and
// inconsistent Metrics are dropped. Invalid parts of the MetricFamilies
// (e.g. syntactically invalid metric or label names) will go undetected.
// Gatherers 是一个 Gatherer 实例的切片,它自身实现了 Gatherer 接口。
// 其 Gather 方法会按顺序调用切片中所有 Gatherer 的 Gather 方法,并返回合并后的结果。
// 从 Gather 调用返回的错误将以扁平化的 MultiError 形式返回。
// 重复和不一致的指标将被跳过(切片顺序中的首次出现优先),并在返回的错误中报告。
//
// Gatherers 可用于合并多个注册表的 Gather 结果。
// 它还提供了一种方法,通过创建一个自定义的 Gatherer,其 Gather 方法直接返回现有的 MetricFamily 协议缓冲区,
// 从而将现有的 MetricFamily 协议缓冲区直接注入到收集过程中。
// 请注意,这里不涉及注册(与 Collector 注册不同),因此显然无法进行注册时的检查。
// Gather 方法会将收集到的 MetricFamily 之间的任何不一致情况报告为错误,并丢弃不一致的指标。
// MetricFamily 的无效部分(例如语法无效的指标或标签名称)将无法被检测到。
type Gatherers []Gatherer

// Gather implements Gatherer.
// Gather 方法实现了 Gatherer 接口
func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) {
    // 用于存储按名称分组的 MetricFamily
    var (
        metricFamiliesByName = map[string]*dto.MetricFamily{}
        // 用于存储指标的哈希值,用于去重
        metricHashes         = map[uint64]struct{}{}
        // 收集所有错误,最终返回
        errs                 MultiError 
    )

    // 遍历所有的 Gatherer
    for i, g := range gs {
        // 调用当前 Gatherer 的 Gather 方法获取指标族
        mfs, err := g.Gather()
        if err != nil {
            multiErr := MultiError{}
            // 检查错误是否为 MultiError 类型
            if errors.As(err, &multiErr) {
                // 如果是 MultiError 类型,将其中的每个错误添加到最终错误集合中
                for _, err := range multiErr {
                    errs = append(errs, fmt.Errorf("[from Gatherer #%d] %w", i+1, err))
                }
            } else {
                // 如果不是 MultiError 类型,直接将错误添加到最终错误集合中
                errs = append(errs, fmt.Errorf("[from Gatherer #%d] %w", i+1, err))
            }
        }
        // 遍历当前 Gatherer 返回的所有指标族
        for _, mf := range mfs {
            // 检查当前指标族名称是否已经存在于 metricFamiliesByName 中
            existingMF, exists := metricFamiliesByName[mf.GetName()]
            if exists {
                // 如果帮助信息不一致,添加错误信息并跳过当前指标族
                if existingMF.GetHelp() != mf.GetHelp() {
                    errs = append(errs, fmt.Errorf(
                        "gathered metric family %s has help %q but should have %q",
                        mf.GetName(), mf.GetHelp(), existingMF.GetHelp(),
                    ))
                    continue
                }
                // 如果指标类型不一致,添加错误信息并跳过当前指标族
                if existingMF.GetType() != mf.GetType() {
                    errs = append(errs, fmt.Errorf(
                        "gathered metric family %s has type %s but should have %s",
                        mf.GetName(), mf.GetType(), existingMF.GetType(),
                    ))
                    continue
                }
            } else {
                // 如果不存在,创建一个新的 MetricFamily 实例
                existingMF = &dto.MetricFamily{}
                existingMF.Name = mf.Name
                existingMF.Help = mf.Help
                existingMF.Type = mf.Type
                // 检查后缀冲突
                if err := checkSuffixCollisions(existingMF, metricFamiliesByName); err != nil {
                    errs = append(errs, err)
                    continue
                }
                // 将新的 MetricFamily 实例添加到 metricFamiliesByName 中
                metricFamiliesByName[mf.GetName()] = existingMF
            }
            // 遍历当前指标族中的所有指标
            for _, m := range mf.Metric {
                // 检查指标的一致性
                if err := checkMetricConsistency(existingMF, m, metricHashes); err != nil {
                    errs = append(errs, err)
                    continue
                }
                // 将一致的指标添加到现有的 MetricFamily 实例中
                existingMF.Metric = append(existingMF.Metric, m)
            }
        }
    }
    // 对合并后的指标族进行规范化处理,并返回最终结果和错误信息
    return internal.NormalizeMetricFamilies(metricFamiliesByName), errs.MaybeUnwrap()
}
      • Gatherers 本质上就是 []Gatherer,即多个 Gatherer 的集合。
      • 它自己也实现了 Gatherer 接口,因此可以像单个 Gatherer 一样被使用。

核心能力:其Gather方法会依次调用所有子 Gatherer 的Gather方法,合并结果并处理冲突(如不同 Gatherer 产生相同指标时,保留最后一个值)。​
典型场景:​

  • 整合来自不同注册中心的指标(如默认 Registry 与自定义 Registry)。​
  • 临时添加一次性指标采集逻辑(如按需触发的诊断指标)。​

Gatherers 的设计使 Exporter 能够横向扩展指标来源,适应复杂监控场景的需求。 

4.3、GathererFunc:函数式 Gatherer

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

// GathererFunc turns a function into a Gatherer.
// GathererFunc 类型可以将一个函数转换为 Gatherer 接口的实现。
// 通过定义这个类型,我们可以让任何符合特定签名的函数表现得像一个 Gatherer,
// 从而方便地将自定义的指标收集逻辑集成到 Prometheus 的指标收集系统中。
type GathererFunc func() ([]*dto.MetricFamily, error)

// Gather implements Gatherer.
// Gather 方法实现了 Gatherer 接口。
// 当调用 GathererFunc 实例的 Gather 方法时,实际上是调用了该实例所代表的函数。
func (gf GathererFunc) Gather() ([]*dto.MetricFamily, error) {
    // 调用 GathererFunc 类型的实例 gf 所代表的函数,并返回其结果。
    // 这个函数应该返回一个包含 *dto.MetricFamily 指针的切片和一个错误对象。
    return gf()
}

        这段代码定义了一个名为 GathererFunc 的类型,它是一个函数类型,其签名为 func() ([]*dto.MetricFamily, error)。这个类型实现了 Gatherer 接口的 Gather 方法,使得任何符合该签名的函数都可以被当作 Gatherer 使用。这样的设计提供了一种灵活的方式,允许开发者通过定义自定义函数来实现指标的收集逻辑,并将其集成到 Prometheus 的指标收集流程中。

        下面是一个使用 GathererFunc 的示例,展示了如何将自定义函数转换为 Gatherer 并集成到 Prometheus 的指标收集系统中。

package main

import (
	"fmt"
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	dto "github.com/prometheus/client_model/go"
)

// 自定义的指标收集函数
func customMetricsGatherer() ([]*dto.MetricFamily, error) {
	// 创建一个新的指标族
	mf := &dto.MetricFamily{
		Name:  prometheus.StringToPtr("custom_metric"),
		Help:  prometheus.StringToPtr("This is a custom metric"),
		Type:  dto.MetricType_GAUGE.Enum(),
		Metric: []*dto.Metric{
			{
				Gauge: &dto.Gauge{
					Value: prometheus.Float64ToPtr(42),
				},
			},
		},
	}
	return []*dto.MetricFamily{mf}, nil
}

func main() {
	// 将自定义函数转换为 Gatherer
	customGatherer := prometheus.GathererFunc(customMetricsGatherer)

	// 创建一个新的 HTTP 处理程序,用于暴露指标
	http.Handle("/metrics", promhttp.HandlerFor(customGatherer, promhttp.HandlerOpts{}))

	// 启动 HTTP 服务器
	fmt.Println("Starting server on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("Error starting server:", err)
	}
} 

代码说明:

    1. 自定义指标收集函数:customMetricsGatherer 函数返回一个包含自定义指标的 dto.MetricFamily 切片。在这个例子中,我们创建了一个名为 custom_metric 的指标,类型为 GAUGE,值为 42。
    2. 将自定义函数转换为 Gatherer:使用 prometheus.GathererFunc 将 customMetricsGatherer 函数转换为 Gatherer 接口的实现。
    3. 创建 HTTP 处理程序:使用 promhttp.HandlerFor 创建一个 HTTP 处理程序,用于暴露指标。
    4. 启动 HTTP 服务器:使用 http.ListenAndServe 启动一个 HTTP 服务器,监听 :8080 端口。

五、架构设计的核心原则

从上述组件的设计可以看出,Prometheus Exporter 的架构遵循以下原则:

    1. 接口抽象:通过 Gatherer、Collector 等接口定义核心能力,使不同实现可以灵活替换。​
    2. 职责单一:Collector 专注于采集,Gatherer 专注于汇总,HTTP 服务专注于暴露,各组件各司其职。​
    3. 可扩展性:通过 Registry、Gatherers 等机制,支持横向扩展采集能力,适应不同规模的监控需求。​
    4. 容错性:多 Gatherer 架构允许部分采集逻辑失败而不影响整体,提高系统稳定性。

这些原则为 Exporter 的定制化开发提供了清晰的指导,开发者可根据实际场景选择合适的架构模式(单 Gatherer 或多 Gatherer),并通过 Registry 和 GathererFunc 等工具灵活扩展功能。

posted @ 2025-07-16 14:36  左扬  阅读(45)  评论(0)    收藏  举报