10.6 观察与诊断并发
并发故障有两个时间尺度:
historical:
lock/deadlock/rollback/latency metrics + logs + traces
live:
exact session → wait → blocker graph + transaction age + SQL指标告诉你“何时、影响多大”,catalog 告诉你“现在谁等谁”。杀会话只会改变 live graph,不会自动解释根因。
10.6.1 pg_stat_activity、pg_locks 与等待事件
state 与 wait_event 是两个维度
SELECT
pid,
backend_start,
xact_start,
query_start,
state_change,
datname,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
backend_xid,
backend_xmin,
query_id,
left(query, 500) AS query_sample
FROM pg_stat_activity
WHERE backend_type = 'client backend';state='active' 只表示 backend 正在执行 query;它仍可能:
active + Lock/transactionid → 等另一事务结束
active + Lock/relation → 等 table lock
active + Lock/advisory → 等 advisory key
active + Client/ClientWrite → server 等客户端读取
active + IO/... → I/O waitidle in transaction 则没有正在执行 query,却仍持有 transaction、snapshot 和 locks;它常比一条 active query 更危险。
完整 query/session 信息需要适当监控权限,例如受控 pg_read_all_stats;不要给普通应用 superuser。query text、参数、client_addr 可能含敏感数据,证据包应脱敏并设置保留期。
pg_locks 是 lockable object 明细
SELECT
lock.pid,
activity.application_name,
lock.locktype,
lock.mode,
lock.granted,
lock.fastpath,
lock.waitstart,
lock.relation::regclass AS relation_name,
lock.page,
lock.tuple,
lock.transactionid,
lock.virtualxid,
lock.classid,
lock.objid,
lock.objsubid
FROM pg_locks AS lock
LEFT JOIN pg_stat_activity AS activity
ON activity.pid = lock.pid
WHERE lock.database = (
SELECT oid
FROM pg_database
WHERE datname = current_database()
)
OR lock.database IS NULL
ORDER BY lock.granted, lock.waitstart, lock.pid;字段按 locktype 才有意义。relation、transactionid、virtualxid、tuple、advisory、object 等可能共同出现。
row-level lock 的常见观察陷阱:holder 的 row locks 通常不逐行显示在 pg_locks;当另一个 transaction 等该 row 时,它经常表现为等待 holder 的 transaction ID:
wait_event_type=Lock
wait_event=transactionid所以只搜 locktype='tuple' 会漏掉真实 row blocker。
直接使用 pg_blocking_pids()
手工用 pg_locks 所有 nullable identity columns 做 self join 容易错,也难处理 soft blockers。PostgreSQL 提供:
SELECT
waiter.pid,
waiter.backend_start,
waiter.application_name,
waiter.wait_event_type,
waiter.wait_event,
pg_blocking_pids(waiter.pid) AS blocker_pids
FROM pg_stat_activity AS waiter
WHERE cardinality(pg_blocking_pids(waiter.pid)) > 0;展开成边:
SELECT
waiter.pid AS waiter_pid,
waiter.backend_start AS waiter_epoch,
waiter.application_name AS waiter_app,
waiter.wait_event_type,
waiter.wait_event,
blocker.pid AS blocker_pid,
blocker.backend_start AS blocker_epoch,
blocker.application_name AS blocker_app,
blocker.state AS blocker_state,
blocker.xact_start AS blocker_xact_start,
blocker.query_start AS blocker_query_start
FROM pg_stat_activity AS waiter
CROSS JOIN LATERAL unnest(
pg_blocking_pids(waiter.pid)
) AS edge(blocker_pid)
LEFT JOIN pg_stat_activity AS blocker
ON blocker.pid = edge.blocker_pid;LEFT JOIN 很重要:prepared transaction 可能成为 blocker 却没有普通 backend activity row。遇到 blocker PID/活动缺失,应同时查 prepared transactions 和 lock catalog,而不是假设采样坏了。
一次采样只是瞬间
短等待可能在两次查询之间消失。实时事件要:
- 设置低成本周期采样或 exporter;
- 保存 UTC timestamp;
- 保留 session identity epoch;
- 关联 log/trace/query id;
- 不因某次 snapshot 为空就否定历史 lock spike;
- 不用高频全字段 query 把监控本身变成压力。
本章屏障让 row-lock edge 停住,便于可靠捕获;生产没有这种配合。
10.6.2 从 Pigsty 定位锁等待与长事务
从影响面缩到 exact graph
在 Pigsty v4.4 的当前仪表盘体系中,可按以下顺序:
PGSQL Activity
sessions/load/active-idle/locks overview
PGSQL Xacts
transaction rate, rollback, locks, transaction time
PGCAT Locks
current activity and lock waits from catalog
PGSQL Query / PGCAT Query
affected query family and statistics
PGLOG Overview / Session
deadlock, lock wait, timeout and SQLSTATE context
PGSQL Persist / Replication
long snapshot, WAL, replica side effects仪表盘名称/布局会随版本变化,以当前 Dashboard 文档 为准,不把截图坐标写进 runbook。
常用时间序列包括:
pg_lock_count{mode=...}
pg_db_deadlocks
pg_db_ixact_time
transaction commit/rollback rate
session state/time
query calls/runtime
WAL and replica lag具体 metric/label 以当前 Pigsty Metrics reference 为准。counter 要用 rate/increase 并注意 reset epoch;deadlocks=0 的瞬时值不能代表历史从未发生。
先固定四个维度
调查窗口至少固定:
cls/ins:哪个 cluster/instance,primary 还是 replica;datname:哪个 database;- UTC time range:与用户错误/发布窗口对齐;
- query/application identity:谁受影响、谁可能持锁。
然后回答:
等待数量/持续时间是否超过 SLO?
是 Lock 还是 Client/IO/其他 wait?
一条 root blocker 还是多条独立冲突?
blocker 是 active、idle in transaction、DDL、autovacuum、
prepared transaction 还是业务 writer?
transaction age 从何时开始?
是否伴随 deployment、batch、schema change、retry storm?看到 lock count 高不一定是问题:已 granted 的非冲突 locks 很正常。重点是 ungranted wait、阻塞时长、队列扩散和用户 SLI。
40001 不一定在 lock 面板出现
SSI SIReadLock 不造成常规 blocking;serialization failure 可能没有一条长 lock wait 曲线。需要 application/driver 暴露 SQLSTATE 40001 和 retry attempts,并关联:
- transaction family;
- abort/success rate;
- active connections;
- transaction duration;
- query plan/predicate lock 粒度;
- hot key/tenant;
- deploy/version。
同理,deadlock victim 很快被 abort,live graph 已消失;pg_db_deadlocks 与 PostgreSQL log 才保留历史。
transaction age 是放大器
长 transaction:
- 持锁更久;
- 保留 old snapshot;
- 增加 SSI overlap;
- 阻碍 vacuum cleanup;
- 放大 WAL/replication/DDL 等待;
- 让 retry 代价更大。
因此并发性能优化常常不是改 lock mode,而是把 remote call、用户思考、巨大 batch 移出 transaction,并治理 pool 中 idle in transaction。
10.6.3 保存阻塞图,而不是先杀会话
动作前证据包
最小 live artifact:
captured_at UTC
cluster/instance/database
waiter PID + backend_start + user/app/client
waiter state/wait/query/xact/query start
every blocker edge
blocker PID + backend_start + state/query/xact age
relevant pg_locks rows
query_id / normalized query / parameters where safe
deployment/job/request identity
impact/SLO本章由并发协调器生成的
row-lock-graph.csv,一次关系是:
waiter:
pg36-ch10-row-lock-waiter
active / Lock / transactionid
blocker:
pg36-ch10-row-lock-holder
active / Lock / advisory
edge count=1holder 故意等教学 barrier;生产则要问 holder 为何尚未 commit。
找 root blocker,而非随便处理叶子
取消 waiter 只减少一个症状,root blocker 仍可能阻塞几十个请求。应把 graph 沿边向上追到:
no blocker
or cycle/deadlock
or prepared transaction再按影响与业务 owner 决策。root 也可能是正在执行必须完成的财务事务、migration 或恢复操作;“阻塞最多”不自动等于“应该杀”。
cancel 与 terminate 不同
SELECT pg_cancel_backend($pid);请求取消当前 query。若 session 在显式 transaction 中,query error 会使 transaction failed,但 client 若不 rollback,仍可能继续占用连接/某些事务资源。
SELECT pg_terminate_backend($pid);终止整个 backend,未提交 transaction rollback,client 断开。它的影响更大,可能触发应用 retry storm 或留下外部副作用未知状态。
执行前必须重新验证 PID epoch,避免 PID reuse:
SELECT pid, backend_start, datname, usename, application_name
FROM pg_stat_activity
WHERE pid = $pid
AND backend_start = $captured_epoch
AND datname = $expected_db
AND application_name = $expected_app;还要确认:
- 是否为 autovacuum/background/replication/system backend;
- transaction rollback 的业务影响;
- application 是否会自动 retry;
- external effect 是否 commit-unknown;
- 是否有 owner/incident approval;
- 动作后怎样验收 graph 与数据不变量。
自动化绝不能按 xact_start 最老或 application name 模糊匹配批量 kill。
处理后仍要解释根因
完成止血后保存 after:
edge disappeared
waiter outcomes
rollback/commit
application error/retry
business invariant/reconciliation
remaining workers/locks再修:
- transaction scope;
- lock order;
- missing index 导致访问过多 rows;
- queue claim;
- external call in transaction;
- timeout/retry storm;
- DDL 发布方式;
- leaked pool connection;
- missing idempotency。
“杀掉 blocker,图空了”只是动作成功,不是问题解决。
延伸阅读
- PostgreSQL 18:Monitoring Database Activity
- PostgreSQL 18:Viewing Locks
- PostgreSQL 18:
pg_blocking_pids - Pigsty:Dashboard
- Pigsty:Metrics
上一节:咨询锁与跨行协调 · 返回本章目录 · 下一节:实战:库存扣减与支付幂等 · 查看全书目录 · 查看索引中心