8.1 先定义“慢”
一次有效诊断从可检验的事件定义开始。若工单只有“数据库今天很慢”,不同的人会任意选择时间、实例和指标,最后得到彼此矛盾却都无法反驳的故事。
最小事件边界可以写成:
这段话尚未宣称 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$ 是平均停留时间。它不是用来从三个噪声瞬时值“算根因”的,而是做一致性检查:若吞吐近似不变、停留时间增大,系统内请求数理应上升;若监控没有上升,可能是测量边界不同、采样漏掉队列,或请求已在上游被拒绝。
一个适合告警和诊断的 SLI 组合通常至少包括:
平均值仍有价值,它适合预算、容量和与累计 query time 对齐;但它不能替代尾延迟。反过来,p99 能说明尾部体验,却不能告诉你该 query family 消耗了多少总 CPU/执行时间。
8.1.2 单次慢、持续慢与整体退化
“慢”至少要从时间范围和影响范围两个轴分类:
| 时间形态 | 局部范围 | 全局范围 |
|---|---|---|
| 单次/尖峰 | 特定参数、一次锁等待、一次冷读 | checkpoint、主机抖动、网络事件 |
| 周期性 | 报表、定时任务、批量发布 | 定时备份、周期流量、共享资源争用 |
| 持续性 | query plan/数据分布变化 | 容量不足、配置/版本变更、长期膨胀 |
单次慢样本适合做法证:保存 trace、PID、参数、日志与当时 wait,但不能据此证明系统长期退化。持续慢需要同口径的对照窗口,例如:
“昨天平均 10 ms,今天一次 2 s”混合了统计粒度;“所有数据库都慢”也常把一个共享连接池、一个可用区或一个应用版本误写成数据库全局。诊断前依次收窄:
- 哪个用户动作或后台任务;
- 哪个路由、租户、参数簇与返回规模;
- 哪个应用版本、实例、可用区和服务入口;
- 哪个 PostgreSQL cluster、instance、database、user、application;
- 哪个查询族与事务;
- 是执行慢、等待慢、排队慢,还是消费结果慢。
持续退化也不等于“从某次发布起就一定由发布造成”。发布是高优先级假设,因为时间顺序与作用范围吻合;它仍需机制证据,例如 SQL shape 改变、calls/rows 改变、plan estimate 偏离、连接池并发改变,或错误重试放大负载。
建议把事件分为三个状态:
- 未确认:用户报告存在,但监控口径或范围尚未复现;
- 已确认:同一时间窗的 SLI 证明退化,根因未知;
- 已归因:存在机制、对照与反证,修复后同口径指标恢复。
这样能避免在“已确认慢”和“已证明 PostgreSQL 根因”之间偷换概念。
8.1.3 应用时间、排队时间与数据库时间
端到端时间可以用核账式分解:
这不是说各组件一定能无缝相加。计时器可能使用不同 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。
一个常见对照:
这里 PostgreSQL 只用了约 85 ms 计算,但连接池排队与客户端接收共同制造了“SQL 调用 620 ms”。给查询加索引几乎不会改变 1700 ms 排队,也不能修复 500 ms 的慢消费;正确方向是分别调查 pool saturation 与返回规模/客户端读速。
本节的产出不是一张性能图,而是一份写入诊断记录模板的事件边界:
有了这条边界,下一节才开始从会话和查询统计定位 PostgreSQL 内部范围。
返回本章目录 · 下一节:从会话到语句定位范围 · 查看全书目录 · 查看索引中心