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 1700Summary(客户端分位数计算)
客户端直接计算分位数并输出,无需预定义桶。
- 优势:分位数计算更灵活,无需提前设计桶;
- 缺点:不支持跨实例聚合。
示例输出:
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 核心抽象是 Metric 和 Collector 接口,所有指标类型均基于这两个接口实现:
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 接口内嵌 Metric 和 Collector,并扩展值修改方法(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 核心字段
| 字段 | 作用 |
|---|---|
| metrics | key为标签值的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 方法)
- 启动 goroutine 调用 Collector 的
Describe方法,获取所有 Desc; - 校验 Desc 唯一性(ID、维度 hash),若重复则返回错误;
- 计算 Collector ID(所有 Desc ID 的哈希和),存入
collectorsByID; - 注册成功后,Collector 即可被
Gather方法采集。
数据采集(Gather 方法)
- 启动多 goroutine 调用 Collector 的
Collect方法,获取所有 Metric; - 将 Metric 序列化为
dto.MetricFamily,供/metrics接口返回; - 动态控制 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 核心架构分为三层,层层封装:
图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:部署与抓取
编译 Exporter:
go build -o business-exporter main.go;启动 Exporter:
./business-exporter;在 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 监控覆盖从系统到业务的全维度。
