跳至内容

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_activitypg_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 wait

idle 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 的瞬时值不能代表历史从未发生。

先固定四个维度

调查窗口至少固定:

  1. cls/ins:哪个 cluster/instance,primary 还是 replica;
  2. datname:哪个 database;
  3. UTC time range:与用户错误/发布窗口对齐;
  4. 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=1

holder 故意等教学 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,图空了”只是动作成功,不是问题解决。

延伸阅读


上一节:咨询锁与跨行协调 · 返回本章目录 · 下一节:实战:库存扣减与支付幂等 · 查看全书目录 · 查看索引中心

最后更新于