# 先定义“慢”

LLMS 索引： [llms.txt](/llms.txt)

---

一次有效诊断从可检验的事件定义开始。若工单只有“数据库今天很慢”，不同的人会任意选择时间、实例和指标，最后得到彼此矛盾却都无法反驳的故事。

最小事件边界可以写成：

```text
2026-07-29 12:04:00Z–12:09:00Z，
checkout/create 路由 18432 次请求，
p99 从同星期基线 180 ms 升至 2.4 s，
吞吐从 70 req/s 降至 61 req/s，错误率从 0.1% 升至 1.8%，
影响租户 A/B；只发生在写流量，读流量与其他区域正常。
```

这段话尚未宣称 PostgreSQL 有问题，却已经给出了可在 trace、应用、连接池、代理和数据库各层复核的同一范围。

## 8.1.1 延迟分位数、吞吐、并发与错误率 {#item-8-1-1}

延迟不是一个数，而是一组事件的分布。p99 的含义是：在给定样本集合中，约 99% 的观测不超过该值；它必须和以下限定一起出现：

- 测量对象：HTTP 路由、业务动作、数据库 query family，还是单个 backend；
- 起止时间与时区；
- 成功请求、全部请求还是某类错误；
- 样本数与采样方式；
- histogram bucket、客户端 timer 或 tracing span 的来源；
- 是否包含重试、排队、结果传输和超时。

没有样本数的 p99 很危险。一个窗口只有 40 个请求时，所谓 p99 几乎就是最大样本；一个窗口有 100 万请求时，尾部才有足够事件可供分层。也不要把十个一分钟 p99 求平均来冒充十分钟 p99：分位数一般不可加、不可平均。只有保留可合并的原始分布或边界一致的 histogram bucket，才可能重新计算整体分位数。

分位数还必须和吞吐、并发、错误率联合阅读：

| 现象 | 可能解释 | 还缺什么证据 |
|---|---|---|
| p99 上升，吞吐稳定 | 少数参数/租户退化、锁尖峰、下游抖动 | 慢样本标签、参数、wait |
| p50/p99 同时上升，吞吐下降 | 普遍排队或资源饱和 | 队列长度、CPU/I/O、连接占用 |
| 延迟下降，错误率上升 | 请求更早失败或超时，不是性能改善 | SQLSTATE、HTTP 状态、取消来源 |
| QPS 上升，均值稳定，总数据库时间上升 | 单次没变但预算消耗变大 | calls、total_exec_time、容量余量 |
| 并发上升，吞吐不再增长 | 到达瓶颈后开始排队 | active sessions、pool wait、服务率 |

在近似稳定系统中，Little 定律给出：

$$
L = \lambda W
$$

其中 $L$ 是系统内平均并发，$\lambda$ 是吞吐，$W$ 是平均停留时间。它不是用来从三个噪声瞬时值“算根因”的，而是做一致性检查：若吞吐近似不变、停留时间增大，系统内请求数理应上升；若监控没有上升，可能是测量边界不同、采样漏掉队列，或请求已在上游被拒绝。

一个适合告警和诊断的 SLI 组合通常至少包括：

```text
event count
success/error/timeout count
latency histogram or quantiles
throughput
in-flight/queue depth
measurement scope and labels
```

平均值仍有价值，它适合预算、容量和与累计 query time 对齐；但它不能替代尾延迟。反过来，p99 能说明尾部体验，却不能告诉你该 query family 消耗了多少总 CPU/执行时间。

## 8.1.2 单次慢、持续慢与整体退化 {#item-8-1-2}

“慢”至少要从时间范围和影响范围两个轴分类：

| 时间形态 | 局部范围 | 全局范围 |
|---|---|---|
| 单次/尖峰 | 特定参数、一次锁等待、一次冷读 | checkpoint、主机抖动、网络事件 |
| 周期性 | 报表、定时任务、批量发布 | 定时备份、周期流量、共享资源争用 |
| 持续性 | query plan/数据分布变化 | 容量不足、配置/版本变更、长期膨胀 |

单次慢样本适合做法证：保存 trace、PID、参数、日志与当时 wait，但不能据此证明系统长期退化。持续慢需要同口径的对照窗口，例如：

```text
incident:  12:04Z–12:09Z
baseline:  前七天同星期、同五分钟、相近 QPS
control:   同集群未受影响的路由/租户/只读实例
```

“昨天平均 10 ms，今天一次 2 s”混合了统计粒度；“所有数据库都慢”也常把一个共享连接池、一个可用区或一个应用版本误写成数据库全局。诊断前依次收窄：

1. 哪个用户动作或后台任务；
2. 哪个路由、租户、参数簇与返回规模；
3. 哪个应用版本、实例、可用区和服务入口；
4. 哪个 PostgreSQL cluster、instance、database、user、application；
5. 哪个查询族与事务；
6. 是执行慢、等待慢、排队慢，还是消费结果慢。

持续退化也不等于“从某次发布起就一定由发布造成”。发布是高优先级假设，因为时间顺序与作用范围吻合；它仍需机制证据，例如 SQL shape 改变、calls/rows 改变、plan estimate 偏离、连接池并发改变，或错误重试放大负载。

建议把事件分为三个状态：

- **未确认**：用户报告存在，但监控口径或范围尚未复现；
- **已确认**：同一时间窗的 SLI 证明退化，根因未知；
- **已归因**：存在机制、对照与反证，修复后同口径指标恢复。

这样能避免在“已确认慢”和“已证明 PostgreSQL 根因”之间偷换概念。

## 8.1.3 应用时间、排队时间与数据库时间 {#item-8-1-3}

端到端时间可以用核账式分解：

```text
用户等待
≈ 网关/应用排队
 + 业务代码与外部调用
 + 等待连接池 slot
 + 建连/认证/路由
 + PostgreSQL 内规划、执行与数据库等待
 + 结果传输与客户端消费
 + 序列化/响应发送
```

这不是说各组件一定能无缝相加。计时器可能使用不同 clock，span 可能重叠，重试会创建多次数据库调用，连接池也可能没有 trace。分解的价值是找出“缺失时间”：例如应用记录 2.4 s，而数据库完成日志与 server-side `EXPLAIN ANALYZE` 都约 80 ms，那么剩余 2.32 s 不能继续用索引解释。

PostgreSQL planner 的 cost 不包含把值转换为文本以及把结果传给客户端的时间；`EXPLAIN ANALYZE` 的服务器执行时间也不等于用户看到的完整取数时间。相反，连接池排队发生在 backend 分配之前，`pg_stat_activity` 根本看不到这批尚未进入 PostgreSQL 的请求。

要让时间可以关联，最低限度应统一：

- 日志与展示使用 UTC，并保留原始 timezone；
- duration 使用 monotonic clock，wall clock 只做跨系统关联；
- trace/request ID 贯穿应用，database span 记录 database/service；
- PostgreSQL `application_name` 能映射应用/worker；
- 日志保存 PID 与 session identity，而不是只保存 SQL 文本；
- 明确数据库 span 是“从发起驱动调用到返回”，还是 server duration。

一个常见对照：

```text
request span                       2400 ms
  pool acquire                     1700 ms
  database driver call              620 ms
    server log duration              85 ms
    ClientWrite observed            500 ms
  application work                  80 ms
```

这里 PostgreSQL 只用了约 85 ms 计算，但连接池排队与客户端接收共同制造了“SQL 调用 620 ms”。给查询加索引几乎不会改变 1700 ms 排队，也不能修复 500 ms 的慢消费；正确方向是分别调查 pool saturation 与返回规模/客户端读速。

本节的产出不是一张性能图，而是一份写入[诊断记录模板](/labs/ch08/hypothesis-template.md)的事件边界：

```text
谁慢、哪里慢、何时慢、慢多少、多少样本、
当时流量/并发/错误怎样、与哪个基线相比、
端到端时间已分解多少、尚有多少无法解释。
```

有了这条边界，下一节才开始从会话和查询统计定位 PostgreSQL 内部范围。

---

[返回本章目录](../) · [下一节：从会话到语句定位范围](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
