主题
指标与可观测性体系
一句话:可观测性用指标回答「系统现在好不好」,Prometheus 把「拉取、存储、查询、告警」串成一条标准链路;代价是它只擅长聚合后的数值,说不清某一次请求慢在哪。
适用版本:Prometheus 2.5x / 3.x。示例默认值以官方文档为准。
为什么需要
只有日志时,判断「服务是否正常」要靠人翻错误堆栈,不同实例的日志格式还不一致。想回答「错误率是不是涨了」「P99 延迟有没有恶化」「哪台机器资源快满了」,日志要么算不出来,要么算得很贵。
指标把这些问题变成固定成本:每个服务暴露一组数值,Prometheus 周期性拉取并按标签存储,查询时用同一套 PromQL 聚合。代价是数值被预先聚合,丢失了单次请求的上下文;要定位「哪一次调用慢」,仍要回到链路追踪和日志。
核心机制
三种信号各回答一个问题
| 信号 | 回答什么 | 典型工具 | 成本 |
|---|---|---|---|
| 指标(Metrics) | 整体趋势、告警、容量 | Prometheus | 低,适合长期保存 |
| 链路(Traces) | 这一次请求慢在哪一跳 | Jaeger、SkyWalking | 高,通常采样 |
| 日志(Logs) | 当时到底发生了什么 | Loki、ELK | 中高,取决于留存 |
排障顺序通常是「指标发现异常 → 链路定位服务 → 日志看具体错误」。指标是入口,不是全部。
数据模型:一条时间序列由名字和标签决定
http_requests_total{job="order", instance="10.0.0.11:8080", code="500"} 12345 1710000000指标名加一组标签唯一确定一条时间序列(time series),时间戳和数值只是它上面的点。标签是查询和聚合的维度,也是成本的来源:每多一个取值,就多一条序列。把 userId、orderId、原始 URL 放进标签,序列数会随业务量爆炸,内存和查询一起恶化。
四种指标类型:选错类型,查询一定写错
| 类型 | 语义 | 查询方式 | 什么时候用 |
|---|---|---|---|
| Counter | 只增不减的累计值 | rate() / increase() | 请求数、错误数、字节数 |
| Gauge | 可增可减的瞬时值 | 直接读、delta() | 内存占用、连接数、队列长度 |
| Histogram | 按桶累计的分布 | histogram_quantile() | 延迟、响应大小,需要跨实例聚合 |
| Summary | 客户端算好的分位数 | 直接读 | 单实例精确分位,且不聚合 |
为什么延迟优先用 Histogram:Histogram 在服务端按 le(小于等于)分桶累计,多实例的桶可以直接相加,再还原分位数;Summary 的分位数是客户端算的,两个实例的 P99 不能相加,也无法求平均。Histogram 的代价是分位数是桶内线性插值,精度受桶边界限制。
拉模型与 up
Prometheus 按 scrape_interval 主动请求目标的 /metrics,解析文本、写入本地 TSDB。拉模型附带一个免费的健康信号:up{job="..."}。目标不可达时它的值是 0,不需要应用额外上报心跳。
推送只适合短生命周期任务(批处理、脚本),用 Pushgateway 暂存;常驻服务不要用推送,否则目标宕机后旧指标会一直被当成最新值。
RED 与 USE:两套互补的面板骨架
| 方法 | 视角 | 三个指标 | 用在 |
|---|---|---|---|
| RED | 服务 | Rate(每秒请求)、Errors(错误率)、Duration(延迟) | 微服务、API |
| USE | 资源 | Utilization(使用率)、Saturation(饱和度)、Errors(错误数) | CPU、内存、磁盘、连接池 |
RED 回答「用户感受到的服务好不好」,USE 回答「资源是不是瓶颈」。面板先放 RED,再用 USE 下钻到具体资源。
分位数:必须按 le 聚合
Histogram 暴露的是累计桶:
http_request_duration_seconds_bucket{le="0.1"} 100
http_request_duration_seconds_bucket{le="0.3"} 300
http_request_duration_seconds_bucket{le="1"} 800多实例的 P99 要先按 le 求和,再交给 histogram_quantile:
promql
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))漏掉 _bucket 会查到一个不存在的指标;漏掉 sum by (le) 会把每个实例的桶分开算,得到的不是全局 P99。常见查询:
promql
# QPS
sum(rate(http_requests_total[5m]))
# 错误率
sum(rate(http_requests_total{code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# 平均延迟(不是 P99)
sum(rate(http_request_duration_seconds_sum[5m]))
/ sum(rate(http_request_duration_seconds_count[5m]))
# 序列数最高的指标,用来查基数
topk(10, count by (__name__)({__name__=~".+"}))最小上手
写一个只抓自己的配置,十秒内验证链路通:
yaml
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]bash
docker run -d --name prom -p 9090:9090 \
-v "$PWD/prometheus.yml:/etc/prometheus/prometheus.yml" \
prom/prometheus:v2.55.1
# 期望 status 为 success,up 的值为 1
curl -sG localhost:9090/api/v1/query \
--data-urlencode 'query=up' | jq '.data.result'把业务服务的 /metrics 加进 static_configs,或接入服务发现,就完成了第一条采集链路。
生产动作
| 参数 | 默认值 | 说明 |
|---|---|---|
scrape_interval | 1m(全局默认,以官方文档为准) | 按目标调整,延迟指标不要粗于 15s |
scrape_timeout | 10s(以官方文档为准) | 必须小于 scrape_interval,否则抓取重叠 |
evaluation_interval | 1m(以官方文档为准) | 告警规则的评估周期 |
--storage.tsdb.retention.time | 15d(以官方文档为准) | 本地只保留这么久,更久要远程存储 |
排障路径:
- 面板没数据 →
up{job="xxx"}→ 为 0 先查目标进程、端口、网络和scrape_configs,不要先改查询。 - 抓取变慢 →
scrape_duration_seconds / scrape_interval→ 超过 0.8 说明目标或网络吃力,先看目标端/metrics的生成耗时。 - 内存持续上涨 →
prometheus_tsdb_head_series与topk(10, count by (__name__)({__name__=~".+"}))→ 先找新增的高基数指标,靠 relabel 丢弃或改标签设计。 - P99 明显偏离 → 检查查询是否带
_bucket和sum by (le)→ 再看桶边界是否覆盖目标延迟,缺桶时插值误差会很大。
告警至少覆盖三类:up == 0、5xx 比例、P99 超阈值;每条都加 for,避免瞬时抖动直接叫人。
边界
- 不是日志或链路系统:指标只有聚合值,定位单次异常要跳转到 trace 或日志。
- 分位数是近似值:Histogram 用桶内线性插值,桶边界设计不好时 P99 会失真;Summary 不能跨实例聚合。
- 基数是硬约束:标签取值随用户、订单、URL 增长时,序列数和内存会先于流量出问题。
- 本地 TSDB 不是长期存储:默认 15 天,磁盘损坏会丢数据;长期留存和跨集群查询要远程存储或 Thanos、Mimir 一类方案。
- 拉模型有覆盖盲区:NAT 后、短生命周期、无法暴露 HTTP 的目标要单独设计,Pushgateway 只适合批处理且要清理旧指标。
讲清楚
指标解决的是「系统整体好不好」:每个服务暴露数值,Prometheus 定时拉取,按指标名和标签存成时间序列,查询时用 PromQL 聚合。它和日志、链路的分工是:指标发现异常,链路定位哪一跳,日志看具体错误。 选类型要看查询方式:累计值用 Counter 配
rate,瞬时值用 Gauge,延迟用 Histogram。Histogram 按le分桶,多实例必须先sum by (le)再histogram_quantile,否则算出来的不是全局分位数。最容易被低估的成本是标签基数。把用户 ID 或原始 URL 放进标签,序列数会随业务量爆炸,内存和查询一起坏掉。生产上先守住这条,再谈面板和告警。