8.1 先定义“慢”
一次有效诊断从可检验的事件定义开始。若工单只有“数据库今天很慢”,不同的人会任意选择时间、实例和指标,最后得到彼此矛盾却都无法反驳的故事。
最小事件边界可以写成:
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 延迟分位数、吞吐、并发与错误率
延迟不是一个数,而是一组事件的分布。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 组合通常至少包括:
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 单次慢、持续慢与整体退化
“慢”至少要从时间范围和影响范围两个轴分类:
| 时间形态 | 局部范围 | 全局范围 |
|---|---|---|
| 单次/尖峰 | 特定参数、一次锁等待、一次冷读 | checkpoint、主机抖动、网络事件 |
| 周期性 | 报表、定时任务、批量发布 | 定时备份、周期流量、共享资源争用 |
| 持续性 | query plan/数据分布变化 | 容量不足、配置/版本变更、长期膨胀 |
单次慢样本适合做法证:保存 trace、PID、参数、日志与当时 wait,但不能据此证明系统长期退化。持续慢需要同口径的对照窗口,例如:
incident: 12:04Z–12:09Z
baseline: 前七天同星期、同五分钟、相近 QPS
control: 同集群未受影响的路由/租户/只读实例“昨天平均 10 ms,今天一次 2 s”混合了统计粒度;“所有数据库都慢”也常把一个共享连接池、一个可用区或一个应用版本误写成数据库全局。诊断前依次收窄:
- 哪个用户动作或后台任务;
- 哪个路由、租户、参数簇与返回规模;
- 哪个应用版本、实例、可用区和服务入口;
- 哪个 PostgreSQL cluster、instance、database、user、application;
- 哪个查询族与事务;
- 是执行慢、等待慢、排队慢,还是消费结果慢。
持续退化也不等于“从某次发布起就一定由发布造成”。发布是高优先级假设,因为时间顺序与作用范围吻合;它仍需机制证据,例如 SQL shape 改变、calls/rows 改变、plan estimate 偏离、连接池并发改变,或错误重试放大负载。
建议把事件分为三个状态:
- 未确认:用户报告存在,但监控口径或范围尚未复现;
- 已确认:同一时间窗的 SLI 证明退化,根因未知;
- 已归因:存在机制、对照与反证,修复后同口径指标恢复。
这样能避免在“已确认慢”和“已证明 PostgreSQL 根因”之间偷换概念。
8.1.3 应用时间、排队时间与数据库时间
端到端时间可以用核账式分解:
用户等待
≈ 网关/应用排队
+ 业务代码与外部调用
+ 等待连接池 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。
一个常见对照:
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 与返回规模/客户端读速。
本节的产出不是一张性能图,而是一份写入诊断记录模板的事件边界:
谁慢、哪里慢、何时慢、慢多少、多少样本、
当时流量/并发/错误怎样、与哪个基线相比、
端到端时间已分解多少、尚有多少无法解释。有了这条边界,下一节才开始从会话和查询统计定位 PostgreSQL 内部范围。
返回本章目录 · 下一节:从会话到语句定位范围 · 查看全书目录 · 查看索引中心