RED 方法 · 服务只需要三个指标
Rate、Errors、Duration —— 三个指标就能覆盖一个服务的健康。这一章讲清 RED 为什么完备、每个信号的埋点与 PromQL 写法(可直接抄)、以及三个高频陷阱:错误率的分母、延迟必须用分位数、以及为什么不要看平均值。
RED 方法 · 服务只需要三个指标
RED 是 Tom Wilkie 提出的服务监控方法论,主张:任何请求驱动的服务,监控三个指标就够了。
| 信号 | 全称 | 含义 |
|---|---|---|
| R | Rate | 每秒请求数 |
| E | Errors | 每秒失败的请求数(或错误率) |
| D | Duration | 请求耗时的分布 |
三个而已。但它们完备——这一章讲清为什么,以及具体怎么落地。
为什么这三个就够
因为对一个“接收请求、处理、返回”的服务,用户只在乎三件事:能不能用(Errors)、快不快(Duration)、有多少人在用(Rate)。这三个问题穷尽了“服务健康”的定义。
- Rate —— 不只是“业务量”。它是其他两个指标的上下文:错误率 5% 在 10 QPS 和 10 万 QPS 下是完全不同的事故等级。Rate 突然掉到 0 本身也是重大信号(没有流量往往比有错误更可怕——说明上游根本没进来)。
- Errors —— 直接对应“用户能不能用”。
- Duration —— 直接对应“用户等多久”。慢和坏在用户眼里没区别:一个 30 秒才返回的成功响应,和一个失败没有本质差异。
完备性的论证:一个服务出问题,表现只可能是“报错了”(E)、“变慢了”(D)、或“没流量了”(R 掉零)。你想不出第四种用户能感知的故障形态。 这就是 RED 敢说“三个就够”的底气。
而且这三个天然是第 0 部分说的“便宜”的指标——都是聚合量,不随流量线性增长,可以全量长存。
落地:一个 histogram 全搞定
一个漂亮的事实:RED 的三个信号,可以从一个 histogram 全部导出。
// 只需要埋这一个指标
httpDuration := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 请求耗时",
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
[]string{"method", "route", "status"}, // 注意:route 不是 path!
)
// 每个请求处理完
httpDuration.WithLabelValues("POST", "/orders/:id", "200").Observe(elapsed.Seconds())histogram 自带 _count,而 _count 就是一个 counter——所以 Rate 和 Errors 都能从它算出来,不用额外埋。 三个信号的 PromQL(可以直接抄):
# R —— 每秒请求数(按服务聚合)
sum(rate(http_request_duration_seconds_count[5m])) by (service)
# E —— 每秒错误数
sum(rate(http_request_duration_seconds_count{status=~"5.."}[5m])) by (service)
# E' —— 错误率(更常用:错误占比)
sum(rate(http_request_duration_seconds_count{status=~"5.."}[5m])) by (service)
/
sum(rate(http_request_duration_seconds_count[5m])) by (service)
# D —— P99 延迟(注意 sum by (le) 才能跨实例聚合)
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)sum by (le) 那一步是关键——第 1 部分讲过,histogram 的桶可加,所以先把所有实例的桶加起来,再算分位数,得到的才是真正的全局 P99。
陷阱一:错误率的分母
最常见的错误:用“错误数”告警,而不是“错误率**”。**
# ❌ 错:绝对数没有上下文
sum(rate(http_requests_total{status=~"5.."}[5m])) > 10
# ✅ 对:比率有上下文
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.01“每秒 10 个错误”在大促(10 万 QPS)时是 0.01% 的正常抖动,在深夜(50 QPS)时是 20% 的重大事故。同一个绝对值,含义完全相反——用绝对数告警必然导致“白天漏报、晚上误报”。
但比率也有它的陷阱:低流量时比率会剧烈抖动。 深夜只有 2 个请求,错了 1 个 = 50% 错误率,立刻炸响告警——但这可能只是一次无关紧要的重试。所以生产实践里常同时要求“比率超标”和“绝对量够大”:
# 错误率 > 1% 且 每秒错误数 > 0.1(避免低流量误报)
(sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01)
and
(sum(rate(http_requests_total{status=~"5.."}[5m])) > 0.1)陷阱二:什么算“错误”
看似显然,其实要想清楚:
- 5xx 算错误 —— 显然,服务端的锅。
- 4xx 算不算? —— 通常不算。400/404 是客户端的问题,你的服务正确地拒绝了它,这是成功的工作。但 429(限流) 和 401/403 突然飙升值得单独看——前者说明你扛不住了,后者可能是鉴权系统炸了。
- 超时算不算? —— 算,而且这是最重要的一类。注意:客户端超时时,你的服务端可能根本没记到这个请求失败(它还在慢慢跑)——服务端指标会漏掉这类错误。
- 业务失败算不算? —— HTTP 200 但 body 里
{"code": -1}?这取决于你的定义,但用户显然认为它失败了。如果你的服务这么设计,RED 的 E 必须包含它,否则监控全绿而用户全炸。
推论:只看 HTTP status 的 RED 是不完整的。 两个盲区:
- 业务错误藏在 200 里 —— 必须让埋点认识“业务成功”的定义。
- 客户端超时服务端看不见 —— 服务端的 Duration 只统计它自己跑完的请求,那些被客户端提前放弃的不在样本里。这会让你的 P99 系统性偏乐观。
解法:关键路径上,同时在客户端侧埋 RED。客户端视角的延迟和错误才是用户的真实体验——服务端说 P99 是 200ms,客户端可能因为网络、重试、排队而体验到 2s。这两个数都要有,差值本身就是信息。
陷阱三:永远不要看平均延迟
平均延迟(_sum / _count)几乎总是在骗你。
假设 100 个请求:99 个 10ms,1 个 5000ms。
- 平均 = (99×10 + 5000) / 100 ≈ 60ms —— 看起来很健康!
- P99 = 5000ms —— 真相:1% 的用户等了 5 秒。
平均值把长尾抹平了,而长尾恰恰是你要找的东西。更糟的是:平均值对异常值极度敏感又极度不敏感——一个 60 秒的超时能把平均拉高,但一千个 200ms 的慢请求可能一点看不出来。
正确的做法是看分位数,而且至少看两个:
- P50(中位数) —— 典型用户的体验。
- P99 / P99.9 —— 最差那批用户的体验。大规模系统里,P99 影响的绝对人数非常可观:100 万请求/天,P99 就是 1 万次糟糕体验。
P50 和 P99 的差距(尾部延迟比)本身就是个信号:差距突然拉大 = 出现了长尾问题(GC、锁竞争、慢查询、某个坏实例)。
唯一该看 _sum 的场合:算总耗时(比如“这个服务今天总共花了多少 CPU 秒”)。永远不要用它除以 _count 然后告警。
关键:标签用 route,不是 path
RED 埋点最致命的基数错误:把原始 URL path 当标签。
// ❌ 灾难:每个订单 ID 一条 series
httpDuration.WithLabelValues("GET", "/orders/12345", "200") // → 无限基数!
// ✅ 正确:用路由模板
httpDuration.WithLabelValues("GET", "/orders/:id", "200") // → 一条 series/orders/12345、/orders/12346……每个 ID 都会创建一条新 series,series 数随订单数无限增长——这是 TSDB 内存爆炸的头号原因,也是第 1 部分那个 scrape_series_added 持续 > 0 的典型元凶。
必须用路由模板(/orders/:id)。绝大多数 Web 框架都能拿到“匹配的路由”而非“原始路径”——用它。如果框架不给,宁可自己维护一个白名单做归一化,也不要放原始 path 进去。下一部分会给完整的基数控制规则。
RED 的完整落地清单
对每一个服务都做这一套。 六步而已,但它给你的覆盖,超过大多数团队几百个面板的总和。
小结与下一章
RED = Rate、Errors、Duration,主张请求驱动的服务三个指标就够——因为用户可感知的故障只有“报错 / 变慢 / 没流量”三种形态,想不出第四种。落地上**一个 histogram 就能导出全部三个(_count 即 counter),PromQL 记得 sum by (le) 才能跨实例算对分位数。三个陷阱:①错误率要有分母**(绝对数没有上下文,但低流量下比率会抖,故常与绝对量取 and);②想清楚什么算错误(4xx 通常不算、超时必算、业务错误藏在 200 里;且客户端超时服务端看不见,故 P99 系统性偏乐观 → 关键路径客户端也要埋);③永远别看平均延迟(长尾被抹平,至少看 P50 + P99,两者差距本身是信号)。埋点最致命的基数错误是用原始 path 而非路由模板。
服务测完了,底下的资源怎么测?下一章看 USE,它的精髓在一个常被忽略的信号上。👉 USE 方法。