上一章(Ch 9)讲解了 pgrac 8 KB block 的双轨布局:ITL slot 数组嵌入 special area,每行元组通过 t_itl_slot_idx 索引到对应 ITL slot,ITL slot 的 undo_segment_head(UBA,16 字节)指向 per-instance undo segment。本章深入 UBA 所指向的目的地——undo 子系统的物理结构与生命周期。
pgrac 的 undo 子系统实现了完整的 Oracle 风格 MVCC:每实例持有独立的 undo tablespace,所有 DML 先写 undo 再写 heap,rollback 和 CR block 构造均通过 undo chain 完成。核心设计目标是跨节点零 undo 写冲突——每实例只写自己的 undo tablespace,其他实例通过 Cache Fusion 只读访问。
PG 原生没有独立的 undo 子系统。历史版本存在 heap 内(dead tuple),由 VACUUM 异步清理;可见性由 xmin/xmax + CLOG 确定,是典型的"MVCC by heap versioning"。pgrac 引入完整 undo 子系统后,历史版本离开 heap,进入专属 undo segment,heap 行的生命周期因此大幅缩短。
| 维度 | PG 原生 | pgrac Per-instance Undo |
|---|---|---|
| 历史版本位置 | heap 内(dead tuple) | 独立 undo segment |
| 可见性信息 | xmin/xmax in tuple + CLOG | ITL slot → UBA → TT slot(commit_scn) |
| MVCC 模型 | heap versioning | Oracle MVCC(undo-based) |
| 跨节点 MVCC | ❌ 不支持 | ✅ cluster-wide SCN + undo chain |
| Undo tablespace | ❌ 无 | 每实例独立(per-instance,AD-010) |
| 写冲突 | — | 无跨节点 undo 写争用 |
| 回收机制 | VACUUM 扫 heap | undo_vacuum bgworker 按 retention 回收 |
| Rollback 路径 | 标 xmax 无效 | 沿 undo chain 反向 apply inverse 操作 |
若所有节点共用一个 undo tablespace,则每次 DML 都需竞争 undo segment 的写锁:segment 分配、TT slot 申请、undo block 写入均引入跨节点锁协议,在高并发写入场景下将成为严重瓶颈。
pgrac 选择 per-instance undo tablespace(AD-010)的动机:
pg_basebackup 无需备份 undo tablespace,大幅减少备份数据量跨节点读 undo 数据(CR 构造、读取远程 TT slot 的 commit_scn)通过 Cache Fusion(#119)完成——这是只读访问,无写争用。
每实例默认配置 16 个 segment × 64 MB = 1 GB undo 空间,在 pgrac_ctl initdb 阶段自动创建。高负载实例可扩展至 10+ GB(max_undo_segments_per_instance 默认上限 64)。
每个 segment 文件内部分为两层:
┌─────────────────────────────┐
│ shared data tablespace │ (cluster-wide, all nodes read/write)
└─────────────────────────────┘
↑
┌─────────────┴─────────────┐
│ │
Node 1 Node N
┌────────────────┐ ┌────────────────┐
│ undo_node_1 │ │ undo_node_N │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ seg_001 │ │ │ │ seg_001 │ │
│ │ 64 MB │ │ │ │ 64 MB │ │
│ └──────────┘ │ │ └──────────┘ │
│ ... │ │ ... │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ seg_016 │ │ │ │ seg_016 │ │
│ └──────────┘ │ │ └──────────┘ │
│ 16 × 64MB │ │ 16 × 64MB │
│ = 1 GB │ │ = 1 GB │
└────────────────┘ └────────────────┘
(only Node 1 writes) (only Node N writes)
Tablespace catalog 在 pg_tablespace 中以 spctype = 'undo' 标识(AD-004 新类型),spcowner_instance 字段记录归属实例:
SELECT spcname, spctype, spcparams, spcowner_instance
FROM pg_tablespace
WHERE spctype = 'undo';
spcname | spctype | spcparams | spcowner_instance
----------------------+---------+----------------------------------------+-------------------
undo_tbs_instance_1 | undo | {size=1GB, segments=16, retention=900} | 1
undo_tbs_instance_2 | undo | {size=1GB, segments=16, retention=900} | 2
Segment Header 占 Block 0 的全部 8 KB。前 24 字节复用 PageHeaderData(LSN + checksum + flags),随后依次存放段元数据、块分配指针、retention 信息,以及 48 个 TT slot 组成的事务表(1.5 KB)。
段的生命周期经历五态流转:ALLOCATED → ACTIVE → COMMITTED → RECYCLABLE → ALLOCATED。每实例默认 8 个段处于 ACTIVE(被活跃事务占用),4 个处于 COMMITTED(retention 保留期内),剩余为空闲或可回收。每事务独占一个 segment(per-tx 独占策略),消除段内并发争用,同一事务的所有 undo record 也因此具有良好的局部性。
每个 TT slot 固定 32 字节,是 undo 子系统的事务状态权威数据源:
| 字段 | 大小 | 含义 |
|---|---|---|
xid | 4 B | PG 32-bit 事务 ID(高位含 instance_id,A1-v2 跨实例 xid 分段) |
wrap | 2 B | slot 复用计数(WRAP);防止 ABA 误判 |
status | 1 B | ACTIVE / COMMITTED / ABORTED / RECYCLABLE |
flags | 1 B | cleanout 状态 + 保留位 |
commit_scn | 8 B | commit 时写入;INVALID(0)表示未 commit |
first_undo_block(UBA) | 16 B | 本事务第一条 undo record 的精确地址(segment_id, block_no, tt_slot_offset, row_offset) |
WRAP 防 ABA:TT slot 在 COMMITTED 后不立即释放——reader 可能仍需查询 commit_scn。只有当 commit_scn < oldest_active_snapshot_scn 且该 slot 关联的所有 undo records 已被回收后,slot 才标为 RECYCLABLE 并被复用。复用时 wrap++,ITL slot 引用 TT slot 时会比对 wrap 值:不匹配则说明 slot 已被复用,改用 ITL 缓存的 commit_scn。
48 个 slot × 32 B = 1.5 KB,占 segment header 约 19%;结合默认 16 个 segment,每实例支持 8 × 48 = 384 个并发活跃事务(不含 COMMITTED 槽位)。
每条 undo record 由固定 32 字节的 UndoRecordHeader + 变长 payload 组成。Header 中 prev_undo_in_tx(16 B UBA)指向同一事务的前一条 undo record,形成反向单链表——这是 rollback 的核心数据结构。
Tx A 写入顺序(时间序):
Block 5: Record 1 (INSERT row P) prev = NULL
Block 5: Record 2 (UPDATE row Q) prev → (seg X, blk 5, rec 0)
Block 5: Record 3 (DELETE row R) prev → (seg X, blk 5, rec 1)
Block 6: Record 4 (UPDATE row S) prev → (seg X, blk 5, rec 2)
TT slot[A].first_undo_block → (seg X, blk 6, rec 3) ← 最新一条(rollback 起点)
Rollback 路径(反向):
Record 4 → Record 3 → Record 2 → Record 1 → NULL
逐条执行 inverse 操作(INSERT→删行;UPDATE→还原 pre-image;DELETE→re-insert)
三种操作的 payload 大小不同:INSERT undo 最轻(仅需 row offset,~40 B 总计);UPDATE undo 采用 column-delta 优化,只存被改列的 pre-image(5 列变更约 80 B);DELETE undo 最重,需存储完整旧 row(200 B row 约 240 B 总计)。
CR block 构造:读取时若 block_scn > snapshot.read_scn,pgrac 在 buffer pool 克隆一份 CR copy,对每个 commit_scn > read_scn 的 ITL slot 沿 UBA 取 undo records,反向 apply 还原历史版本。CR block 仅存在于 buffer pool,不落盘。
Undo retention:默认保留 15 分钟(cluster_undo_retention_sec = 900),默认采用 max 模式(有活跃 reader 持有更老 snapshot 时无限延长,防 STO)。后台 undo_vacuum worker 每 60 秒扫描一次,将过期 segment 三层回收:TT slot → RECYCLABLE,undo records → 可丢弃,整 segment → 归还空闲池。
跨节点可见性是 pgrac MVCC 最复杂的路径:Node A 读到一行 tuple,该行的最后一次修改由 Node B 的事务完成,相应的 undo 数据在 Node B 的 undo segment 中。
Node A: read tuple at row R
│
↓
Tuple has ITL slot index → look up ITL slot
│
↓
ITL slot has UBA = (segment_id, block_no, record_offset)
│
↓
cluster TT lookup → which node owns this undo segment? Node B
│
↓
fetch undo block (segment_id, block_no) from Node B's undo
(via Cache Fusion if cached, else shared storage IO)
│
↓
SCN check: is this version visible at read_scn?
│
↓
visible / invisible
Segment header cache:Node A 拉取 Node B 的 segment header(含 TT slot 数组)后,本地 LRU 缓存 30 秒(segment_header_cache_ttl = 30s),避免每次 SCN 查询都触发 Cache Fusion 请求。ITL slot 中的 commit_scn 一旦写回(delayed cleanout),后续读取直接从 block 内拿,无需再查 TT slot。
Undo block cache:CR 构造时拉取的 undo block 本地缓存 5 分钟(undo_block_cache_ttl = 5min),受 PI pool 配额限制。缓存命中率通常在 90% 以上(见 pg_cluster_undo_cf_activity)。
跨节点 undo 访问属于只读操作(Node A 从不写 Node B 的 undo),因此没有写冲突。Cache Fusion 在此只需下发 shared 请求(S 模式),不触发 XCR(Exclusive CR)协议,开销显著低于跨节点 heap 写争用。
深度设计细节与相关特性:
UndoSegmentHeader 完整 C struct、undo record 三种 op payload、5 态生命周期状态机、TT slot 复用协议、undo_vacuum bgworker 实现、CREATE UNDO TABLESPACE DDLsegment_id / block_no / tt_slot_offset / row_offset 四字段布局,以及 ITL slot 如何持有 UBABufferDesc 扩展字段(is_cr_block / cr_scn / cr_chain_next)、CR block 构造完整流程、PI block 与 undo block 的协同