跳至内容

34.3 失控查询、锁与事务

“杀慢查询”不是故障判型。一个耗时最长的 session 可能是阻塞根节点、也可能是等待者; 可能正在做有价值的恢复,也可能已经超过用户 deadline;可能可以安全 cancel,也可能 一旦 terminate 就触发数小时回滚。动作必须绑定 query、transaction、application、 owner 与业务语义。

34.3.1 识别高消耗查询和阻塞根节点

当前现场与历史重查询分开

pg_stat_activity 说明此刻有哪些 backend、状态与等待;pg_stat_statements 聚合的是 一段时间内同类语句的执行统计。前者适合回答“谁现在占着资源”,后者适合回答“哪类 语句长期贡献最多”。不能用累计榜单代替当前事故现场。

一个不导出完整 SQL 文本的当前投影:

SELECT pid,
       usename,
       application_name,
       state,
       wait_event_type,
       wait_event,
       clock_timestamp() - query_start AS query_age,
       clock_timestamp() - xact_start AS xact_age,
       backend_xid,
       backend_xmin,
       query_id
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND pid <> pg_backend_pid()
ORDER BY xact_start NULLS LAST, query_start NULLS LAST;

对历史工作量,可按目标排序,而不是永远按 total time:

SELECT queryid,
       calls,
       total_exec_time,
       mean_exec_time,
       rows,
       shared_blks_read,
       shared_blks_written,
       temp_blks_read,
       temp_blks_written,
       wal_bytes
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

版本、扩展列和统计起点应随报告一起记录。统计 reset 或重启后的短窗口不能与一周基线 直接比较。

找根阻塞者,不要只杀等待者

WITH RECURSIVE lock_tree AS (
  SELECT a.pid,
         a.application_name,
         a.xact_start,
         pg_blocking_pids(a.pid) AS blockers,
         ARRAY[a.pid] AS path
  FROM pg_stat_activity AS a
  WHERE cardinality(pg_blocking_pids(a.pid)) > 0

  UNION ALL

  SELECT b.pid,
         b.application_name,
         b.xact_start,
         pg_blocking_pids(b.pid),
         t.path || b.pid
  FROM lock_tree AS t
  CROSS JOIN LATERAL unnest(t.blockers) AS p(pid)
  JOIN pg_stat_activity AS b ON b.pid = p.pid
  WHERE NOT b.pid = ANY(t.path)
)
SELECT * FROM lock_tree;

根 blocker 可能显示 idle in transaction,因为它已经执行完持锁语句,正在等客户端下 一条命令。仅筛选 state='active' 会漏掉它。也要排除 autovacuum、logical worker、 备份和维护工作等不同 backend_type,不要把每个 PID 都当作应用会话。

高消耗不是自动有罪

取消前回答:

is this the root blocker or a victim?
is its client deadline already expired?
is it OLTP, migration, maintenance, backup, recovery, or batch?
what locks and objects does it own?
what rows/WAL/temp/I/O has it already produced?
does it carry a business idempotency key?
who owns the decision?

对 query_id 做执行计划分析时,转到第 10、11 章的方法;在线事故中不要在主库上无界 执行 EXPLAIN ANALYZE 复现一条未知重查询。

34.3.2 cancel、terminate 与回滚成本

两个函数的边界

SELECT pg_cancel_backend(:pid);
SELECT pg_terminate_backend(:pid);

pg_cancel_backend 向目标 backend 发送取消当前 query 的请求。session 通常仍存在; 当前事务会进入错误状态,客户端需要 ROLLBACKpg_terminate_backend 终止整个 session,连接断开,未提交事务由服务器回滚。两者都需要相应权限;不要通过给应用 超级用户来获得事故处置能力。

优先级一般是:

application cooperative cancel/deadline
  -> pg_cancel_backend exact PID
      -> wait and verify
          -> pg_terminate_backend exact PID when justified

“exact”至少绑定:

pid + backend_start
database + user + application_name
query_id / transaction age
incident run id or ticket

PID 会复用。先查 PID、过几分钟再裸 terminate,可能命中完全不同的新连接。执行动作的 SQL 应在同一事务/语句里重验识别字段。

cancel 不等于立即释放全部资源

  • query 可能在到达可中断点前继续运行;
  • client 可能自动重试同一工作;
  • 事务未 rollback 前仍可能持有锁;
  • parallel workers 与 leader 的收敛需要时间;
  • remote/extension 调用的中断语义取决于组件;
  • query 已经产生的 WAL、temp 或脏页不会凭空消失。

terminate 也不是免费的“更强 cancel”。大型事务回滚需要遍历并清理状态,可能继续消耗 CPU、I/O 与 WAL,且锁直到 transaction end 才释放。事故时最差选择是连续 terminate 多个大事务,再因为“资源没降”重启数据库,把恢复工作叠加到启动路径。

动作后必须复核

SELECT pid, backend_start, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE pid = :pid;

同时观察:

root blocker disappeared?
dependent waiters made progress?
pool stopped recreating the work?
rollback/recovery still consuming I/O?
user success and tail latency recovered?

如果应用立刻重建同一 session,数据库端 cancel 只是短暂擦除症状,真正控制点在 admission 和 retry。

34.3.3 长事务和大事务结束前先评估后果

“长”与“大”是两个维度

long but small
  idle transaction holds snapshot/locks for hours

short but large
  bulk UPDATE changes millions of rows in minutes

long and large
  migration/ETL both retains old state and creates rollback work

长事务主要风险是 lock、backend_xmin、vacuum 回收与连接占用;大事务还带来 WAL、 dirty buffers、replication lag、commit/rollback 延迟。prepared transaction 即使没有 活动 session,也可长期保留锁和 XID 状态:

SELECT gid,
       prepared,
       owner,
       database,
       clock_timestamp() - prepared AS age
FROM pg_prepared_xacts
ORDER BY prepared;

不要看到 prepared transaction 就 ROLLBACK PREPARED。它属于两阶段提交协议,必须先 与 transaction manager/业务 ledger 对账,判断应 commit 还是 rollback。

结束前的后果清单

identity:
  pid_backend_start: ...
  application_owner: ...
  transaction_or_job_id: ...
business:
  partial_external_effects: ...
  idempotency_or_reconciliation: ...
database:
  locks: ...
  xmin_retention: ...
  rows_wal_temp_estimate: ...
  replicas_and_archive_effect: ...
rollback:
  expected_resource_cost: ...
  observation_query: ...
  escalation_timeout: ...

若事务包含数据库外部副作用,PostgreSQL rollback 只能撤销数据库内未提交状态,不能 撤回已经发出的邮件、支付或消息。此时需要业务补偿,不是更强的 terminate。

更好的预防

  • 为交互式事务设置合理的 idle_in_transaction_session_timeout
  • 为不同工作负载设置 statement/lock timeout,而不是一个全局极小值;
  • 大批处理分块提交,并让每块有可恢复 checkpoint;
  • schema change 使用受控 lock timeout 和发布门;
  • 统一 application_name、query tag 与业务 job id;
  • 为重要操作保留可对账 token。

timeout 是保护栏,不是容量。设置后还要验证应用如何处理取消、事务错误和重试。


上一节:连接风暴与排队失控 · 返回本章目录 · 下一节:CPU、内存、I/O 与 OOM · 查看全书目录 · 查看索引中心

最后更新于