跳到正文

指标与可观测性体系 ​

一句话:可观测性用指标回答「系统现在好不好」,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_interval1m(全局默认,以官方文档为准)按目标调整,延迟指标不要粗于 15s
scrape_timeout10s(以官方文档为准)必须小于 scrape_interval,否则抓取重叠
evaluation_interval1m(以官方文档为准)告警规则的评估周期
--storage.tsdb.retention.time15d(以官方文档为准)本地只保留这么久,更久要远程存储

排障路径:

  • 面板没数据 → 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 放进标签,序列数会随业务量爆炸,内存和查询一起坏掉。生产上先守住这条,再谈面板和告警。

把一门技术讲透到既懂也会用