5.6 实战:观察一笔订单事务
这个实验不追求制造最大并发,而是把一条最小 blocking edge 观察完整:blocker 写入未提交版本,普通 reader 读旧版本,waiter 写同一行并等待;observer 同时采集 activity、blocking PID 与 locks,最后取消精确实验 query,让两个事务都回滚并验证状态。
风险分级:
verify/observe:R0·观察,只读 catalog、sample row 和 WAL positions;transaction:R2·受控演练,触发三个预期 error,并 rollback 一次真实 UPDATE;blocking:R2·受控演练,两个 session 对订单 1002 UPDATE,调用pg_cancel_backend取消精确 blocker;all/review:R2·受控演练,执行前验、全部实验和后验。
即使不提交,写入仍产生 tuple/WAL/lock。只在已确认可演练的 Pigsty L1 或本地测试库运行;生产只能复用只读取证方法,不能复用“主动注入阻塞”。
5.6.1 从 SQL 观察会话、快照、锁和 WAL 位置
先使用绝对路径指向自己的私有 service file,不把 password 写进命令历史:
export PGSERVICEFILE=/absolute/private/path/pg_service.conf
export PGSERVICE=pg36-admin
cd static/labs/ch05
export PG36_EVIDENCE_DIR="$PWD/evidence/ch05/$(date -u +%Y%m%dT%H%M%SZ)"
psql -X -w "service=$PGSERVICE" \
-c '\conninfo' \
-c "SELECT current_database(), pg_is_in_recovery();"
./task.sh verifycontext guard 要求 database=pg36_shop、primary/writable、可 SET ROLE pg36_owner,且 shop_private.schema_version 是 ch04-v1。verify.sql复用完整 ch04 验收,再额外要求:
active_lab_workers=0
order_1002_fingerprint=2bfa6eac30b9a1cfa2d51e98c4e98332
relation_checksum=f8a7bfae59c6d16cd323abecfefe1014观察一个没有 write XID 的 transaction
运行:
./task.sh observe
sed -n '1,120p' "$PG36_EVIDENCE_DIR/observe.txt"observe.sql显式开启:
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED READ ONLY;然后只取一次 pg_current_snapshot(),解析其边界,并从 pg_stat_activity 反查自己的 backend_xid/backend_xmin。典型片段:
transaction_isolation=read committed
transaction_read_only=on
assigned_xid_before_write=<none>
snapshot=959:959:
snapshot_xmin=959
snapshot_xmax=959
snapshot_in_progress_count=0
backend_snapshot=<none>|959|<none>|<none>XID 数值随实例推进,不能 hard-code。要观察的是:transaction/snapshot 已存在,read-only backend 却可以没有 assigned write XID;backend_xmin 暴露它对清理 horizon 的影响。
同一脚本输出:
tuple_diagnostic=1002|xmin|xmax|ctid|request_fingerprint
wal_positions=insert_lsn|write_lsn|flush_lsnxmin/xmax/ctid只用于版本取证。反复运行 rollback 实验后,一个仍可见 tuple 甚至可能有非零 xmax,这正说明不能从 xmax<>0 直接推断“已删除”。三个 WAL position 分别表示 insert、write、flush 进度;短暂相等也不证明未来始终没有 pending WAL。
观察 failed transaction 与 savepoint
./task.sh transaction
sed -n '1,120p' "$PG36_EVIDENCE_DIR/transaction-errors.stderr"
sed -n '1,160p' "$PG36_EVIDENCE_DIR/wal-rollback.txt"task 要求 error stream 中每个 SQLSTATE 恰好一次:
22012 division_by_zero
25P02 in_failed_sql_transaction
23514 check_violation前两项证明 error 后普通 SQL 不能继续;第三项发生在 savepoint 后,ROLLBACK TO 恢复 transaction,再完成一条合法 UPDATE,最终 outer rollback。脚本不靠本地化错误消息判断,而用稳定 SQLSTATE。
WAL probe 要同时满足:
wal_insert_advanced=t
state_restored=tLSN 是实例全局位置,差值中可能包含其他 backend;本实验在隔离 L1 中只用它证明“回滚路径仍有 WAL 活动”,不把字节数当单条 SQL benchmark。
5.6.2 从 Pigsty 观察连接、事务与等待指标
SQL catalog 是当前瞬时状态,Pigsty/Grafana 提供时间序列、层级导航和跨组件上下文。两者不是替代关系:
| 问题 | PostgreSQL 原生证据 | Pigsty v4.4 入口 |
|---|---|---|
| cluster 是否出现 session/load/lock 波峰 | pg_stat_activity、database stats | PGSQL Activity |
| 某 instance 的 active/idle/idle-in-tx 演变 | activity + backend timestamps | PGSQL Session |
| TPS/QPS、transaction 与 lock 趋势 | database/xact stats、locks | PGSQL Xacts |
| WAL、XID、checkpoint、archive、I/O 是否异常 | WAL/admin/stats views | PGSQL Persist |
| 当前 database 的 activity 与 lock wait 明细 | activity、pg_blocking_pids、pg_locks | PGCAT Locks |
官方 v4.4 dashboard 索引把 PGSQL Activity 定义为 cluster 级 session/load/QPS/TPS/locks,把 Persist 定义为 WAL/XID/checkpoint/archive/I/O,把 PGCAT Locks 定义为 catalog-derived activity 与 lock wait。部署若定制 dashboard、collector 或版本,面板与 metric 可能变化,所以正文依赖的是问题映射,不是像素位置。
让连接可归因
所有 worker 都带唯一 application_name:
pg36-ch05-blocker-<UTC timestamp>-<shell pid>
pg36-ch05-waiter-<UTC timestamp>-<shell pid>真实应用也应给 service/driver 设置稳定 application name,并在 tracing 中关联:
cluster / instance
database / user / application
request trace ID
backend PID + backend_start
transaction/query start
dashboard time range + timezone只记录 PID 不够,PID 会重用;只记录 SQL 也不够,同一 statement 可由大量租户并发执行。涉及权限时还要知道:普通角色在 pg_stat_activity 中只能完整看到自己的 session,跨用户 query text/细节需要 pg_read_all_stats 等受控监控权限或 superuser。不要为了 dashboard 方便给业务账号 superuser。
处理采样与瞬时现场的差异
catalog query 能在 waiter 正等待时看到精确 edge;Prometheus/exporter 按采集周期采样,短于一个 scrape interval 的实验可能根本不出现在图上。默认 blocking harness 取完 SQL 证据便立即释放,不靠固定 sleep 同步。
若只在 L1 教学库中需要让 dashboard 有机会采到,可显式延长观察窗:
export PG36_DASHBOARD_HOLD_SECONDS=20 # 只允许 0..25
./task.sh blocking此变量只在已经确认 edge 后 sleep,不能参与 worker 同步;它会人为延长订单行等待,不允许用于生产。打开 Pigsty Web UI 后,在同一 UTC 时间窗依次看 PGSQL Activity、PGSQL Session/Xacts、PGCAT Locks,再回到 evidence 的 PID/app name 对照。若 panel 没采到,SQL evidence 仍是实验验收依据,不应继续延长生产锁来“等图变漂亮”。
dashboard 擅长回答“何时开始、范围多大、是否反复、同时还有什么资源变化”;catalog 擅长回答“现在这条边究竟是谁阻塞谁”。事故诊断通常先由告警/趋势定位时间窗,再用原生视图和日志确认现场。
5.6.3 注入阻塞并解释“现象—证据—原理”
单独执行 blocking:
unset PG36_DASHBOARD_HOLD_SECONDS
./task.sh blocking
sed -n '1,160p' "$PG36_EVIDENCE_DIR/blocking/summary.txt"
sed -n '1,160p' "$PG36_EVIDENCE_DIR/blocking/activity.csv"
sed -n '1,220p' "$PG36_EVIDENCE_DIR/blocking/locks.csv"blocking-lab.sh不靠“sleep 两秒大概启动好了”同步。它轮询 blocker 直到:
state=active
wait_event_type=Timeout
wait_event=PgSleep这证明未提交 UPDATE 已经完成并正持有 transaction。随后 ordinary reader 带 1 秒 statement timeout 读取旧 fingerprint;再启动 waiter,直到 pg_blocking_pids(waiter) 精确等于 blocker PID 且 wait type 是 Lock,才采集 CSV。
sequenceDiagram
participant B as Blocker
participant R as Ordinary reader
participant W as Waiter
participant O as Observer
B->>B: BEGIN; UPDATE order 1002
Note over B: uncommitted new tuple version
R->>B: plain SELECT
B-->>R: no row-lock wait; old committed version
W->>B: UPDATE same logical row
Note over W: waits for Blocker's XID
O->>O: activity + blocking_pids + locks
O->>B: pg_cancel_backend(exact PID)
Note over B: psql exits on 57014; transaction rolls back
B-->>W: lock released
W->>W: UPDATE succeeds; explicit ROLLBACK
O->>O: verify baseline and no workers
一次实测 summary:
status=ok
reader_saw_previous_committed_version=true
waiter_blocked_by=<blocker_pid>
waiter_wait_event_type=Lock
waiter_wait_event=transactionid
cancel_exact_blocker=t
blocker_expected_nonzero_exit=3
waiter_exit=0
state_restored=true
remaining_workers=0从三组证据回到原理
| 现象 | 直接证据 | 可以得出的原理 | 不能过度推出 |
|---|---|---|---|
| reader 成功返回旧指纹 | 1 s timeout 内结果等于 baseline | 普通读使用 MVCC 旧 committed version,不等 row update lock | 所有 SELECT 永不等待 |
| waiter 停住 | active + Lock + transactionid | 同一行 writer 必须等待前一 XID outcome | 表被“全锁死” |
pg_blocking_pids 单边 edge | waiter → blocker PID | 当前 regular lock queue 的直接 blocker 已识别 | 数秒前/后的历史仍完全相同 |
| locks 中 waiter XID ShareLock 未 granted | transactionid=blocker XID | 等待的是 blocker transaction completion | 必须找到 blocker 的 ungranted tuple lock |
| cancel 后 waiter 前进 | blocker 收到 57014 并断连 rollback | conflicting transaction 结束会释放 lock | cancel 任意生产 query 都安全 |
| 前后 checksum 相同 | verify-before/after | 两个业务写入最终都未提交 | 没产生 WAL/dead tuple/统计代价 |
这种“现象—证据—原理—边界”四列,比只保存一张 dashboard 截图更可审计。它允许后来者复核当时看见什么、为什么得出结论、结论没有覆盖哪些情况。
失败清理和停止线
正常路径只 pg_cancel_backend 精确 blocker query;blocker psql 因 ON_ERROR_STOP 收到 SQLSTATE 57014 后退出,server rollback connection transaction。waiter 获锁后显式 rollback。
若 harness 中途失败,EXIT trap 只按本次唯一 application names 终止它启动的 blocker/waiter,并等待本地 psql process;不会扫描或清理其他会话。随后仍应运行:
./task.sh verify若 active_lab_workers<>0、fingerprint/checksum 漂移,或 blocker identity 不再精确,停止自动处置并人工核对。这个实验没有 reset,因为成功路径不应留下需要 reset 的对象或数据;“写一个 reset 抹掉异常”反而会掩盖事务边界错误。
最后做全章验收:
export PG36_EVIDENCE_DIR="$PWD/evidence/ch05/final-$(date -u +%Y%m%dT%H%M%SZ)"
./task.sh all只有 verify-after 恢复稳定摘要,且 blocker cancellation、waiter exit、SQLSTATE 和 WAL/rollback 断言全部通过,才能把本章标记为完成。
上一节:隔离现象与后续路线 · 返回本章目录 · 下一章:立木取信:开发规约与交付基线 · 查看全书目录 · 查看索引中心