一个案例带你搞懂Prometheus指标类型(上)

116 阅读6分钟

在云原生和微服务架构盛行的今天,监控系统的重要性不言而喻。Prometheus 作为其中的佼佼者,凭借其强大的数据模型和查询语言,成为了监控领域的标准。

然而要真正发挥 Prometheus 的威力,首先需要深刻理解其核心——四大指标类型。这篇文章我们将深入实践,彻底搞懂 Counter、Gauge、Histogram 和 Summary,并结合实际场景,让你知道在何时何地选择合适的指标类型。准备好了吗?让我们开始这场监控之旅! 😎

🛒 实践场景

想象你正在负责一个电商订单系统的监控,老板天天问你:

"今天卖了多少单?" 📊

"库存还剩多少?" 📦

"API响应慢不慢?" ⏱️

"订单金额分布如何?" 💰

这时候,Prometheus的四大指标类型就派上用场了!让我们通过一个真实的电商系统来搞懂它们。

📊 四大指标对比

Prometheus 是一个开源的系统监控和告警工具包,它以其多维数据模型、强大的查询语言 PromQL、高效的时间序列数据库以及灵活的告警机制而闻名。

Prometheus 的核心是其时间序列数据。一个时间序列由一个指标名称(metric name)和一组标签(labels)唯一确定。

例如,http_requests_total{method="POST", handler="/api/messages"} 就是一个时间序列,它记录了对 /api/messages 路径的 POST 请求总数。

理解 Prometheus 的第一步,就是理解它如何收集和分类这些数据,这就要从它的四种基本指标类型说起。

指标类型特性电商案例适用场景关键操作
Counter只增不减的累计值订单总量计数类指标Inc()
Gauge可增可减的瞬时值库存数量状态类指标Set(), Inc(), Dec()
Histogram观察值的分布统计API响应时间性能分析Observe()
Summary观察值的分位数统计订单金额分布业务分析Observe()

🤔 Counter vs Gauge:什么时候用哪个?

Counter: 像计步器,只会往上加 📈

Counter 是一个只增不减的累积型指标。它通常用于记录应用启动以来发生的事件总数,例如处理的 HTTP 请求总数、完成的任务数或出现的错误数。

比如,订单数量:今天1000单,明天1050单,错误次数:累计错误500次

Gauge: 像温度计,可上可下 🌡️

Gauge 是一个可以任意增减的瞬时值指标。它通常用于表示那些可以上下波动的测量值,例如当前的内存使用量、CPU 负载、队列中的任务数或活动的线程数。

比如,库存数量:现在100件,卖了10件剩90件,在线用户:当前1000人在线

🤔 Histogram vs Summary:都是分布,有啥区别?

Histogram: 服务端聚合,客户端分桶 📊

Histogram 主要用于观察和分析事件的分布情况,最常见的场景是监控请求延迟或响应大小。它会将一段时间内的数据采样,并将其计入可配置的存储桶(bucket)中,同时也会提供所有采样值的总和(sum)和总数(count)。

优点是可以聚合多个实例的数据,但缺点主要在于分位数是估算的。主要适合API响应时间、请求大小等场景。

Summary: 客户端聚合,精确分位数 🎯

Summary 与 Histogram 类似,也用于观察事件的分布。但它不通过分桶,而是在客户端直接计算和存储分位数(Quantile)。

其优点在于分位数精确,缺点在于无法跨实例聚合,主要适合于业务指标、SLA监控。

🏗️ 系统架构

💻 代码实现

指标定义与初始化

我们的电商系统定义了四种核心指标:

// Counter: 订单总量 - 只增不减的累计指标
var orderTotal = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "ecommerce_orders_total",
        Help: "电商系统订单总数",
    },
    []string{"status", "payment_method"}, // 标签:订单状态、支付方式
)

// Gauge: 库存数量 - 可增可减的瞬时指标
var inventoryStock = prometheus.NewGaugeVec(
    prometheus.GaugeOpts{
        Name: "ecommerce_inventory_stock",
        Help: "商品库存数量",
    },
    []string{"product_id", "category"}, // 标签:商品ID、分类
)

// Histogram: API响应时间分布 - 观察值的分布情况
var apiDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "ecommerce_api_duration_seconds",
        Help:    "API请求响应时间分布",
        Buckets: prometheus.DefBuckets, // 默认桶
    },
    []string{"method", "endpoint", "status_code"}, // 标签
)

// Summary: 订单金额分布 - 观察值的分位数统计
var orderAmount = prometheus.NewSummaryVec(
    prometheus.SummaryOpts{
        Name: "ecommerce_order_amount_yuan",
        Help: "订单金额分布统计",
        Objectives: map[float64]float64{
            0.5:  0.05, // 50分位数,误差5%
            0.9:  0.01, // 90分位数,误差1%
            0.99: 0.001, // 99分位数,误差0.1%
        },
    },
    []string{"user_type", "promotion"}, // 标签
)
业务逻辑实现

创建订单 - 四大指标的综合应用

func (e *ECommerceSystem) CreateOrder(productID string, quantity int, amount float64, userType, paymentMethod string) {
    start := time.Now()
    
    // 模拟API处理时间
    processingTime := time.Duration(rand.Intn(500)+50) * time.Millisecond
    time.Sleep(processingTime)
    
    // 检查库存
    if stock, exists := e.products[productID]; exists && stock >= quantity {
        // 库存充足,创建订单成功
        e.products[productID] -= quantity
        
        // 🔢 Counter: 订单成功计数 +1
        orderTotal.WithLabelValues("success", paymentMethod).Inc()
        
        // 📊 Gauge: 更新库存数量
        inventoryStock.WithLabelValues(productID, "electronics").Set(float64(e.products[productID]))
        
        // 💰 Summary: 记录订单金额
        orderAmount.WithLabelValues(userType, "none").Observe(amount)
        
        // ⏱️ Histogram: 记录API响应时间
        apiDuration.WithLabelValues("POST", "/api/orders", "200").Observe(time.Since(start).Seconds())
        
        log.Printf("✅ 订单创建成功: 商品=%s, 数量=%d, 金额=%.2f元", productID, quantity, amount)
    } else {
        // 库存不足,订单失败
        orderTotal.WithLabelValues("failed", paymentMethod).Inc()
        apiDuration.WithLabelValues("POST", "/api/orders", "400").Observe(time.Since(start).Seconds())
        
        log.Printf("❌ 订单创建失败: 商品=%s库存不足", productID)
    }
}

关键代码解析

Counter使用技巧

// ✅ 正确:使用标签区分不同状态
orderTotal.WithLabelValues("success", "alipay").Inc()
   
// ❌ 错误:Counter不能减少
// orderTotal.Dec() // 这会panic!

为什么用 Counter? 因为 订单数量是一个不断累积的量。我们不关心某个瞬间的绝对值(因为它只会越来越大),而是关心它在单位时间内的变化率,比如“每秒请求数”(RPS)。Counter + rate() 函数的组合完美地满足了这一需求。

Gauge使用技巧

// ✅ 设置绝对值
inventoryStock.WithLabelValues("iphone15", "electronics").Set(100)
   
// ✅ 增加/减少
inventoryStock.WithLabelValues("iphone15", "electronics").Inc()
inventoryStock.WithLabelValues("iphone15", "electronics").Dec()

为什么用 Gauge? 因为库存数量不是一个累积值,它会随着系统的负载情况动态变化。我们关心的是它在某个时间点的确切值,以便判断系统是否健康(例如,队列长度是否过长,是否需要扩容处理节点)。

Histogram vs Summary

// Histogram: 适合可聚合的性能指标
apiDuration.WithLabelValues("POST", "/api/orders", "200").Observe(0.123)
   
// Summary: 适合业务分位数分析
orderAmount.WithLabelValues("vip", "double11").Observe(15999.0)

为什么用 Histogram? 因为它提供了数据的分布视图。通过观察不同延迟区间的请求数量,我们可以更精确地定位性能瓶颈。例如,如果我们发现大量请求落在了 500ms-1s 的桶里,这就明确指出了一个需要优化的方向。

为什么用 Summary? 如果你的核心需求是获取精确的分位数,并且可以接受其不可聚合的限制,Summary 是一个直接的选择。

Histogram vs. Summary 对比

直方图与汇总对比

特性HistogramSummary
分位数计算服务端 (histogram_quantile)客户端 (直接暴露)
聚合能力可聚合(跨实例计算分位数)不可聚合
性能开销客户端较低,服务端较高客户端较高,服务端较低
配置需要预先定义 buckets需要定义 quantiles 和其误差

在绝大多数情况下,优先选择 Histogram。它的灵活性和可聚合性使其在现代分布式系统中更为实用。只有在你明确知道不需要聚合,并且对客户端性能开销不敏感时,才考虑使用 Summary。