跳至内容
8.4 建立而不是猜测假设

8.4 建立而不是猜测假设

假设不是“可能是磁盘”“可能缺索引”的清单。它必须把机制写成一条可被事实推翻的预测:

因为 tenant 1 的真实选择率为 90%,generic plan 仍估 0.1%,
所以 hot parameter 会读取远多于估算的行;
若强制 custom plan 且其他条件不变,estimate/actual 应接近,
访问路径或资源量应随之改善。

优先级由“现有证据支持度 × 用户影响 × 最小验证成本 ÷ 风险”决定,而不是由团队最熟悉什么决定。

8.4.1 计划与估算问题

计划假设应在 wait 排查之后进入。一个正在 Lock 等待的 backend,即使计划里有 Seq Scan,当前不返回的直接原因仍是锁;一个 ClientWrite backend 可能已经算出大量结果,planner cost 又不包含把结果传给客户端的时间。

确认计划方向时,从第一个显著偏差节点而非根节点名称开始:

query semantics and representative parameter
  → estimated rows vs actual rows × loops
  → filter/recheck rows and join multiplicity
  → buffers/WAL/temp/settings
  → custom vs generic plan
  → statistics age/distribution/extended statistics
  → predicate/index/partition expression match

可量化 cardinality error:

$$ E = \max\left( \frac{\text{estimate}}{\text{actual}}, \frac{\text{actual}}{\text{estimate}} \right) $$

若 actual 为 0,应单独描述“估算 N、实际 0”,不要用无穷大排序掩盖业务含义。高误差是调查入口,不是固定阈值自动修复;它是否影响路径选择、内存分配、join order 或响应目标,还要看对照。

常见假设与反证:

假设预测最小对照反证
统计陈旧estimate 偏离当前分布安全副本/fixture ANALYZE 前后estimate 与路径不变且数据分布本就一致
跨列相关缺失多 predicate 近似独立相乘extended statistics 前后单列条件就已偏离,或相关统计不改善
参数敏感 generic planhot/cold 共用 estimate/shapeforce generic/custom 对照两类参数 estimate/资源均相近
predicate 不可用于索引/裁剪条件落到 Filter,扫描范围扩大语义等价、可 sargable 的表达式扫描范围未变化
index 缺失选择性高且 heap/blocks 成本主导第 9 章 hypo index/安全建索引实验路径已合适,时间主要在 wait/client

禁用 planner 方法(如 enable_seqscan=off)最多是受控诊断探针,不是生产修复;它也不能绝对禁止所有路径。不要把“强迫 Index Scan 后这一次更快”直接推广为长期结论,必须覆盖参数分布、cache、并发、写成本和磁盘占用。

计划 change 同样不是根因。统计、参数、配置、数据量或版本变化可能让 planner 合理换路;判断回归要比较结果正确性、SLO、estimate、资源与 workload,而不是 diff 节点名。

8.4.2 锁、I/O、CPU、内存与临时文件

wait event 是“backend 在采样瞬间等待哪里”,不是完整时间账本。把它与 blocker、查询计划、累计资源和 OS 指标组合:

候选机制PostgreSQL 证据外部/对照证据常见误判
heavyweight lockLock/*pg_blocking_pids()pg_locksblocker 事务/应用身份只取消 waiter;把长 SQL 当 blocker
I/O waitIO/*、plan buffers、I/O timing、pg_stat_iodevice latency/queue、kernel/cache一次 IO sample 就断言磁盘故障
CPU 饱和多次 active 且无稳定 wait,calls/rows/plan 工作量CPU、run queue、steal/throttle“无 wait”等于 CPU;CPU 高就归目标 SQL
temp/spillplan sort/hash temp、temp_blks_*、temp file logmemory pressure、并发直接全局增大 work_mem
shared memory contentionLWLock/*BufferPin并发/版本/具体 wait 名把所有 Lock/LWLock 当行锁
checkpoint/WAL pressureWAL/checkpoint/I/O 指标、query WALstorage 与写 workload只凭时间相关归因某个 query

锁诊断必须保存等待边两端的:

PID + backend_start
user/database/application/client
xact_start/query_start/state
wait_event and lock modes
current/last query
transaction owner and business action

真正修复通常是缩短事务、统一锁顺序、避免事务中等外部 I/O、减少过宽写集合,或把冲突转为显式业务协议。增加 statement timeout 只是限制损失,不能替代根因修复。

I/O

EXPLAIN (ANALYZE, BUFFERS) 的 shared read/hit 是 executor 访问证据;track_io_timing 开启后可补 read/write time;pg_stat_io 给出 backend type/context/target 维度。它们都不能单独证明物理盘读取:PostgreSQL miss 仍可能由 kernel page cache 满足。应与设备层 latency、queue、吞吐和同主机 control workload 对照。

CPU

CPU 没有一个叫 CPU 的 wait event。backend 在执行用户态工作时往往 active 且 wait 为 NULL,但短采样也可能恰好落在两个 wait 之间。证明 CPU 瓶颈需要:

  • 多次采样而非一行 activity;
  • query calls/rows/plan work 与 CPU 时间窗同范围;
  • 主机 run queue、利用率、throttling/steal;
  • 限制并发或减少工作量后吞吐/延迟按模型变化。

内存与临时文件

sort/hash spill 是“该节点的内存预算与数据规模/并发不匹配”的证据。全局提高 work_mem 很危险,因为它不是整个实例固定池,而可能被一条 query 的多个节点、多个并发 backend 分别使用。优先:

  1. 确认 rows/width estimate 与返回规模;
  2. 减少不必要数据、改善计划;
  3. 对单一 role/session 做对照;
  4. 计算最坏并发内存;
  5. 观察 spill 改善、RSS/pressure 与其他 workload 副作用。

8.4.3 客户端取数、网络与连接池排队

数据库算得快,不代表用户收得快。第 8 章实验用一个大 COPY TO STDOUT 和受控慢 reader 复现:

state=active
wait_event_type=Client
wait_event=ClientWrite
blocking_pid_count=0

这组证据表示 server 正在尝试把数据写给客户端,而 socket backpressure 让它等待。可能原因包括:

  • 客户端逐行做昂贵处理,读取速度低;
  • 客户端线程暂停、GC 或 event loop 被阻塞;
  • 网络丢包、拥塞或带宽受限;
  • 返回行/列过多、payload 过大;
  • 游标/fetch size 与消费方式不合理;
  • 下游已放弃请求但连接尚未及时取消。

ClientRead 则表示 server 等客户端发数据。普通 idle backend 经常在 ClientRead 等下一条命令,这不是慢 SQL;active session 在 COPY FROM、协议交互等场景也可能等客户端。必须结合 state、query、协议阶段与应用 trace。

客户端慢消费的修复候选是限制返回规模、分页/流式语义、修复 consumer、调整驱动读取方式、网络与超时传播,而不是先建索引。索引也许能缩短产生第一批行的时间,却不能让慢 reader 更快接收 400 MB。

连接池排队位于另一个边界:

request arrives
  → waits for application/PgBouncer pool slot
  → obtains PostgreSQL backend/transaction
  → statement becomes visible in pg_stat_activity

未获得 slot 的请求不会出现在 pg_stat_activity。如果应用 p99 高、数据库 active sessions 刚好打满 pool size、server 单条执行仍快,应同时看:

  • application pool acquire duration、waiter count、timeout;
  • PgBouncer client/server active/waiting 与 pool mode;
  • HAProxy/service route、连接拒绝与 backend health;
  • PostgreSQL max_connections、可用连接与角色/database 限额;
  • 事务长度、连接泄漏、重试风暴与并发上限。

“把 pool size 加倍”也是需要实验的假设。若数据库已经 CPU/I/O 饱和,更多并发会增加排队与上下文切换;吞吐不升而尾延迟更差。连接池的作用是排队和保护下游,不是消灭容量边界。

一棵够用的初始假设树可以写成:

请求慢
├─ 尚未进入 PostgreSQL
│  ├─ 应用队列/连接池
│  ├─ 路由/建连/认证
│  └─ 上游重试或限流
├─ backend 正等待
│  ├─ Lock → blocker edge
│  ├─ IO/LWLock/BufferPin → 具体 wait + 资源
│  └─ ClientWrite/Read → client/protocol
├─ backend 正执行
│  ├─ estimate/path/join/scan
│  ├─ CPU/JIT/expression
│  └─ sort/hash/temp/WAL
└─ server 已完成
   ├─ 结果传输/消费
   └─ 应用后处理/下游

这棵树不是固定排障脚本。它的价值是强迫每个解释声明边界和证据,下一节再从中选一个最小、可逆的实验。


上一节:关联日志、指标与计划 · 返回本章目录 · 下一节:设计受控实验 · 查看全书目录 · 查看索引中心

最后更新于