类型选择:Counter、Gauge、Histogram、Summary
判定规则可以压缩成一句话:会单调累加、只增不减的是 Counter(HTTP 请求总数、队列写入次数);会上下波动、当前值有意义的是 Gauge(内存使用量、连接数、队列长度);观察的是「值的分布」用 Histogram 或 Summary(请求延迟、响应体大小)。选错类型的代价不是命名难看,而是 `rate()` 与告警规则直接算错。
最经典的错误是把「当前在线连接数」做成 Counter。这会导致 `rate()` 得出负值或无意义的结果,容量告警彻底失效。另一个反例是把「请求总次数」做成 Gauge——它确实会因重启而归零,但 Gauge 语义下你无法区分「归零」与「计数器溢出」,长期趋势图会出现无法解释的断崖。
Histogram 与 Summary 的关键差异是分位数由谁计算:Histogram 在客户端把观测值分到预设 bucket 里,聚合成 `_bucket`、`_sum`、`_count` 三个时间序列,分位数可以在服务端任意计算和重新聚合;Summary 则在客户端用分位数流(reservoir)计算分位数,只能得到 `_sum`、`_count` 和若干不可再聚合的 `quantile` 标签。时间序列会分叉的指标(可加总的服务、多个实例汇总)必须用 Histogram。
Histogram 的代价是 bucket 数量直接乘进基数:`buckets` 设 10 个就产生 11 条时间序列(`_bucket` 包含 le 标签)。bucket 的设计应根据 SLO 关键区间来,而不是照抄默认值。比如 P99 SLO 是 200ms,那么 100/150/200/300/500ms 这些边界必须有,其它可以不设。Summary 则在客户端占用内存维护 reservoir,且分位数不可跨实例合并。
# Counter:只增不减,进程重启才归零
http_requests_total{method="GET", status="200"} 10240
# Histogram:可跨实例再聚合
http_request_duration_seconds_bucket{le="0.2"} 942
http_request_duration_seconds_sum 187.3
http_request_duration_seconds_count 1024基数量化:先算再写
基数的估算方法很简单:时间序列数 ≈ 指标名数量 × 所有 label 取值组合的乘积。`method` 有 4 个值、`status` 有 8 个值、`endpoint` 有 30 个值,那么这个指标就是 4 × 8 × 30 = 960 条序列。每个 label 维度都在乘,不是加——这是最容易被低估的地方。
经验阈值:单个 Prometheus 实例的活跃时间序列通常控制在 100 万以内比较稳妥,超过 200 万后查询延迟和内存压力会明显上升。一个 `status` label 带上完整的 HTTP 状态码(含几百种非标准码)加上几十个 endpoint,能轻松把基数推到十万量级。
危险的 label 来源有固定的几种:用户 ID、请求 ID、会话 ID、完整 URL 路径(含参数)、时间戳、错误消息全文。这些都是无界集合——它们没有「上界」这个概念,所以基数在理论上是无限的,任何静态上限配置都救不了。
相对安全的来源:方法、状态码类别(把状态码归并成 2xx/4xx/5xx 三类)、路由模板(`/users/:id` 而不是 `/users/12345`)、分片/实例标识。判断标准是「取值集合是否可枚举且规模可控」——如果答案是「不能」,就不要放进 label。
promtool tsdb analyze metrics.prom.gz
# 输出中看 Total series 与各 label 的基数贡献
# 逐指标查看基数
curl -s http://localhost:9090/api/v1/label/__name__/values | jq '.data | length'命名与 label 规范
指标名建议遵循 `<namespace>_<subsystem>_<unit>` 结构,单位作为后缀且用 base 单位:秒写 `_seconds` 而不是 `_ms`,字节写 `_bytes`。这样 `rate()` 和单位换算规则才一致——一个叫 `foo_duration_ms` 的指标在所有客户端里都会有人忘记换算。
Counter 的名称要体现「累计」语义。社区惯例是直接用名词:`http_requests_total`、`process_cpu_seconds_total`。加 `_total` 后缀不是强制的,但它让 review 的人一眼看出这是 Counter 而 Gauge,避免类型混用。Histogram 类同时产出 `_bucket`、`_sum`、`_count`,主名不带后缀。
label 命名用小写下划线,含义自解释:`method`、`status`、`endpoint`、`instance`。label 名本身也是基数维度——每个指标用 5 个 label 和用 3 个 label,序列数差距是量级的。所以能用一个 label 表达的,不要拆成两个。
同一语义只允许一处表达。常见事故是既有 `status` 又有 `status_class`,两者可能不一致,聚合时也无法对齐。规范化的做法是在客户端就把状态码归并成类别,只保留一个 label。
# 规范示例
http_requests_total{method,code_class,endpoint} # Counter,无 _total 之外的后缀
http_request_duration_seconds_bucket{le,endpoint} # Histogram
process_resident_memory_bytes # Gauge,单位明确在 review 阶段拦住高基数
最有效的防线是 CI 里跑 `promtool check metrics` —— 它会拒绝格式不合法的指标。更进一步的做法是把服务暴露的 `/metrics` 输出作为 CI 产物,用 `promtool tsdb analyze` 统计基数并设门禁:超过阈值直接让构建失败。这比上线后再在监控上发现要便宜得多。
运行时可以用 Prometheus 的 TSDB status API 观察实际状态:`/api/v1/status/tsdb` 返回按指标名分组的最热序列列表,`/api/v1/status/tsdb?limit=N` 可以拿到占用最高的那些 label 组合。这是排查「到底哪个指标把内存吃光了」最直接的入口。
对已经存在的高基数指标,止血手段有两个:一是加 metric_relabel_configs 在抓取端丢弃不需要的序列(这在 Prometheus 服务端配置,成本最低);二是在 exporter 端修复(成本更高但更彻底)。两者都要评估丢弃规则对历史数据与告警规则的影响,避免规则静默失效。
长期看,给关键指标设基数告警是值得的:`prometheus_tsdb_head_series` 本身就是内置指标,对它做阈值告警可以在膨胀早期发现。多数团队的问题不是不知道怎么修,而是发现得太晚——等到查询超时,已经有一堆仪表盘和告警规则依赖了那些高基数序列,删除的代价极高。
promtool check metrics < app.txt
promtool tsdb analyze metrics.txt
curl -s "http://localhost:9090/api/v1/status/tsdb?limit=15"
# 抓取端丢弃高基数序列
metric_relabel_configs:
- source_labels: [user_id]
regex: ".*"
action: drop