Skip to content

Prometheus 技术秘笈(十一):深入Client - 业务埋点与Exporter实现

约 3183 字大约 11 分钟

2026-03-28

导语

Prometheus 监控生态中,Server 负责数据抓取与存储,而监控数据的源头则是 Client、Exporter 或 Pushgateway。无论是为自研业务做监控埋点,还是基于官方 Exporter 扩展监控能力,深入理解 Golang 版本 Prometheus Client 的核心实现逻辑,都是打通「指标定义-数据采集-接口暴露」全链路的关键。

本文从 Client 核心数据类型、底层实现原理入手,结合 Node Exporter 实战案例拆解 Exporter 实现逻辑,最终落地到业务埋点和自定义 Exporter 的开发实践。

一、Client 核心数据类型

Prometheus Client 定义了 4 种基础指标类型,每种类型对应特定监控场景,底层设计围绕场景做针对性优化:

1.1 核心类型定义与适用场景

类型核心特征典型场景
Gauge瞬时值,可增可减,重启后重置内存使用率、CPU 负载、磁盘容量
Counter累加值,只增不减(重启后重置)请求总数、错误总数、任务完成数
Histogram服务端计算分位数,记录区间内样本数+总和,支持自定义桶(bucket)请求耗时分布、接口响应时间分布
Summary客户端计算分位数(如 P50/P90/P99),直接输出分位值+总和核心接口的长尾延迟统计

1.2 关键差异:Histogram vs Summary

两者均用于统计数据分布,核心差异在于分位数计算的位置

Histogram(服务端分位数计算)

客户端仅记录区间样本数,由 Prometheus Server 通过 le(小于等于)桶计算分位数。

  • 优势:支持多实例聚合(如集群维度汇总);
  • 缺点:分位数精度依赖桶的设计。

示例输出:

prometheus_tdb_compaction_chunk_size_bytes_bucket{le="108"} 0  
prometheus_tdb_compaction_chunk_size_bytes_bucket{le="162"} 327  
prometheus_tdb_compaction_chunk_size_bytes_bucket{le="+Inf"} 1700  
prometheus_tdb_compaction_chunk_size_bytes_sum 427336  
prometheus_tdb_compaction_chunk_size_bytes_count 1700

Summary(客户端分位数计算)

客户端直接计算分位数并输出,无需预定义桶。

  • 优势:分位数计算更灵活,无需提前设计桶;
  • 缺点:不支持跨实例聚合。

示例输出:

prometheus-tsdb_head_gc_durationSeconds{quantile="0.5"} 0.00128098
prometheus-tsdb_head_gc_durationSeconds{quantile="0.9"} 0.001837322
prometheus-tsdb_head_gc_durationSeconds_sum 0.6309119349999994
prometheus-tsdb_head_gc_durationSeconds_count 422

二、核心实现(以 Gauge 为例)

Gauge 是最易理解的基础类型,其实现逻辑贯穿 Client 核心组件(接口、结构体、注册、暴露),是理解 Counter/Histogram/Summary 的关键。

2.1 核心接口:Metric 与 Collector

Client 核心抽象是 MetricCollector 接口,所有指标类型均基于这两个接口实现:

Metric 接口

抽象单个时序指标,定义指标描述(Desc)和数据序列化(Write)能力:

package prometheus

import "github.com/prometheus/client_model/go"

// Metric 抽象单个时序指标
type Metric interface {
    Desc() *Desc // 返回指标描述实例,包含名称、帮助信息、标签等
    Write(*dto.Metric) error // 将指标数据写入序列化结构体,供/metrics接口返回
}

Collector 接口

抽象指标收集器,定义描述(Describe)和采集(Collect)逻辑,负责将 Metric 写入通道供后续处理:

// Collector 抽象指标收集器,管理一组Metric的采集逻辑
type Collector interface {
    Collect(chan<- Metric) // 采集指标数据,写入Metric实例到通道
    Describe(chan<- *Desc) // 暴露指标元信息,写入Desc实例到通道
}

2.2 Gauge 底层结构体与方法实现

Gauge 接口内嵌 MetricCollector,并扩展值修改方法(Set/Inc/Dec 等),其唯一实现是 gauge 结构体(内嵌 selfCollector 实现自收集):

Gauge 接口定义

// Gauge 定义Gauge类型的核心方法
type Gauge interface {
    Metric
    Collector
    Set(float64)        // 直接设置值
    Inc()               // 自增1
    Dec()               // 自减1
    Add(float64)        // 累加指定值
    Sub(float64)        // 递减指定值
    SetToCurrentTime()  // 额外扩展:设置为当前时间戳(秒级)
}

gauge 结构体核心实现

gauge 结构体通过原子操作保证并发安全,核心字段与方法如下:

// gauge 是Gauge接口的唯一实现
type gauge struct {
    valBits  uint64        // 原子存储浮点值(通过math.Float64bits/Frombits转换)
    desc     *Desc         // 指标描述信息
    labelPairs []*dto.LabelPair // 标签键值对
    selfCollector           // 内嵌自收集器,实现Collector接口
}

// Add 累加指定值,基于CAS原子操作保证并发安全
func (g *gauge) Add(val float64) {
    for {
        // 原子加载当前值
        oldBits := atomic.LoadUint64(&g.valBits)
        oldVal := math.Float64frombits(oldBits)
        // 计算新值
        newVal := oldVal + val
        newBits := math.Float64bits(newVal)
        // CAS替换,成功则退出循环,失败则重试
        if atomic.CompareAndSwapUint64(&g.valBits, oldBits, newBits) {
            return
        }
    }
}

// Write 将指标值序列化到dto.Metric结构体
func (g *gauge) Write(out *dto.Metric) error {
    // 原子加载当前值
    val := math.Float64frombits(atomic.LoadUint64(&g.valBits))
    // 填充到Metric结构体(Gauge类型)
    out.Gauge = &dto.Gauge{Value: &val}
    out.Label = g.labelPairs
    return nil
}

2.3 GaugeVec:带标签的指标实现(metricMap)

单实例 Gauge 无法区分多维度(如多主机、多接口),GaugeVec 通过 metricMap 实现多标签维度的指标管理。

metricMap 核心字段

字段作用
metricskey为标签值的hash,value为metricWithLabelValues(标签值+Metric实例)
desc所有子Metric共用的描述实例
mtx读写锁(sync.RWMutex),保证并发安全
newMetric新建Metric的函数指针,适配不同指标类型(Gauge/Counter等)

核心流程(With 方法)

With 方法用于获取指定标签组合的 Gauge 实例,核心逻辑:

// getOrCreateMetricWithLabels 获取或创建指定标签的Metric实例
func (m *metricMap) getOrCreateMetricWithLabels(
    hash uint64,
    labels Labels,
    curry []curriedLabelValue,
) Metric {
    // 1. 读锁查找已有实例,减少写锁竞争
    m.mtx.RLock()
    metric, ok := m.getMetricWithHashAndLabels(hash, labels, curry)
    m.mtx.RUnlock()
    if ok {
        return metric
    }

    // 2. 写锁创建新实例(双重检查锁)
    m.mtx.Lock()
    defer m.mtx.Unlock()
    metric, ok = m.getMetricWithHashAndLabels(hash, labels, curry)
    if !ok {
        // 提取标签值
        lvs := extractLabelValues(m.desc, labels, curry)
        // 创建新Gauge实例
        metric = m.newMetric(lvs...)
        // 存入map
        m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{
            values: lvs,
            metric: metric,
        })
    }
    return metric
}

2.4 指标注册:Registerer 与 Registry

所有指标必须通过 Registerer 注册后才能被采集,Registry 是其核心实现。

Registry 核心字段

字段作用
collectorsByID按 Collector ID 存储已注册的采集器,避免重复注册
descIDs存储所有 Desc 的唯一ID,防止同一指标描述重复注册
dimHashesByName维护指标名(fqName)到维度hash的映射,防止同名不同维度的指标冲突
mtx全局锁,保证注册/采集过程的并发安全

注册流程(Register 方法)

  1. 启动 goroutine 调用 Collector 的 Describe 方法,获取所有 Desc;
  2. 校验 Desc 唯一性(ID、维度 hash),若重复则返回错误;
  3. 计算 Collector ID(所有 Desc ID 的哈希和),存入 collectorsByID
  4. 注册成功后,Collector 即可被 Gather 方法采集。

数据采集(Gather 方法)

  1. 启动多 goroutine 调用 Collector 的 Collect 方法,获取所有 Metric;
  2. 将 Metric 序列化为 dto.MetricFamily,供 /metrics 接口返回;
  3. 动态控制 goroutine 数量(默认最大 10),平衡并发效率与资源占用。

2.5 指标暴露:Handler 与 /metrics 接口

Client 通过 promhttp.Handler 暴露 /metrics 接口,核心逻辑包含基础监控限流/超时控制

1. 接口基础监控(InstrumentMetricHandler)

/metrics 接口添加自身监控(请求总数、正在处理的请求数):

package main

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

// InstrumentMetricHandler 为/metrics接口添加监控
func InstrumentMetricHandler(reg prometheus.Registerer, handler http.Handler) http.Handler {
    // 1. 定义请求总数Counter(按状态码维度)
    reqTotal := prometheus.NewCounterVec(
        prometheus.CounterOptions{
            Name: "promhttp_metric_handler_requests_total",
            Help: "Total number of scrapes by HTTP status code.",
        },
        []string{"code"},
    )
    reg.MustRegister(reqTotal)

    // 2. 定义正在处理的请求数Gauge
    reqInFlight := prometheus.NewGauge(prometheus.GaugeOptions{
        Name: "promhttp_metric_handler_requests_in_flight",
        Help: "Current number of scrapes being served.",
    })
    reg.MustRegister(reqInFlight)

    // 3. 包装handler,注入监控逻辑
    return promhttp.InstrumentHandlerCounter(reqTotal,
        promhttp.InstrumentHandlerInFlight(reqInFlight, handler),
    )
}

2. 接口暴露与限流(HandlerFor)

// 暴露/metrics接口,配置限流和超时
func main() {
    // 1. 创建Handler,设置最大并发10、超时10秒
    handler := promhttp.HandlerFor(
        prometheus.DefaultGatherer,
        promhttp.HandlerOpts{
            MaxRequestsInFlight: 10, // 最大并发请求数
            Timeout:             10 * time.Second, // 单个请求超时时间
        },
    )

    // 2. 包装handler,添加基础监控
    http.Handle("/metrics", InstrumentMetricHandler(prometheus.DefaultRegisterer, handler))

    // 3. 启动HTTP服务
    log.Fatal(http.ListenAndServe(":9090", nil))
}

三、Exporter 实现原理(以 Node Exporter 为例)

Node Exporter 是 Prometheus 官方主机监控 Exporter,核心是基于 Client 封装系统指标采集逻辑,暴露 /metrics 接口供 Server 抓取。

3.1 Node Exporter 整体架构

Node Exporter 核心架构分为三层,层层封装: Node Exporter 架构图图1:Node Exporter 三层架构

层级核心职责
采集层通过 Linux 系统调用(/proc、/sys)采集 CPU、内存、磁盘、网络等指标
适配层将系统指标封装为 Client 的 Gauge/Counter/Histogram 等类型
暴露层复用 Client 的 Registry 和 promhttp.Handler,暴露 /metrics 接口

3.2 系统指标采集逻辑(以 CPU 为例)

CPU 指标采集核心是读取 /proc/stat 文件,计算两次采集的差值得到使用率:

package collector

import (
    "bufio"
    "os"
    "strconv"
    "strings"
    "github.com/prometheus/client_golang/prometheus"
)

// cpuCollector 实现Collector接口
type cpuCollector struct {
    cpuUsageDesc *prometheus.Desc // CPU使用率指标描述
}

// NewCPUCollector 创建CPU采集器
func NewCPUCollector() prometheus.Collector {
    return &cpuCollector{
        cpuUsageDesc: prometheus.NewDesc(
            "node_cpu_usage_percent",
            "CPU usage percentage by core",
            []string{"core"}, // 标签:CPU核心编号
            nil,
        ),
    }
}

// Describe 暴露指标元信息
func (c *cpuCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- c.cpuUsageDesc
}

// Collect 采集CPU使用率
func (c *cpuCollector) Collect(ch chan<- prometheus.Metric) {
    // 1. 读取/proc/stat
    file, err := os.Open("/proc/stat")
    if err != nil {
        return
    }
    defer file.Close()

    // 2. 解析CPU统计数据
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()
        if !strings.HasPrefix(line, "cpu") {
            continue
        }

        // 拆分字段:cpu0 100 20 30 400 ...
        fields := strings.Fields(line)
        if len(fields) < 5 {
            continue
        }

        // 3. 计算CPU总耗时和空闲耗时
        total := 0.0
        idle, _ := strconv.ParseFloat(fields[4], 64)
        for i := 1; i < len(fields); i++ {
            val, _ := strconv.ParseFloat(fields[i], 64)
            total += val
        }

        // 4. 计算使用率(瞬时值,Gauge类型)
        usage := (total - idle) / total * 100
        core := fields[0] // 标签值:cpu0/cpu1/...

        // 5. 写入Metric到通道
        ch <- prometheus.MustNewConstMetric(
            c.cpuUsageDesc,
            prometheus.GaugeValue,
            usage,
            core,
        )
    }
}

3.3 指标注册与接口暴露

Node Exporter 复用 Client 组件完成指标注册和接口暴露:

package main

import (
    "log"
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "collector" // 自定义采集器包
)

func main() {
    // 1. 创建默认Registry
    reg := prometheus.NewRegistry()

    // 2. 注册CPU采集器
    reg.MustRegister(collector.NewCPUCollector())
    // 可扩展:注册内存、磁盘、网络等采集器
    // reg.MustRegister(collector.NewMemCollector())
    // reg.MustRegister(collector.NewDiskCollector())

    // 3. 暴露/metrics接口
    http.Handle("/metrics", promhttp.HandlerFor(
        reg,
        promhttp.HandlerOpts{
            MaxRequestsInFlight: 10,
            Timeout:             10 * time.Second,
        },
    ))

    // 4. 启动服务(默认端口9100)
    log.Fatal(http.ListenAndServe(":9100", nil))
}

3.4 自定义 Exporter 开发实践

基于 Client 原理,自定义 Exporter 开发可总结为 5 步:

步骤1:定义指标

根据业务场景选择指标类型,定义 Desc:

// 定义订单数Counter(按订单类型维度)
var (
    orderTotalDesc = prometheus.NewDesc(
        "business_order_total",
        "Total number of orders by type",
        []string{"order_type"}, // 标签:订单类型(普通/秒杀/预售)
        nil,
    )

    // 定义接口耗时Histogram(按接口名维度)
    apiLatencyDesc = prometheus.NewDesc(
        "business_api_latency_seconds",
        "API latency distribution by api name",
        []string{"api"},
        nil,
    )
)

步骤2:实现 Collector 接口

type businessCollector struct {
    // 依赖的业务服务
    orderService *OrderService
    apiService   *APIService
}

func (b *businessCollector) Describe(ch chan<- *prometheus.Desc) {
    ch <- orderTotalDesc
    ch <- apiLatencyDesc
}

func (b *businessCollector) Collect(ch chan<- prometheus.Metric) {
    // 1. 采集订单数(Counter类型)
    orderTypes := []string{"normal", "seckill", "presale"}
    for _, t := range orderTypes {
        count := b.orderService.GetOrderCount(t)
        ch <- prometheus.MustNewConstMetric(
            orderTotalDesc,
            prometheus.CounterValue,
            float64(count),
            t,
        )
    }

    // 2. 采集接口耗时(Histogram类型)
    apis := []string{"/api/order/create", "/api/order/pay"}
    for _, api := range apis {
        latency := b.apiService.GetLatency(api)
        ch <- prometheus.MustNewConstMetric(
            apiLatencyDesc,
            prometheus.HistogramValue,
            latency,
            api,
        )
    }
}

步骤3:注册指标

// 创建Registry并注册采集器
reg := prometheus.NewRegistry()
reg.MustRegister(&businessCollector{
    orderService: NewOrderService(),
    apiService:   NewAPIService(),
})

步骤4:暴露接口

// 暴露/metrics接口
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
log.Fatal(http.ListenAndServe(":9200", nil))

步骤5:部署与抓取

  1. 编译 Exporter:go build -o business-exporter main.go

  2. 启动 Exporter:./business-exporter

  3. 在 Prometheus Server 配置抓取任务:

    scrape_configs:
      - job_name: "business-exporter"
        static_configs:
          - targets: ["<exporter-ip>:9200"]
        scrape_interval: 15s

四、关键最佳实践

4.1 指标类型选择

  • 瞬时值(如使用率、水位)→ Gauge;
  • 累加值(如请求数、错误数)→ Counter;
  • 分布式系统的延迟分布 → Histogram(支持聚合);
  • 单实例的精准分位数 → Summary。

4.2 Histogram 桶设计

  • 桶的范围覆盖 99.9% 的业务场景(如接口耗时:[0.001, 0.01, 0.1, 1, 5, 10] 秒);
  • 桶的步长遵循“对数增长”,避免稀疏或密集分布;
  • 核心接口单独设计桶,通用接口使用默认桶。

4.3 并发安全

  • 指标修改(Set/Add/Inc)必须保证原子操作(如 Gauge 的 CAS 实现);
  • 多 goroutine 采集数据时,使用读写锁(RWMutex)减少竞争。

4.4 避免重复注册

  • 使用 MustRegister 前,先通过 Registerer.Register 检查错误;
  • 全局复用一个 Registry,避免多个 Registry 注册同一指标。

小结

Prometheus Client 的核心是通过 Metric/Collector 抽象指标与采集逻辑,通过 Registry 管理注册,通过 promhttp 暴露接口;而 Exporter 本质是基于 Client 封装特定场景的指标采集逻辑(如 Node Exporter 的系统指标、业务 Exporter 的自定义指标)。

掌握 Client 实现原理后,既能为自研业务做精细化埋点(如接口耗时、业务指标),也能开发自定义 Exporter 覆盖监控盲区。核心是围绕「指标定义-数据采集-接口暴露」全链路,结合业务场景选择合适的指标类型,让 Prometheus 监控覆盖从系统到业务的全维度。