# RED 方法 · 服务只需要三个指标

> Rate、Errors、Duration —— 三个指标就能覆盖一个服务的健康。这一章讲清 RED 为什么完备、每个信号的埋点与 PromQL 写法（可直接抄）、以及三个高频陷阱：错误率的分母、延迟必须用分位数、以及为什么不要看平均值。

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/red-method
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# RED 方法 · 服务只需要三个指标

**RED** 是 Tom Wilkie 提出的服务监控方法论，主张：**任何请求驱动的服务，监控三个指标就够了**。

| 信号 | 全称 | 含义 |
| --- | --- | --- |
| **R** | **Rate** | 每秒请求数 |
| **E** | **Errors** | 每秒失败的请求数（或错误率） |
| **D** | **Duration** | 请求耗时的分布 |

三个而已。但它们**完备**——这一章讲清为什么，以及**具体怎么落地**。

## 为什么这三个就够

<Callout type="info">
  **因为对一个“接收请求、处理、返回”的服务，用户只在乎三件事：**能不能用**（Errors）、**快不快**（Duration）、**有多少人在用**（Rate）。这三个问题穷尽了“服务健康”的定义。**

  - **Rate** —— 不只是“业务量”。它是**其他两个指标的上下文**：错误率 5% 在 10 QPS 和 10 万 QPS 下是完全不同的事故等级。Rate 突然掉到 0 本身也是重大信号（**没有流量往往比有错误更可怕**——说明上游根本没进来）。
  - **Errors** —— 直接对应“用户能不能用”。
  - **Duration** —— 直接对应“用户等多久”。**慢和坏在用户眼里没区别**：一个 30 秒才返回的成功响应，和一个失败没有本质差异。

  **完备性的论证**：一个服务出问题，表现只可能是“报错了”（E）、“变慢了”（D）、或“没流量了”（R 掉零）。**你想不出第四种用户能感知的故障形态。** 这就是 RED 敢说“三个就够”的底气。

  而且这三个**天然是[第 0 部分说的“便宜”的指标](https://daiw.org/manual/observability/three-pillars)**——都是聚合量，[不随流量线性增长](https://daiw.org/manual/observability/three-pillars)，可以[全量长存](https://daiw.org/manual/observability/three-pillars)。
</Callout>

## 落地：一个 histogram 全搞定

一个漂亮的事实：**RED 的三个信号，可以从[一个 histogram](https://daiw.org/manual/observability/metric-types) 全部导出**。

```go
// 只需要埋这一个指标
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())
```

<Callout type="info">
  **[histogram 自带 `_count`](https://daiw.org/manual/observability/metric-types)，而 `_count` 就是一个 counter——所以 Rate 和 Errors 都能从它算出来，不用额外埋。** 三个信号的 PromQL（**可以直接抄**）：

  ```text
  # 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 部分讲过](https://daiw.org/manual/observability/metric-types)，histogram 的桶可加，所以先把所有实例的桶加起来，再算分位数，得到的才是**真正的全局 P99**。
</Callout>

## 陷阱一：错误率的分母

<Callout type="warn">
  **最常见的错误：用“错误数”告警，而不是“错误**率**”。**

  ```text
  # ❌ 错:绝对数没有上下文
  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% 错误率，立刻炸响告警——但这可能只是一次无关紧要的重试。所以生产实践里常**同时要求“比率超标”和“绝对量够大”**：

  ```text
  # 错误率 > 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)
  ```

  [第 5 部分讲告警](https://daiw.org/manual/observability/alert-thresholds)时会深入这个模式，以及更优雅的解法（[SLO 燃烧率](https://daiw.org/manual/observability/slo-burn-rate)）。
</Callout>

## 陷阱二：什么算“错误”

看似显然，其实要想清楚：

- **5xx 算错误** —— 显然，服务端的锅。
- **4xx 算不算？** —— **通常不算**。400/404 是**客户端**的问题，你的服务正确地拒绝了它，这是**成功的工作**。但 **429（限流）** 和 **401/403 突然飙升**值得单独看——前者说明你扛不住了，后者可能是鉴权系统炸了。
- **超时算不算？** —— **算**，而且这是最重要的一类。注意：客户端超时时，**你的服务端可能根本没记到这个请求失败**（它还在慢慢跑）——**服务端指标会漏掉这类错误**。
- **业务失败算不算？** —— HTTP 200 但 body 里 `{"code": -1}`？**这取决于你的定义**，但用户显然认为它失败了。**如果你的服务这么设计，RED 的 E 必须包含它**，否则监控全绿而用户全炸。

<Callout type="warn">
  **推论：**只看 HTTP status 的 RED 是不完整的**。** 两个盲区：
  - **业务错误藏在 200 里** —— 必须让埋点认识“业务成功”的定义。
  - **客户端超时服务端看不见** —— 服务端的 Duration 只统计**它自己跑完**的请求，那些被客户端提前放弃的**不在样本里**。这会让你的 P99 **系统性偏乐观**。

  **解法：关键路径上，同时在[客户端侧埋 RED](https://daiw.org/manual/observability/red-method)**。客户端视角的延迟和错误才是**用户的真实体验**——服务端说 P99 是 200ms，客户端可能因为网络、重试、排队而体验到 2s。这两个数**都要有**，差值本身就是信息。
</Callout>

## 陷阱三：永远不要看平均延迟

<Callout type="warn">
  **平均延迟（`_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` 然后告警。**
</Callout>

## 关键：标签用 route，不是 path

<Callout type="warn">
  **RED 埋点最致命的基数错误：**把原始 URL path 当标签**。**

  ```go
  // ❌ 灾难:每个订单 ID 一条 series
  httpDuration.WithLabelValues("GET", "/orders/12345", "200")   // → 无限基数!

  // ✅ 正确:用路由模板
  httpDuration.WithLabelValues("GET", "/orders/:id", "200")     // → 一条 series
  ```

  `/orders/12345`、`/orders/12346`……每个 ID 都会创建**一条新 series**，series 数随订单数**无限增长**——这是 [TSDB 内存爆炸](https://daiw.org/manual/observability/defining-metrics)的头号原因，也是[第 1 部分那个 `scrape_series_added` 持续 > 0](https://daiw.org/manual/observability/scrape-loop) 的典型元凶。

  **必须用路由模板**（`/orders/:id`）。绝大多数 Web 框架都能拿到“匹配的路由”而非“原始路径”——用它。如果框架不给，**宁可自己维护一个白名单做归一化**，也不要放原始 path 进去。[下一部分](https://daiw.org/manual/observability/defining-metrics)会给完整的基数控制规则。
</Callout>

## RED 的完整落地清单

```mermaid
flowchart TD
    A["① 埋一个 histogram<br/>http_request_duration_seconds"] --> B["② 标签:method, route(模板!), status"]
    B --> C["③ 桶:覆盖你的 SLO 区间"]
    C --> D["④ R = rate(_count)<br/>E = rate(_count{status=~'5..'}) / rate(_count)<br/>D = histogram_quantile(0.99, sum by(le))"]
    D --> E["⑤ 关键路径:客户端侧也埋一份"]
    E --> F["⑥ 业务错误?让埋点认识它"]
```

**对每一个服务都做这一套。** 六步而已，但它给你的覆盖，超过大多数团队几百个面板的总和。

## 小结与下一章

**RED = Rate、Errors、Duration**，主张请求驱动的服务**三个指标就够**——因为用户可感知的故障只有“[报错 / 变慢 / 没流量](https://daiw.org/manual/observability/red-method)”三种形态，想不出第四种。落地上**[一个 histogram 就能导出全部三个](https://daiw.org/manual/observability/metric-types)**（`_count` 即 counter），PromQL 记得 **`sum by (le)` 才能跨实例算对分位数**。三个陷阱：①**错误率要有分母**（[绝对数没有上下文，但低流量下比率会抖，故常与绝对量取 `and`](https://daiw.org/manual/observability/alert-thresholds)）；②**想清楚什么算错误**（[4xx 通常不算、超时必算、业务错误藏在 200 里](https://daiw.org/manual/observability/red-method)；且**客户端超时服务端看不见，故 P99 系统性偏乐观 → 关键路径客户端也要埋**）；③**永远别看平均延迟**（[长尾被抹平，至少看 P50 + P99，两者差距本身是信号](https://daiw.org/manual/observability/red-method)）。埋点最致命的基数错误是**用原始 path 而非路由模板**。

服务测完了，底下的**资源**怎么测？下一章看 USE，它的精髓在一个常被忽略的信号上。👉 [USE 方法](https://daiw.org/manual/observability/use-method)。

## 参考资料

- [1] Tom Wilkie「The RED Method: key metrics for microservices architecture」（Weaveworks）。
- [2] Prometheus 官方文档：Histograms and summaries、Best practices — Instrumentation。
- [3] 关联[指标类型选型](https://daiw.org/manual/observability/metric-types)、[基数控制](https://daiw.org/manual/observability/defining-metrics)、[告警阈值](https://daiw.org/manual/observability/alert-thresholds)。
