指标(Metrics):Counter、Gauge、Histogram、Summary
聊可观测性,绕不开四种核心指标类型。它们就像监控世界的四种基本颜色,混搭才能调出真实的系统画面。我个人习惯把这四种类型当成「四种视角的探针」,每种看到的系统侧面都不一样。
Counter、Gauge、Histogram、Summary —— 这四个名词你肯定见过。但什么时候用哪个?各有什么坑?这才是关键。先看一张总览图,把全貌装进脑子里,我们再逐个拆。
Counter(计数器)—— 只增不减的累计值
Counter 是最朴素的指标类型。它只能加,不能减。重启时归零,然后重新累加。你想想看,什么场景符合这个特征?
- HTTP 请求总数
- 数据库查询次数
- 队列投递的消息量
- 业务异常发生次数
Counter 本身其实是个单调递增的数字,但光看值往往没有意义。几千次请求 vs 几万次请求,脱离了时间窗口很难判断。所以我们通常配合 rate() 或 irate() 函数计算速率,比如 rate(http_requests_total[5m]) 就能知道最近 5 分钟平均每秒处理的请求数。
// Prometheus 客户端示例(Go)
counter := prometheus.NewCounter(prometheus.CounterOpts{
Name: "myapp_requests_total",
Help: "Total number of requests processed.",
})
counter.Inc() // 加 1
counter.Add(5) // 加 5
Gauge(仪表盘)—— 系统的即时快照
Gauge 和 Counter 正好相反,它允许数值上下波动。它反映的是「这一刻」的状态,而不是累计历史。
适合的场景很明显:
- 当前 CPU 使用率
- 内存使用量
- 活跃连接数
- 队列当前深度
- 磁盘剩余空间
Gauge 没有速率的概念,它的价值在于快照和趋势。监控面板上常见的实时曲线图,大多由 Gauge 驱动。需要注意的是,Gauge 的值通常由程序主动上报,如果上报间隔过长,曲线会变得很毛糙,建议配合合理的拉取频率(比如 10-30 秒一次)。
// Prometheus 客户端示例(Java)
Gauge gauge = Gauge.build()
.name("myapp_memory_used_bytes")
.help("Current memory usage in bytes.")
.register();
gauge.set(1024 * 1024 * 256); // 设 256MB
gauge.dec(1024 * 1024); // 减 1MB
gauge.inc(); // 加 1
Histogram(直方图)—— 统计分布的秘密武器
Histogram 解决了一个很实际的问题:光知道平均值没用,得知道数据是怎么分布的。你想想看,99% 的请求都在 100ms 以内,但 1% 的请求耗时 10 秒,平均值可能还是 200ms,漂亮的平均值掩盖了严重的毛刺。
Histogram 在工作时会把你传入的观测值分配到预定义的桶(bucket)里。比如定义桶为:[0.1, 0.5, 1, 2.5, 5, 10] 秒,每次请求耗时 0.8 秒,就丢进 <=1 秒的那个桶。最终你会得到每个桶里的计数,然后可以用公式算出百分位数(P99、P95 等)。
| 桶区间 | 累计计数 |
|---|---|
| ≤ 0.1s | 1500 |
| ≤ 0.5s | 8500 |
| ≤ 1s | 9800 |
| ≤ 2.5s | 9970 |
| ≤ 5s | 9990 |
| ≤ 10s | 10000 |
从这张表里可以算出:P99 大约是 2.5s(因为 99% 的请求都在 <=2.5s 桶内)。P50 则在 0.5s 到 1s 之间,因为 85% 在 0.5s 以内,98% 在 1s 以内,所以中位数大约在 0.6s 左右。
// Prometheus 客户端示例
histogram := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "myapp_request_duration_seconds",
Help: "Request latency in seconds.",
Buckets: prometheus.DefBuckets, // 默认桶:[.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]
})
histogram.Observe(0.8)
Summary(摘要)—— 客户端预计算的分位数
Summary 和 Histogram 有点像,都是为了算分位数。但区别在于:Histogram 把原始分布抛给服务端去算,而 Summary 在客户端就完成了分位数的预计算,直接上报计算好的值(比如 0.5、0.9、0.99 分位数)。
这么做的好处是服务端压力小,不需要实时计算百分位。代价是你必须提前定义好想要哪些分位点(例如只想要 P50、P90、P99),且后续无法再增加新的分位点(因为原始数据已经丢了)。
| 特性 | Histogram | Summary |
|---|---|---|
| 分位数计算位置 | 服务端(查询时聚合) | 客户端(上报前计算) |
| 是否需要预定义分位点 | 不需要 | 需要(定义 quantile) |
| 可在多实例间聚合 | 可以(累计直方图) | 不可以(无法聚合预计算值) |
| 内存开销 | 与桶数量成正比 | 与流式算法实现有关,通常可控 |
| 适用场景 | 需要灵活查询任意百分位 | 已经知道明确的 SLO 分位点 |
// Prometheus 客户端示例(Go)
summary := prometheus.NewSummary(prometheus.SummaryOpts{
Name: "myapp_request_duration_seconds",
Help: "Request latency in seconds.",
Objectives: map[float64]float64{
0.5: 0.05, // 允许 5% 误差
0.9: 0.01, // 允许 1% 误差
0.99: 0.001, // 允许 0.1% 误差
},
})
summary.Observe(0.8)
选型速查表
| 需求 | 推荐指标类型 | 原因 |
|---|---|---|
| 统计每秒请求数 | Counter + rate() | 只增不减,速率计算天然适合 |
| 当前内存使用量 | Gauge | 可增可减,记录瞬时状态 |
| 请求延迟分布、P99 | Histogram | 灵活,可聚合,桶可调 |
| 已确定 SLO 的延迟分位数 | Summary | 客户端计算,服务端压力小 |
| 业务非负累加值(订单量、访问量) | Counter | 天然累计语义 |
| 队列大小、连接池占用 | Gauge | 随时变化,快照数据 |
| 数据库查询耗时分布 | Histogram | 需要多维度分析慢查询 |
嗯,四种指标到这里就聊完了。记住一句话:没有银弹,每种指标都有自己的性格。Counter 老实巴交只管累加,Gauge 反应灵敏但容易受毛刺干扰,Histogram 信息量大但桶的设计有讲究,Summary 省服务端但灵活性差。选对工具,监控工作至少轻松一半。我在项目中碰到的绝大多数告警漂移问题,追到底都是指标类型用错了。所以,每次加新指标之前,先问自己一句:这个指标到底是什么语义?该用 Counter 还是 Gauge?该用 Histogram 还是 Summary?答案自然就出来了。