凌晨被告警叫醒,打开看板:CPU 70%、QPS 正常、GC 时间不长,八十个图没有一个能回答"用户现在到底受不受影响"。指标体系的第一个问题从来不是"能不能采到",而是"这个图给谁看、看完要做什么决定"。
RED 与 USE 是两把不同的尺子:一把量服务,一把量资源。混着用会得出"CPU 高所以要扩容"这类结论,而真正的原因可能是下游慢导致的线程堆积。
1. 四种指标类型
| 类型 | 语义 | 聚合特性 | 典型用途 |
|---|---|---|---|
| Counter | 单调递增的累计值 | rate 后可跨实例求和 |
请求数、错误数、字节数 |
| Gauge | 瞬时值 | 可求和,求平均常无意义 | 队列深度、连接数、线程数 |
| Histogram | 分桶计数 + 总和 + 计数 | 桶可跨实例合并,再算分位 | 延迟、请求/响应大小 |
| Summary | 客户端算好的分位 | 分位不可跨实例合并 | 单进程内部分位(慎用) |
选型的唯一判据是你要不要跨实例聚合。要,就必须用 Histogram;Summary 一旦跨实例相加就是错的,而错得很安静。
2. Histogram 分桶怎么选
management:
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
slo:
http.server.requests: 50ms,100ms,200ms,500ms,1s,2s
| 原则 | 说明 |
|---|---|
| 覆盖 SLO 阈值附近 | 阈值 300ms,桶里必须有 250/300/400 这种边界 |
| 按倍数而非等距 | 10/20/40/80/160 比 10/20/30/40 更有用 |
| 桶不宜过多 | 每个桶一组时间序列,直接乘进基数 |
| 改边界要留痕 | 边界一变,历史曲线不可比,需在变更记录里注明 |
用 slo 而非手写桶 |
Micrometer 的 SLO 边界会同时产出 le 标签,便于 histogram_quantile |
按 Micrometer 1.13.x 的行为核对:percentiles-histogram 打开后才导出 _bucket 序列,没打开时 histogram_quantile 直接无数据——这是"分位数查不出来"的第一号原因。
3. RED:服务视角
| 信号 | 定义 | 指标形态 | 告警用法 |
|---|---|---|---|
| Rate | 每秒请求数 | rate(..._count[5m]) |
突降(流量断了)比突增更值得报 |
| Errors | 失败占比 | 5xx 或业务失败 / 总数 | 与 SLO 预算挂钩 |
| Duration | 延迟分布 | _bucket 算分位 |
分位超阈值且持续 |
# 错误率
sum(rate(http_server_requests_seconds_count{job="order", status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{job="order"}[5m]))
# p99:先按 le 合并桶,再算分位
histogram_quantile(0.99,
sum by (le) (rate(http_server_requests_seconds_bucket{job="order"}[5m])))
RED 的三个信号要一起看:只有错误率没有流量,分不清"错误变多"还是"流量掉了";只有延迟没有流量,分不清"变慢"还是"没人来"。
4. USE:资源视角
| 信号 | 定义 | 例子 | 判读 |
|---|---|---|---|
| Utilization | 资源忙碌时间占比 | CPU、磁盘 IO、带宽 | 看拐点,不看绝对值 |
| Saturation | 排队程度 | run queue、连接池等待、队列深度 | 持续大于 0 就该关注 |
| Errors | 资源层错误 | 网卡丢包、磁盘错误、OOM kill | 出现即查 |
# 磁盘:利用率与加权 IO 时间(后者近似排队量)
rate(node_disk_io_time_seconds_total[5m])
rate(node_disk_io_time_weighted_seconds_total[5m])
USE 的价值在于它比 RED 更早:饱和度上升时用户还没感知,等到错误率上来已经晚了。但 USE 单独使用会误导——CPU 70% 对一个 IO 密集型服务完全正常,对另一个服务可能已经是瓶颈前兆。
5. 两套怎么组合
| 层次 | 方法论 | 回答的问题 |
|---|---|---|
| 服务 | RED | 用户是否受影响 |
| 实例 | RED(按实例分组)+ USE | 是哪一台 |
| 依赖(DB/缓存/MQ) | RED + USE | 是不是下游拖的 |
| 主机 / 容器 | USE | 资源是否成为瓶颈 |
下钻顺序固定:服务 RED 异常 → 按实例切 RED → 该实例 USE → 横向到依赖。反过来(先看 CPU)会走向"CPU 高所以是它",而高 CPU 常常是结果而不是原因——例如下游变慢导致线程堆积、重试风暴。
6. 延迟指标的四个陷阱
| 陷阱 | 表现 | 正确做法 |
|---|---|---|
| 用平均值 | 平均 50ms,p99 2s,用户仍投诉 | 至少看 p95/p99 |
| 跨实例平均分位 | 各实例 p99 求平均 ≠ 全局 p99 | 先合并桶再算分位 |
| 只统计成功请求 | 超时被排除,延迟反而变好看 | 分母包含失败与超时 |
| 分位相加 | p99 + p99 无意义 | 用 Histogram 合并 |
# 正确:合并桶后算分位
histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket[5m])))
# 错误:对已经是分位的序列再平均
avg(http_server_requests_seconds{quantile="0.99"})
第二条在实际事故里最常见:二十个实例各自 p99 正常,全局 p99 超标——因为少数实例贡献了长尾,一平均就被稀释掉了。
7. 告警设计
| 原则 | 反例 | 做法 |
|---|---|---|
| 症状优先于原因 | CPU > 80% 就 page |
错误率、延迟超 SLO 才 page |
| 多条件组合 | 单点毛刺即告警 | rate + 持续时间同时满足 |
for 抑制抖动 |
瞬时抖动刷屏 | for: 5m |
| 分级与升级 | 全部 P1 | P1 用户可见,P2 容量风险,P3 趋势 |
| 带 runbook | 告警只说"异常" | 附首个排查命令与链接 |
groups:
- name: red
rules:
- alert: OrderHighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{job="order", status=~"5.."}[5m]))
/ sum(rate(http_server_requests_seconds_count{job="order"}[5m])) > 0.02
for: 5m
labels: {severity: page}
annotations:
summary: "下单错误率 > 2%(5 分钟)"
runbook_url: "https://wiki/runbook/order-5xx"
原因告警不是不能留,而是不该直接 page。CPU 高、GC 频繁这类信号放进"诊断面板"或 P3 通知,让它们在排查时被看见,而不是在凌晨叫醒人。
8. SLO 与错误预算
| 概念 | 定义 | 例子(30 天) |
|---|---|---|
| SLI | 可测量的服务质量指标 | 成功请求比例、p99 延迟 |
| SLO | SLI 的目标值 | 99.9% |
| 错误预算 | 1 - SLO 换算成额度 |
约 43 分钟不可用,按 99.9% 核对 |
| 燃尽率 | 预算消耗速度 | 1 小时烧掉 5% → 高燃尽,提前告警 |
# 燃尽倍数:错误率 / 预算率(SLO 99.9% → 预算率 0.001)
(1 - (sum(rate(http_server_requests_seconds_count{status!~"5.."}[1h]))
/ sum(rate(http_server_requests_seconds_count[1h])))) / 0.001
预算烧完之后要有明确动作:冻结非必要发布、把工程时间切到可靠性、复盘触发点。没有动作的预算制度会退化成一张报表——数字每天都在,没人因此改变行为。
9. 黄金信号及其局限
| 信号 | 覆盖 | 局限 |
|---|---|---|
| 延迟 / 流量 / 错误 / 饱和度 | 服务健康状况 | 不覆盖业务正确性 |
| — | — | "下单成功但金额算错"不会触发任何一条 |
补法是加业务指标:下单成功率、支付成功率、库存扣减一致率、退款率。它们和 RED 是两层——RED 说"系统是否在正常工作",业务指标说"工作得对不对"。
10. 基数爆炸:标签是最贵的资源
| 标签 | 基数风险 | 处理 |
|---|---|---|
userId / orderId / traceId |
无上限 | 绝不作为指标标签 |
含 id 的 url |
高 | 用模板化路由 /orders/{id} |
| 错误消息原文 | 极高 | 归一化成 error_code |
service / instance / status / method / route |
可控 | 可以作为标签 |
// 反例:用户维度进指标标签,序列数随用户数线性增长
Counter.builder("order.created").tag("userId", uid).register(registry).increment();
// 正例:用户维度留给日志与 span,指标只保留低基数维度
Counter.builder("order.created").tag("channel", channel).register(registry).increment();
基数一旦爆掉,症状不是"报错",而是采集变慢、查询超时、存储暴涨,而且很难回溯定位到是哪次提交引入的。上线前用 count by (__name__) 之类的方式抽查序列数,成本远低于事后清理。
11. 看板的四层组织
| 层 | 内容 | 回答 |
|---|---|---|
| 概览 | SLO、错误预算、核心 RED | 现在有没有事 |
| 服务 | 每个服务的 RED + 依赖调用 | 是哪个服务 |
| 实例 | 实例级 RED + USE | 是哪一台 |
| 依赖 | DB / 缓存 / MQ 的 RED + USE | 是不是下游 |
四层的意义是每层只回答一个问题。把八十个图堆在一页,等于没有分层:看的人要自己找哪条曲线异常,而这正是告警该干的活。
12. 排错:六种指标体系病灶
| 现象 | 原因 | 处理 |
|---|---|---|
histogram_quantile 无数据 |
没开 percentiles-histogram |
打开并确认 _bucket 序列存在 |
| 分位数突然失真 | 桶边界被改过 | 修改变更记录,或换新指标名重来 |
| 存储暴涨 | 高基数标签 | 找出高基数标签并降维 |
| 告警天天响没人管 | 原因告警 page、无 runbook | 改症状告警 + 补处置步骤 |
| 扩容后延迟没降 | 瓶颈在依赖而非本机 | 按 RED → 实例 → 依赖的顺序重查 |
| 平均值正常但用户投诉 | 长尾被平均 | 改看 p95/p99,并核对失败请求是否被排除 |
第一条和最后一条都指向同一件事:指标看错了比没有指标更危险,因为它会给出确定的错误结论。
13. 边界:指标回答不了的问题
| 场景 | 指标的局限 | 该补什么 |
|---|---|---|
| 单次请求为什么慢 | 只有聚合视角 | 链路追踪(见 S16-X11) |
| 代码里哪一行热 | 只有方法级或接口级 | profiling |
| 数据是否正确 | 指标衡量不了语义 | 业务校验、对账 |
| 偶发一次抖动 | 被聚合抹平 | 日志与事件 |
| 根因链 | 只知道"哪一段异常" | 追踪 + 变更记录 |
一句话:指标负责发现"有异常",追踪负责解释"异常在哪",日志负责说明"为什么"。 三者缺一条,排障就会退化成猜。
14. 自测
- 为什么 Summary 的分位不能跨实例合并,而 Histogram 可以?
slo边界与percentiles-histogram各自起作用在哪一步?- RED 里如果只有错误率没有流量,会误判成什么?
- 二十个实例 p99 都正常、全局 p99 超标,最可能是什么原因?
- 为什么原因类告警(CPU、GC)不该直接 page?
- 错误预算烧完之后,团队应该做的三件事是什么?