# 观察与诊断并发

LLMS 索引： [llms.txt](/llms.txt)

---

并发故障有两个时间尺度：

```text
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` 与等待事件 {#item-10-6-1}

### state 与 wait_event 是两个维度

```sql
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；它仍可能：

```text
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 明细

```sql
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：

```text
wait_event_type=Lock
wait_event=transactionid
```

所以只搜 `locktype='tuple'` 会漏掉真实 row blocker。

### 直接使用 `pg_blocking_pids()`

手工用 `pg_locks` 所有 nullable identity columns 做 self join 容易错，也难处理 soft blockers。PostgreSQL 提供：

```sql
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;
```

展开成边：

```sql
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 定位锁等待与长事务 {#item-10-6-2}

### 从影响面缩到 exact graph

在 Pigsty v4.5 的当前仪表盘体系中，可按以下顺序：

```text
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 文档](https://pigsty.io/docs/pgsql/dashboard/) 为准，不把截图坐标写进 runbook。

常用时间序列包括：

```text
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](https://pigsty.io/docs/pgsql/metric/) 为准。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：谁受影响、谁可能持锁。

然后回答：

```text
等待数量/持续时间是否超过 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 保存阻塞图，而不是先杀会话 {#item-10-6-3}

### 动作前证据包

最小 live artifact：

```text
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
```

本章由[并发协调器](/labs/ch10/run_concurrency.py)生成的
`row-lock-graph.csv`，一次关系是：

```text
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 沿边向上追到：

```text
no blocker
or cycle/deadlock
or prepared transaction
```

再按影响与业务 owner 决策。root 也可能是正在执行必须完成的财务事务、migration 或恢复操作；“阻塞最多”不自动等于“应该杀”。

### cancel 与 terminate 不同

```sql
SELECT pg_cancel_backend($pid);
```

请求取消当前 query。若 session 在显式 transaction 中，query error 会使 transaction failed，但 client 若不 rollback，仍可能继续占用连接/某些事务资源。

```sql
SELECT pg_terminate_backend($pid);
```

终止整个 backend，未提交 transaction rollback，client 断开。它的影响更大，可能触发应用 retry storm 或留下外部副作用未知状态。

执行前必须重新验证 PID epoch，避免 PID reuse：

```sql
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：

```text
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](https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW)
- [PostgreSQL 18：Viewing Locks](https://www.postgresql.org/docs/18/monitoring-locks.html)
- [PostgreSQL 18：`pg_blocking_pids`](https://www.postgresql.org/docs/18/functions-info.html)
- [Pigsty：Dashboard](https://pigsty.io/docs/pgsql/dashboard/)
- [Pigsty：Metrics](https://pigsty.io/docs/pgsql/metric/)

---

[上一节：咨询锁与跨行协调](../05/) · [返回本章目录](../) · [下一节：实战：库存扣减与支付幂等](../07/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
