Learn
Prometheus/05-metric-types

四种指标类型

上一章我们知道了:Prometheus 存的每个样本值都是一个 float64。既然存储层不区分类型,那为什么还要有 Counter、Gauge、Histogram、Summary 这四种「类型」?

因为类型不是给存储引擎看的,是给「人」和「查询」看的。类型回答的是这样一个问题:

拿到这个数字,我该怎么用它?直接看?还是必须先求速率?

答错这个问题,你会得到一张看起来很正常、但完全错误的图表——这是 Prometheus 使用中最隐蔽的一类 bug。

1. 类型是约定,不是强制

先破除一个误解:# TYPE 注释不会被 TSDB 强制执行。

# HELP http_requests_total Total HTTP requests.
# TYPE http_requests_total counter
http_requests_total{method="GET"} 1027

这里的 counter 只是一行元信息。Prometheus 把它记下来展示在 Web UI 里,但如果你的应用下一秒把这个值改成 3,TSDB 照存不误——它并不会拒绝。

那类型的价值在哪?

  • 告诉查询者该用哪些函数:Counter 必须配 rate(),Gauge 不能用 rate()。
  • 约束客户端库的行为:Go/Java 客户端的 Counter 对象根本没有 Set() 方法,只有 Inc() 和 Add()(且 Add 拒绝负数),从源头保证语义。
  • 驱动工具链:Grafana 的指标浏览器、promtool、各种 linter 都会依据类型给出建议。
ℹ️四种类型速览
类型语义值的走向必配函数典型后缀
Counter累计发生次数/总量只增,重启归零rate() increase()_total
Gauge某一刻的瞬时状态可增可减直接看、*_over_time()_bytes _ratio
Histogram观察值的分布(服务端分桶)桶是 Counterhistogram_quantile()_bucket _sum _count
Summary观察值的分位(客户端算好)分位是 Gauge直接看quantile 标签 _sum _count

2. Counter:只增不减的累计量

2.1 定义与心智模型

Counter 就像汽车的总里程表:只会往前走,永远不会倒退;换了发动机(进程重启)会归零重新计。

它适合表达「从某个起点到现在,一共发生了多少次 / 累计了多少量」:

http_requests_total          累计处理了多少个请求
errors_total                 累计出了多少次错
process_cpu_seconds_total    累计消耗了多少 CPU 秒
node_network_receive_bytes_total  累计收了多少字节

2.2 Counter 的原始值几乎没有意义

这是最重要的一条认知。看这张图:

时间     http_requests_total 的值
10:00    1,203,441
10:01    1,203,987
10:02    1,204,512

1203441 这个数字告诉你什么?只说明「这个进程从启动到现在处理过 120 万个请求」。它取决于进程启动了多久,而不是取决于当前的负载。

你真正关心的是它的变化速度:

(1204512 - 1203441) / 120 秒 ≈ 8.9 请求/秒

这正是 rate() 在做的事:

# 每秒请求数(QPS),基于最近 5 分钟的样本拟合
rate(http_requests_total[5m])
 
# 最近 1 小时一共增长了多少(次数,不是速率)
increase(http_requests_total[1h])

increase(x[1h]) 在数学上就等于 rate(x[1h]) * 3600,只是语义更直观。

⚠️看到 _total 结尾却没写 rate,八成是错的

在 Grafana 面板上直接画 http_requests_total,你会得到一条永远向上的斜线。它看起来「很有信息量」,但实际上:流量翻倍了、流量归零了,斜线的形状变化都很难用肉眼分辨。而且不同实例因为启动时间不同,起点天差地别,图表会变成一团乱麻。

自查方法:指标名以 _total 结尾、或者 TYPE 是 counter,查询里就必须出现 rate / irate / increase 之一。

2.3 rate 会自动处理「计数器重置」

进程重启后 Counter 归零。如果朴素地做减法,会得到一个巨大的负数:

10:00    1,204,512     ← 重启前
10:01            83    ← 重启后
朴素差值 = 83 - 1204512 = -1,204,429   💥

rate() / increase() 内置了重置检测:当发现后一个样本比前一个小时,它认为发生了重置,把「重置前的最后值」补加回来:

rate 的处理:83 - 1204512 → 判定为重置 → 修正为 (1204512 - 1204512) + 83 = 83

这就是为什么 Counter 必须用 rate() 而不能用 x - x offset 5m 的原因之一。

想知道某段时间内重启了几次,用 resets():

# 最近 1 小时内该 Counter 归零了几次(约等于进程重启次数)
resets(process_cpu_seconds_total[1h])

2.4 经典陷阱:sum 和 rate 的顺序

# ✅ 正确:先各自求速率,再相加
sum(rate(http_requests_total[5m]))
 
# ❌ 错误:先把多条 Counter 加起来,再求速率
rate(sum(http_requests_total)[5m:])

为什么错?因为 sum() 之后就丢失了「哪条序列」的身份。当其中一个实例重启归零时,求和后的曲线会出现一个向下的台阶,而 rate() 会把这个台阶误判为整体重置,计算结果严重失真。

口诀:rate 在内,sum 在外。

2.5 Counter 的客户端用法

// Go 客户端
var httpRequests = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests.",
    },
    []string{"method", "status"},
)
 
func handler(w http.ResponseWriter, r *http.Request) {
    // ... 处理请求
    httpRequests.WithLabelValues(r.Method, "200").Inc()
}

注意 Counter 接口只有 Inc() 和 Add(float64),且 Add 传负数会 panic。语义由类型系统保证。

💡错误也用 Counter,但不要单独建指标

统计错误时,不要新建一个 errors_total,而是给已有的请求 Counter 加一个标签:

✅ http_requests_total{status="200"} 98213
   http_requests_total{status="500"}    47
 
❌ http_requests_total  98260
   http_errors_total       47

前者算错误率是一个查询里的事,分子分母来自同一次抓取,天然一致;后者两个指标可能来自不同抓取时刻,在流量剧变时会算出 > 1 的荒谬错误率。

3. Gauge:可增可减的瞬时值

3.1 定义与心智模型

Gauge 就像汽车的时速表或油量表:它反映「此时此刻是多少」,可以上升也可以下降,没有累计含义。

node_memory_MemAvailable_bytes   当前可用内存
node_load1                       当前 1 分钟负载
go_goroutines                    当前协程数
queue_length                     当前队列长度
kube_pod_status_ready            当前是否就绪(1/0)
temperature_celsius              当前温度

3.2 Gauge 可以直接看,也可以做时间聚合

# 直接看当前值
node_memory_MemAvailable_bytes
 
# 最近 5 分钟的平均值(削峰填谷)
avg_over_time(node_load1[5m])
 
# 最近 1 小时的峰值(容量规划最常用)
max_over_time(go_goroutines[1h])
 
# 最近 1 小时的最低点
min_over_time(node_memory_MemAvailable_bytes[1h])

*_over_time 系列是 Gauge 的专属工具箱——它们对区间向量做「纵向」的时间聚合,而 sum() / avg() 做的是跨序列的「横向」聚合。两者可以组合:

# 每个实例最近 1 小时的峰值协程数,再取全局最大
max(max_over_time(go_goroutines[1h]))

3.3 Gauge 的变化量:delta 与 deriv

Counter 用 rate / increase,Gauge 对应的是 delta / deriv:

# 最近 1 小时磁盘可用空间变化了多少字节(可能是负数)
delta(node_filesystem_avail_bytes[1h])
 
# 每秒变化率(用最小二乘法拟合,比 delta 平滑)
deriv(node_filesystem_avail_bytes[1h])
 
# 基于当前趋势外推:4 小时后可用空间会是多少
predict_linear(node_filesystem_avail_bytes[6h], 4 * 3600)

predict_linear 是磁盘告警的标准写法:

- alert: DiskWillFillIn4Hours
  expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs"}[6h], 4*3600) < 0
  for: 30m
  labels:
    severity: warning
  annotations:
    summary: "磁盘预计 4 小时内写满"

它比「使用率 > 90%」聪明得多:一块 90% 满但一年没变化的盘不需要告警,一块 50% 满但正在飞速增长的盘才危险。

⚠️千万不要对 Gauge 用 rate
# ❌ 严重错误
rate(node_memory_MemAvailable_bytes[5m])

rate() 假设「值下降 = 计数器重置」。内存可用量下降是完全正常的现象,但 rate() 会把每一次下降都当成重置并做补偿,算出的结果既不是速率也不是差值,而是彻头彻尾的垃圾数据——而且它不会报错,会安安静静地画出一条看起来很合理的曲线。

对 Gauge 想看变化率,用 delta() 或 deriv()。

3.4 Gauge 的客户端用法

var queueLength = prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "myapp_queue_length",
    Help: "Current number of items in the queue.",
})
 
queueLength.Set(42)      // 直接设值
queueLength.Inc()        // +1
queueLength.Dec()        // -1
queueLength.Add(-5)      // 允许负数
queueLength.SetToCurrentTime()  // 设为当前 Unix 时间戳

SetToCurrentTime() 有个经典用法——记录「上次成功执行的时间」:

# 备份任务超过 25 小时没成功 → 告警
time() - myapp_last_successful_backup_timestamp_seconds > 25 * 3600

4. Histogram:观察值的分布

4.1 为什么需要 Histogram

假设你想知道「接口有多慢」。用 Gauge 记录每次请求的耗时?做不到——抓取间隔 15 秒,这 15 秒里可能发生了 1 万次请求,你只能采样到其中一个瞬间。

用平均值?平均值是最会骗人的统计量:

1000 次请求,999 次耗时 10ms,1 次耗时 10 秒
平均值 = (999 × 0.01 + 10) / 1000 ≈ 0.02 秒 = 20ms
 
看起来很健康。但那 1 个等了 10 秒的用户已经把 App 卸载了。

你需要的是分布:多少请求在 100ms 内完成?P99 是多少?这就是 Histogram 存在的意义。

4.2 Histogram 的物理结构

一个 Histogram 指标在 /metrics 里会展开成 N+2 条序列:

# HELP http_request_duration_seconds Request latency in seconds.
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.005"}  120
http_request_duration_seconds_bucket{le="0.01"}   480
http_request_duration_seconds_bucket{le="0.025"}  920
http_request_duration_seconds_bucket{le="0.05"}  1180
http_request_duration_seconds_bucket{le="0.1"}   1290
http_request_duration_seconds_bucket{le="0.25"}  1330
http_request_duration_seconds_bucket{le="0.5"}   1342
http_request_duration_seconds_bucket{le="1"}     1346
http_request_duration_seconds_bucket{le="2.5"}   1347
http_request_duration_seconds_bucket{le="5"}     1348
http_request_duration_seconds_bucket{le="10"}    1348
http_request_duration_seconds_bucket{le="+Inf"}  1350
http_request_duration_seconds_sum                 87.32
http_request_duration_seconds_count             1350

三个组成部分:

序列类型含义
_bucket 带 le 标签Counter耗时 ≤ le 的请求累计有多少个
_sumCounter所有观察值之和(这里是总耗时秒数)
_countCounter观察次数总和,等于 le="+Inf" 的桶

关键点一:桶是「累积」的(cumulative)。

le="0.1" 的值 1290 表示「耗时 ≤ 0.1 秒的请求有 1290 个」,它包含了 le="0.05" 的那 1180 个。所以桶的值一定单调不减,最后一个 le="+Inf" 必然等于 _count。

想知道「落在 50ms 到 100ms 之间的请求数」,需要自己做减法:1290 - 1180 = 110。

关键点二:每个桶都是 Counter。

所以查询时必须先套 rate():

rate(http_request_duration_seconds_bucket[5m])

关键点三:le 是普通标签,值是字符串。

le="0.1" 里的 0.1 是字符串。这就是为什么在配置桶时 1 和 1.0 会产生两条不同的序列(历史上踩过坑,现在客户端会做规范化)。

4.3 从 Histogram 能算出什么

(1)平均值——最简单,几乎免费

# 平均耗时 = 总耗时 / 总次数
rate(http_request_duration_seconds_sum[5m])
/
rate(http_request_duration_seconds_count[5m])

注意分子分母都要 rate(),这样算的是「最近 5 分钟的平均」,而不是「进程启动以来的平均」。

(2)分位数——Histogram 的招牌能力

# P99 延迟(秒)
histogram_quantile(
  0.99,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)
 
# 按接口分别看 P95
histogram_quantile(
  0.95,
  sum by (le, path) (rate(http_request_duration_seconds_bucket[5m]))
)

这个写法有三个必须记住的要点,缺一个结果就是错的:

  1. 内层必须是 rate(..._bucket[...]),因为桶是 Counter。
  2. sum by (le, ...) 中必须保留 le 标签——histogram_quantile 就是靠 le 重建分布的。
  3. 想按某个维度分组(如 path),把它一起写进 by 里;其余维度(如 instance)会被聚合掉,得到的是全局 P95 而不是各实例 P95 的平均。

(3)SLO 达成率——比分位数更实用

# 300ms 内完成的请求占比
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))

这个写法没有任何估算误差(只要 0.3 恰好是一个桶边界),而且比 P99 更贴近 SLO 语言:「99% 的请求在 300ms 内完成」。Google SRE 手册推荐优先用这种形式。

4.4 histogram_quantile 是「估算」,不是精确值

理解这一点,你才不会被 P99 的数字误导。

histogram_quantile 的算法是:找到目标分位落在哪个桶里,然后在这个桶的上下边界之间做线性插值。

用前面的数据算 P99:

总数 _count = 1350
P99 的目标位置 = 1350 × 0.99 = 1336.5
 
查桶:
  le="0.25" → 1330   (还不到 1336.5)
  le="0.5"  → 1342   (超过了 → P99 落在 (0.25, 0.5] 这个桶里)
 
线性插值:
  桶内样本数 = 1342 - 1330 = 12
  目标在桶内的位置 = 1336.5 - 1330 = 6.5
  P99 ≈ 0.25 + (0.5 - 0.25) × (6.5 / 12) ≈ 0.385 秒

**这个 0.385 秒的可信度取决于一个假设:这 12 个样本在 0.25s 到 0.5s 之间均匀分布。**现实中它们可能全都挤在 0.26 秒,也可能全都是 0.49 秒。

⚠️Histogram 的三条铁律

铁律一:分位数永远落在某个桶的范围内。 如果你的最大有限桶是 le="10",而实际有请求耗时 60 秒,histogram_quantile 最多只能告诉你「大于 10 秒」,返回 +Inf。桶的上界必须覆盖你关心的最坏情况。

铁律二:桶要卡在关心的阈值上。 如果 SLO 是「300ms」,就一定要有一个 le="0.3" 的桶。默认桶里没有 0.3,你只能在 0.25 和 0.5 之间插值,误差可能达到 50%。

铁律三:桶越密越准,但序列数线性增长。 桶数是基数的乘数。10 个桶 × 50 个接口 × 6 个实例 = 3600 条序列,还没算 _sum 和 _count。

4.5 桶该怎么选

Go 客户端的默认桶是:

prometheus.DefBuckets = []float64{
    .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10,
}

这套桶是为「典型的网络 API」设计的。如果你的场景不同,一定要自定义:

// 场景 A:内部 RPC,都在毫秒级 —— 默认桶太粗,前 3 个桶就装完了 99% 的请求
Buckets: []float64{0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.5}
 
// 场景 B:SLO 驱动 —— 桶边界直接对齐 SLO 阈值
Buckets: []float64{0.1, 0.3, 1, 3},   // 只要 4 个桶,但每个都有业务含义
 
// 场景 C:批处理任务,耗时几分钟 —— 用指数桶
Buckets: prometheus.ExponentialBuckets(1, 2, 10),  // 1,2,4,...,512 秒
 
// 场景 D:线性分布,比如「批次大小」
Buckets: prometheus.LinearBuckets(100, 100, 10),   // 100,200,...,1000
💡Native Histogram(Prometheus 2.40+)

Prometheus 引入了实验性的 Native Histogram(原生直方图):桶边界由一个分辨率参数自动生成(指数分布),一条序列就能表达整个分布,不再需要为每个桶建一条序列。

优点很诱人:基数从「N+2 条」降到「1 条」,精度还更高(可以做到相对误差 1% 以内),且不需要预先猜测桶边界。

代价是需要客户端库、Prometheus、Grafana 三方都支持,且远程写协议要用 Protobuf。目前(截至 3.x)仍在逐步稳定中,新项目可以关注,存量系统不急着迁移。

5. Summary:客户端算好的分位数

5.1 结构

Summary 表面上和 Histogram 很像,都有 _sum 和 _count,但第三部分完全不同:

# HELP rpc_duration_seconds RPC latency in seconds.
# TYPE rpc_duration_seconds summary
rpc_duration_seconds{quantile="0.5"}   0.012
rpc_duration_seconds{quantile="0.9"}   0.048
rpc_duration_seconds{quantile="0.99"}  0.213
rpc_duration_seconds_sum              87.32
rpc_duration_seconds_count          1350

区别在于:

  • Histogram 暴露的是 _bucket(原始的桶计数),分位数在查询时由 Prometheus 算。
  • Summary 暴露的是 quantile 标签(已经算好的分位值),计算发生在应用进程内。

而且注意:Summary 的分位数序列没有 _ 后缀,它就是指标名本身加一个 quantile 标签。

5.2 Summary 的致命缺陷:分位数不可聚合

这是选型时唯一需要记住的一条:

# ❌ 这个查询在数学上是错的
avg(rpc_duration_seconds{quantile="0.99"})

为什么? 因为分位数不是可加的统计量。

实例 A:1000 次请求,P99 = 100ms
实例 B:  10 次请求,P99 = 900ms
 
avg = (100 + 900) / 2 = 500ms   ← 完全错误
 
真实的全局 P99:
  1010 次请求中最慢的 1%(约 10 次)
  → 这 10 次大概率来自 B 的慢请求 + A 的尾部
  → 真实值可能是 150ms,也可能是 900ms,
    但绝对不可能靠两个 P99 的平均算出来

你无法从「各实例的 P99」推出「集群的 P99」,就像你无法从各班的中位数身高推出全年级的中位数身高。唯一的办法是拿到原始分布——这正是 Histogram 提供而 Summary 不提供的东西。

对比一下,Histogram 因为暴露的是原始桶计数(可加的 Counter),聚合是完全合法的:

# ✅ 桶可以先 sum 再算分位,这才是真正的全局 P99
histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

5.3 Summary 的滑动窗口

Summary 的分位数是在一个客户端维护的滑动时间窗口上计算的(Go 客户端默认 10 分钟,分 5 个 2 分钟的桶轮转):

prometheus.NewSummary(prometheus.SummaryOpts{
    Name: "rpc_duration_seconds",
    Objectives: map[float64]float64{
        0.5:  0.05,    // P50,允许 5% 相对误差
        0.9:  0.01,    // P90,允许 1% 相对误差
        0.99: 0.001,   // P99,允许 0.1% 相对误差
    },
    MaxAge:     10 * time.Minute,  // 滑动窗口长度
    AgeBuckets: 5,
})

这带来两个后果:

  1. 窗口长度写死在代码里。你在 PromQL 里没法改成「最近 1 小时的 P99」——那是应用配置决定的,改要重新发版。而 Histogram 只要把 [5m] 改成 [1h] 即可。
  2. 计算成本在应用侧。目标误差越小,客户端需要维护的采样数据结构越大、CPU 开销越高。0.99: 0.001 这样的配置在高 QPS 下可能带来可观的开销。
⚠️Summary 不填 Objectives 会怎样

Go 客户端里,如果 Objectives 留空(这是 1.x 之后的默认值),Summary 不会暴露任何 quantile 序列,只剩 _sum 和 _count。

这其实是社区有意为之的引导:大多数人真正需要的只是 _sum / _count(平均值),而分位数应该用 Histogram。见过不少人配了 Summary 却查不到 quantile 标签,原因就在这里。

5.4 什么时候 Summary 反而更合适

Summary 并非一无是处,它有两个 Histogram 做不到的优势:

优势一:精度更高。 Summary 的分位数是基于真实样本流计算的(φ-quantile 算法),误差可控在配置的目标内。Histogram 的插值误差取决于桶边界,可能相差很远。

优势二:不需要预先知道值域。 Histogram 必须提前猜好桶边界,猜错了数据就废了(改桶边界会导致历史数据不可比)。Summary 完全不用操心。

所以 Summary 适合:

  • 单实例、无需聚合的场景:批处理任务、单机守护进程、CLI 工具。
  • 值域完全未知的场景:第一次接入一个新指标,先用 Summary 摸清分布范围,再改成 Histogram 并按实际分布设桶。

6. Histogram vs Summary 决策表

维度HistogramSummary
分位数在哪算查询时,Prometheus 算采集时,应用进程算
能否跨实例聚合✅ 能(桶可加)❌ 不能(分位数不可加)
时间窗口查询时决定,随时可改代码里写死,改要发版
需要预知值域✅ 需要(配桶)❌ 不需要
分位精度受桶边界影响,插值估算高,误差可配置
应用侧 CPU 开销极低(就是几个计数器 +1)中到高(维护采样结构)
产生的序列数桶数 + 2分位数 + 2
服务端查询开销略高(要算插值)低(直接读)
💡一句话选型

默认选 Histogram。 只有当你确信「这个指标永远只有一个实例,且我不知道值域范围」时,才考虑 Summary。

现实中的分布式服务几乎总要跨实例聚合,而聚合能力是 Summary 的硬伤——这一条基本就决定了答案。

7. 命名约定与单位后缀

类型和命名是配套的。好的命名让人不看 # TYPE 就知道该怎么查。

7.1 后缀速查表

后缀适用类型含义示例
_totalCounter累计值http_requests_total
_seconds任意时间,单位秒process_cpu_seconds_total
_bytes任意大小,单位字节node_memory_MemFree_bytes
_ratioGauge比率,取值 0–1node_cpu_usage_ratio
_infoGauge元信息,值恒为 1node_uname_info
_bucketHistogram桶(自动生成)..._seconds_bucket
_sumHistogram/Summary观察值总和(自动生成)..._seconds_sum
_countHistogram/Summary观察次数(自动生成)..._seconds_count
_timestamp_secondsGauge某事件发生的 Unix 时间..._last_success_timestamp_seconds

7.2 组合规则

Counter 的后缀顺序是「单位在前,_total 在后」:

✅ process_cpu_seconds_total        单位 seconds + Counter 标志 total
✅ node_network_receive_bytes_total 单位 bytes + Counter 标志 total
❌ process_cpu_total_seconds        顺序反了

Histogram 只需要给基础名加单位,_bucket / _sum / _count 由客户端自动生成:

声明的指标名:  http_request_duration_seconds
实际暴露的:    http_request_duration_seconds_bucket{le="..."}
              http_request_duration_seconds_sum
              http_request_duration_seconds_count
⚠️Histogram 不要加 _total
❌ http_request_duration_seconds_total
   → 客户端会生成 http_request_duration_seconds_total_bucket,非常难看
   → 而且 _total 暗示这是 Counter,误导查询者
 
✅ http_request_duration_seconds

同理,Gauge 也绝不能加 _total——那会诱导别人对它用 rate(),得到垃圾数据。

7.3 用基本单位

✅ seconds   (不是 ms / us / ns)
✅ bytes     (不是 KB / MB / GiB)
✅ ratio     (0–1,不是 percent 的 0–100)
✅ celsius   (不是 fahrenheit)

原因在上一章讲过:Grafana 能自动把 0.0032 seconds 渲染成 3.2 ms,把 16633360384 bytes 渲染成 15.5 GiB。用非基本单位反而丢掉了这个能力,还容易在跨组件计算时把 ms 和 s 加在一起。

8. 选型决策流程

我要记录什么?
│
├─ 「发生了多少次 / 累计了多少量」,只会往上加
│   → Counter,名字加 _total,查询必配 rate()
│
├─ 「此刻是多少」,会上下波动
│   → Gauge,名字加单位后缀,直接看或用 *_over_time()
│
└─ 「一批数值的分布」(耗时、大小、批次量)
    │
    ├─ 需要跨实例聚合?(几乎总是「是」)
    │   → Histogram
    │
    └─ 单实例 + 值域完全未知
        → Summary(用完摸清分布后建议改回 Histogram)

一个订单服务的完整指标设计示例:

# Counter:累计事件
orders_created_total{channel="app", region="bj"}          累计下单数
orders_failed_total{channel="app", reason="out_of_stock"} 累计失败数
payment_amount_yuan_total{method="alipay"}                累计支付金额
 
# Gauge:瞬时状态
orders_pending_count{region="bj"}                         当前待处理订单
order_queue_length                                        当前队列长度
inventory_available_count{sku_category="phone"}           当前可用库存
 
# Histogram:分布
order_processing_duration_seconds_bucket{le="0.5"}        下单处理耗时
order_amount_yuan_bucket{le="100"}                        订单金额分布
 
# Info:元信息
orderservice_build_info{version="2.3.1", commit="a1b2c3d"} 1
ℹ️Counter 也可以记录「量」而不只是「次数」

payment_amount_yuan_total 累加的是金额而不是次数,这完全合法——Counter 的约束只有「单调不减」,不要求是整数或计数。

同理 process_cpu_seconds_total 累加的是秒数,node_network_receive_bytes_total 累加的是字节数。用 rate() 查它们,得到的就是「每秒收入多少元」「每秒消耗多少 CPU 秒」「每秒收多少字节」。


🎯练习 1:为下列场景选择指标类型

为以下 8 个监控需求各选一种指标类型(Counter / Gauge / Histogram / Summary),并给出符合规范的指标名:

  1. 网站累计被访问的次数
  2. 当前在线用户数
  3. 每次数据库查询的耗时分布,需要看集群整体的 P99
  4. 服务器当前 CPU 温度
  5. 消息队列中积压的消息条数
  6. 上传文件的大小分布,需要按存储桶分组统计
  7. 应用上次成功执行定时任务的时间
  8. 某个单机 CLI 工具内部函数的执行耗时(只在本机看,不上报集群)
🎯练习 2:读懂一段 Histogram 输出

给定下面这段 /metrics 输出(某接口最近的累计值):

# TYPE api_latency_seconds histogram
api_latency_seconds_bucket{le="0.01"}   200
api_latency_seconds_bucket{le="0.05"}   700
api_latency_seconds_bucket{le="0.1"}    900
api_latency_seconds_bucket{le="0.5"}    980
api_latency_seconds_bucket{le="1"}      995
api_latency_seconds_bucket{le="+Inf"}  1000
api_latency_seconds_sum                 62.5
api_latency_seconds_count              1000

回答:

  1. 一共观察了多少次请求?平均耗时是多少?
  2. 耗时落在 50ms 到 100ms 之间的请求有多少个?
  3. 手工估算 P95 是多少(写出计算过程)。
  4. 写出「计算最近 5 分钟集群整体 P95」的 PromQL,并说明为什么每一层都不能省。
🎯练习 3:找出错误用法

下面 6 段代码或查询都有问题,指出问题并给出修正:

1.  指标名: memory_usage_total       类型: gauge      当前值: 8589934592
 
2.  查询:   rate(node_load1[5m])
 
3.  查询:   http_requests_total
 
4.  查询:   avg(rpc_duration_seconds{quantile="0.99"})
 
5.  查询:   histogram_quantile(0.99, sum(rate(api_latency_seconds_bucket[5m])))
 
6.  Go 代码:
    prometheus.NewCounter(prometheus.CounterOpts{
        Name: "queue_size",
    })
    // 使用处
    queueSize.Add(-1)
🎯练习 4:为文件上传服务设计指标

你负责一个文件上传服务,需要监控以下方面:

  • 上传成功 / 失败的次数,需要能区分失败原因和文件类型
  • 当前正在处理的上传请求数
  • 上传文件的大小分布(从几 KB 到几百 MB)
  • 上传处理耗时的 P99,需要看整个集群的
  • 当前存储空间剩余量,并希望在「预计 12 小时内写满」时告警

请设计完整的指标集(含类型、名称、标签),说明 Histogram 桶的选择,并写出关键的 PromQL 查询和一条告警规则。

小结

  • 类型是约定不是强制:TSDB 只存 float64,# TYPE 的价值在于告诉你「该用什么函数查它」。
  • Counter:只增不减,原始值几乎无意义,必须配 rate() / increase();rate 内置计数器重置检测;口诀是「rate 在内,sum 在外」。
  • Gauge:可增可减的瞬时值,直接看或用 avg_over_time / max_over_time;变化量用 delta / deriv,绝不能用 rate。
  • Histogram:服务端分桶,暴露 _bucket(累积计数,带 le 标签)、_sum、_count。分位数在查询时用 histogram_quantile 插值估算——精度取决于桶边界,桶要卡在 SLO 阈值上,上界要覆盖最坏情况。
  • Summary:客户端算好分位数,精度高、不需预知值域,但分位数不可跨实例聚合,且时间窗口写死在代码里。
  • 选型默认 Histogram,只有单实例且值域未知时才用 Summary。
  • 命名与类型配套:Counter 加 _total,一律用基本单位(_seconds / _bytes),Gauge 和 Histogram 绝不加 _total。