pgrac 在 PostgreSQL 8 KB block 格式基础上实施了一次精确的外科手术式升级:保留 PageHeaderData 的物理布局和全部 30+ 个 page-access 宏兼容,在 PageHeader 末尾追加 8 字节 pd_block_scn,并将 ITL slot 数组安置于 PG special area(block 尾部)——与 btree、hash、gin 等索引 AM 复用同一 special-area 机制,也与 Oracle 实际 block layout(ITL 在 block trailer)一致。结果是一套"双轨可见性"体系:本地事务走 PG 原生 xmin/xmax + CLOG 热路径,跨节点事务走 ITL slot → commit_scn → TT 的 SCN 路径;两路在同一物理 block 上并存,互不干扰。
PG 原生 block 是单机视角设计:PageHeader 记录结构元数据,行数据从底部向上增长,行指针从 header 向下增长;全部可见性信息存在每行的 xmin/xmax 字段里,block 本身不承载任何事务状态。pgrac 在此之上做了三处增量:PageHeader +8 字节 SCN、tuple header +1 字节 ITL 索引、block 尾部新增 ITL slot 数组。
pgrac block 格式与 PG 原生格式不兼容:pd_pagesize_version 从 4 bump 到 5,pg_upgrade 无法直接升级。迁移路径为 dump/restore 或专用迁移工具(spec-1.25 验收前提供)。Extension 中依赖 pageinspect 直接读取 block 内容的工具需要适配新格式;大多数不读取 block 内容的 Extension 保持兼容。
| 字段 / 区域 | PG 原生 | pgrac | 差异 |
|---|---|---|---|
| PageHeader | 24 B | 32 B(+8 B pd_block_scn) | +8 B |
行指针(pd_linp[]) | 紧跟 header,offset 24 起 | 同左,offset 32 起 | 偏移后移 8 B |
| 自由空间 | pd_lower ↔ pd_upper | 同左 | 不变 |
| 行数据 | 从底部向上增长 | 同左,含 +1 B t_itl_slot_idx | 每行 +1 B |
| ITL slot 数组 | ❌ 无 | ✅ special area,默认 384 B(8 × 48 B) | 新增 |
| Special area | heap 表为空(0 B) | ITL slot 数组(384 B) | 新增 |
| Block-level SCN | ❌ | ✅ pd_block_scn(8 B) | 新增 |
| Delayed cleanout flag | ❌ | ✅ PD_DELAYED_CLEANOUT(pd_flags bit) | 新增 |
| 可用用户数据(8 KB) | ~8168 B | ~7776 B | -392 B(~4.8%) |
ITL(Interested Transaction List)slot 数组存放在 block 的 special area(pd_special 指向位置,距 block 末尾 384 字节处)。每个 slot 48 字节,默认 8 个(INITRANS = 8),通过 ClusterPageGetItlSlots(page) 函数包装 PageGetSpecialPointer 访问,附带双断言(PageHasItl flag 检查 + special size 检查)。
每个 ITL slot 的 48 字节分布如下:
| 字段 | 大小 | 含义 |
|---|---|---|
xid | 4 B | PG 32-bit 事务 ID;高位编码 instance_id(A1-v2 跨实例 xid 分段) |
wrap | 2 B | slot 复用计数,防止 ABA 误判 |
flags | 1 B | ACTIVE / COMMITTED / ABORTED / CLEAN / NEEDS_CLEANOUT |
lock_count | 1 B | 本 slot 持有行锁计数 |
undo_segment_head(UBA) | 16 B | Undo Block Address,指向 per-instance undo segment(见 §9.3) |
commit_scn | 8 B | commit 时填入;INVALID(0) 表示未 commit |
write_scn | 8 B | 写入时刻 local_scn(AD-008 Lamport 单调推进配套) |
first_change_lsn | 8 B | 本事务在此 block 的首次修改 LSN,crash recovery 锚点 |
+--------------------------------------+ 0
| PageHeaderData (24 B) |
| pd_block_scn ( 8 B) | pgrac 新增(+8 B block-level SCN)
+--------------------------------------+ 32
| pd_linp[0..N] Row Pointers | (PG 兼容,原样保留)
| (each 4 B; FLEXIBLE_ARRAY) |
+--------------------------------------+
| |
| Free Space |
| (between row pointers |
| and row data) |
| |
+--------------------------------------+
| |
| Row Data | (从底部向上增长)
| ClusterHeapTupleHeader (+1 B idx) |
| |
+--------------------------------------+
| ITL Slot 0 (48 B) | ↑ 新增区域 (block 末端 special area)
| ITL Slot 1 |
| ... |
| ITL Slot N-1 | N = INITRANS, 默认 8
+--------------------------------------+ 8192
Each ITL Slot (48 B):
+------+------+-------+-------+--------+------------+----------+-----------------+
| xid | wrap | flags | lock | UBA | commit_scn | write_scn| first_change_lsn|
| 4 B | 2 B | 1 B | 1 B | 16 B | 8 B | 8 B | 8 B |
+------+------+-------+-------+--------+------------+----------+-----------------+
PD_HAS_ITL(pd_flags bit 0x0008)标识本 block 的 special area 是 ITL 数组(区别于 btree opaque 等)。heap AM 通过 PageInitHeapPage(page, 8192, 384) 初始化;index AM 继续调用 PageInit 不加 ITL,PD_HAS_ITL 永远为 0。
UBA(Undo Block Address)是 ITL slot 中 undo_segment_head 字段的类型,16 字节,作为精确指针同时服务两条查询路径:
commit_scn):通过 (segment_id, tt_slot_offset) 直达 undo segment header 内的 TT slot(segment_id, block_no, row_offset) 直达具体 undo recordtypedef struct UBA {
UndoSegmentId segment_id; // 4 B undo segment 编号
BlockNumber block_no; // 4 B undo block 内偏移
uint16 tt_slot_offset; // 2 B segment header 内 TT slot 索引(0-47)
uint16 row_offset; // 2 B undo block 内具体 record 偏移
uint32 reserved; // 4 B 保留,MemSet 为 0
/* total: 16 B */
};
tt_slot_offset 索引到 undo segment header 内的 TT slot 数组(48 个 slot × 32 B = 1536 B per segment header)。TT slot 持有 commit_scn + status(ACTIVE / COMMITTED / ABORTED),是 SCN 可见性路径的权威数据源。
查询链路:Row → ITL slot (block) → UBA → TT slot (undo segment header) → commit_scn;若需要旧版本数据,则继续 UBA → undo record (undo block)。
UBA 的 16 字节大小使 pgrac ITL slot 比 Oracle ITL slot(~32 B)大了 16 字节——其余增量来自 write_scn(8 B,AD-008 Lamport 配套)和 first_change_lsn(8 B,crash recovery 锚点)。总 48 B vs Oracle 32 B 的选择在容量-功能权衡下被明确论证并锁定。
delayed cleanout 是 pgrac 从 Oracle 引入的关键性能优化:事务 commit 时不立即遍历所有已修改行更新其 ITL slot 的 flags 字段——这在高并发批量写入时可将 commit 路径的 I/O 量减少 70-90%。
机制如下:
status → COMMITTED,写入 commit_scn);ITL slot 的 flags 维持 ACTIVE,同时设置 block 的 PD_DELAYED_CLEANOUT flag。COMMITTED,将 commit_scn 写回 ITL slot 的 commit_scn 字段,flags 改为 CLEAN。HeapTupleSatisfiesMVCC_scn 执行 TT lookup 时,如发现 COMMITTED 且 ITL slot 尚未 CLEAN,立即写回——无需额外 pass,顺手完成。CLEAN 或 INACTIVE 时,清除 PD_DELAYED_CLEANOUT flag。PD_DELAYED_CLEANOUT bit 是读路径的快速过滤器:若 block 未设此 flag,则无需扫描 ITL slot 状态,直接走 PG 原生 hint bit 路径。
pgrac 实现"双轨可见性":同一 block 上的每行元组,根据 xid 来源和 snapshot 类型走不同路径。两路在物理上并存,由 AD-012 双维度分流逻辑在读取时选择。
路径 1:PG 原生路径(本地热路径)
前提:xid 属于本 instance + snapshot 是本 session 创建的本地 snapshot。
t_xmin / t_xmaxProcArray 判断 in-progress 状态snapshot.xip[] 比对可见性此路径与 PG 原生代码完全一致,无额外开销。
路径 2:SCN 路径(跨节点 / 导入 snapshot)
前提:xid 属于远程 instance,或 snapshot 是跨节点传入的 SCN snapshot。
t_itl_slot_idx 获得 ITL slot 索引flags == CLEAN:直接比对 commit_scn 与 snapshot.read_scnflags == ACTIVE 或 NEEDS_CLEANOUT:顺 UBA 查 TT slot;根据 TT slot 的 status 和 commit_scn 判定可见性;顺手执行 cleanout Read Heap Tuple
│
┌───────────┴───────────┐
↓ ↓
xmin/xmax ITL slot
in tuple in block
│ │
↓ ↓
PG CLOG check cluster TT lookup
(single instance) (cross-instance)
│ │
↓ ↓
commit / abort commit_scn 比较
visible / invisible visible at read SCN?
│ │
└──── 综合判定 ─────────┘
↓
visible?
CR block 构造:当 block_scn > snapshot.read_scn 时,pgrac 在 buffer pool 内克隆一份 CR copy,对每个 commit_scn > read_scn 的 ITL slot apply undo(撤销其修改),得到历史版本 block。CR block 仅存在于 buffer pool,不写磁盘,通过 is_cr_block = true 和 cr_scn 字段标识。
ITL slot 数量通过 INITRANS 参数 per-table 配置,语法与 Oracle 对齐:
-- 建表时指定
CREATE TABLE orders (...) WITH (INITRANS = 16);
-- 修改已有表(对新分配 block 生效)
ALTER TABLE orders SET (INITRANS = 8);
| 场景 | 推荐 INITRANS | 说明 |
|---|---|---|
| 默认 | 8 | 适合通用 OLTP 负载;block 尾部占 384 B |
| 高并发热表(OLTP) | 16 – 32 | 频繁并发 DML;防止 ITL overflow 触发重排 |
| OLAP / 读多写少 | 4 | 减少 ITL 占用,多留用户数据空间 |
| 索引页 | 0(不适用) | index AM 不加 ITL,PD_HAS_ITL = 0 |
INITRANS = 8 默认值的容量损失约 4.8%(~392 B / 8192 B)。调整为 INITRANS = 4 可将损失降到 ~3%;INITRANS = 16 则约 7%。当所有 ITL slot 均被活跃事务占用时,pgrac 按顺序尝试:复用已 COMMITTED 的 slot(cleanout 后释放)→ 重排 block 腾出空间 → 返回 "block full"(写入方自动转到下一 block)。Stage 3 visibility 改造完成前,INITRANS DDL 和 ITL overflow 处理均为占位实现。
深度设计细节请参阅以下资源:
ClusterPageHeader 完整 C struct、ITL slot 48 B 字段逐一注释、PD_HAS_ITL / PD_DELAYED_CLEANOUT flag 语义、PageInitHeapPage inline 实现、MaxHeapTupleSize 公式推导BufferDesc 扩展字段(is_cr_block / cr_scn / cr_chain_next / has_pi / pi_lsn)、CR block 构造完整流程、PI block 持有时机