上一章(Ch 11)讲解了 buffer pool 三副本模型(XCUR / SCUR / PI)与 PCM 锁协调:脏 buffer 何时产生、何时驱逐。本章深入 WAL 层——脏 block 进入磁盘前必须经过的持久化门槛——以及 pgrac 如何在 PG 原生 WAL 之上实现 per-instance redo stream、扩展 record header、引入集群专属 record 类型,并在崩溃后通过跨节点 merged recovery 安全回放。
pgrac WAL 升级的核心动力来自三个方向:AD-009(per-instance redo thread,每节点独立 WAL stream)、AD-008(Lamport SCN 内嵌 WAL record header,xl_scn 8B 扩展)、AD-006(MVCC 引入 ITL / TT / Undo records)。三者共同使 pgrac WAL 与 Oracle Redo Records 在语义上对齐:每条 record 携带 SCN、每 thread 单调递增、多 thread 按 SCN 顺序合并回放。
PG 原生 WAL 是单 stream + 单线程 LSN 设计:整个集群(实际是单机)写入 pg_wal/ 下的连续段文件,所有 backend 共享同一个 WAL insert lock,recovery 单进程串行回放。pgrac 将这一模型扩展为 per-instance 独立 stream:每个节点拥有自己的 pg_wal_node_N/ 目录,写入互不阻塞,recovery 时再按 SCN 全局合并。
| 维度 | PG 原生 | pgrac |
|---|---|---|
| WAL 目录 | pg_wal/(单目录) | pg_wal_node_1/ … pg_wal_node_N/(每节点一目录) |
| stream 数量 | 1 | 节点数(典型 2–8) |
| LSN 空间 | 全集群共享单一 LSN 序列 | 每 stream 独立 LSN;全局顺序由 xl_scn 决定 |
| WAL 文件可见性 | 仅本机 | 所有文件在共享存储,任意节点可读 |
| WAL record header | 24 B(无 SCN) | 32 B(含 xl_scn 8B,page header 含 thread_id) |
| Record 类型 | heap / btree / xact 等 | 原生类型 + ITL / TT / Undo / PCM / BOC / Reconfig |
| Recovery 模式 | 单进程串行 replay | K-way SCN merge replay(跨 stream 按 SCN 排序) |
| WAL 容量(DML-heavy) | baseline | ~5.5×(header 增量 + ITL + Undo records) |
PG 原生 XLogRecord 固定 24 B:4 B 长度、4 B xid、8 B prev LSN、2 B info/rmid、2 B padding、4 B CRC。这 24 B 不携带任何时间戳或全局顺序信息,recovery 时只能依赖 LSN 线性推进——对于单 stream 没有问题,但无法在多 stream 之间建立全局顺序。
pgrac 通过 ClusterXLogRecord 在原有 24 B 后追加 8 B xl_scn,形成 32 B header。xl_scn 在 commit record 中为 commit_scn,在其他 record 中为写入时的本地 local_scn,由 WAL insert lock 内的原子读保证per-thread 单调不变式(AD-008 第二次扩展):同一 thread 内若 R1.LSN < R2.LSN,则 R1.xl_scn ≤ R2.xl_scn。
thread_id 不放在每条 record header 中(避免冗余),而是存在 WAL Page header 的 xlp_thread_id 字段(2 B):同一 stream 的所有 page 共享同一 thread_id,recovery 时从 page header 一次性读取即可。
PG 原生 24 B (兼容保留):
+--------+--------+--------+--------+--------+--------+
| xl_tot_len (4) | xl_xid (4) | xl_prev (8) |
+--------+--------+--------+--------+--------+--------+
| xl_info | xl_rmid | (padding) | xl_crc (4) |
+--------+--------+--------+--------+--------+--------+
pgrac 扩展 8 B:
+--------+--------+--------+--------+--------+
| xl_scn (8) | tid (1) |
+--------+--------+--------+--------+--------+
↑
thread_id
总 = 32 B
WAL insert critical section 内,"读 local_scn + 分配 LSN slot + 写 xl_scn" 三步原子执行,确保 xl_scn 的单调性不被并发破坏。BOC(Broadcast on Commit)piggyback 更新 local_scn 时,同样必须持 WAL insert lock,防止外部 SCN 推进与本地写入交错。
AD-006 引入了 ITL slot、TT slot 和 undo record 三类集群专属操作,它们产生的变更必须写入 WAL——这是 write-ahead undo 正确性的基础。pgrac 为此新增了 7-8 个 RMGR(Resource Manager),在 PG 原生 RMGR 框架上扩展,保持二进制兼容:
| RMGR | 用途 |
|---|---|
RM_CLUSTER_ITL_ID | ITL slot 写入 / cleanout / commit_scn 回写 |
RM_CLUSTER_TT_ID | TT slot 分配 / commit / abort / wrap 复用 |
RM_CLUSTER_UNDO_ID | Undo record 写入 / segment 状态变更 |
RM_CLUSTER_PCM_ID | PCM 锁转换(默认不写,仅 debug / reconfig 开启) |
RM_CLUSTER_BOC_ID | Broadcast on Commit SCN 广播记录 |
RM_CLUSTER_RECONFIG_ID | Reconfig Phase 切换 / Coordinator 选举 |
RM_CLUSTER_GEN_ID | 通用集群事件扩展点 |
典型 OLTP 事务(5 条 DML)的 WAL 量对比:PG 原生约 230 B / tx,pgrac 约 1,280–1,300 B / tx(5.5×)。增量主要来自 ITL write record(~54 B × 5)和 undo write record(~112 B × 5),header 增量(+8 B / record)仅占小头。NVMe Tier 1 部署下 OLTP TPS 影响 < 5%。
ITL(Interested Transaction List)slot 每次写入、cleanout、commit_scn 回写都必须产生对应 WAL record,使 recovery 能精确重建 block 内的事务状态:
/* ITL slot 写入(DML 时随 heap record 一起产生)*/
typedef struct ItlWriteRecord {
BlockNumber target_block; /* 4 B 目标 block 地址 */
uint8 itl_slot_idx; /* 1 B slot 下标(0-based)*/
uint8 info; /* 1 B CREATE / UPDATE / CLEANUP */
ClusterItlSlotData slot_data; /* 48 B ITL slot 完整内容 */
/* total: ~54 B */
} ItlWriteRecord;
/* ITL cleanout(reader 触发延迟清理,同样必须走 WAL)*/
typedef struct ItlCleanoutRecord {
BlockNumber target_block; /* 4 B */
uint8 itl_slot_idx; /* 1 B */
SCN commit_scn; /* 8 B 回写到 ITL slot 的 commit_scn */
/* total: ~16 B */
} ItlCleanoutRecord;
ITL cleanout 是 reader 触发的脏写(将 commit_scn 回写进 block header),必须走 WAL,否则 recovery 无法区分 cleanout 前后的 block 状态。TT slot 的 alloc / commit / abort / reuse 同样各有对应 record,共同构成完整的事务状态链路。
Cache Fusion 传输脏 block 前,WAL 必须已 fsync——这是 feature-019(Write-ahead 全局版本)规定的核心约束,也是整个 Cache Fusion 持久性保证的基础:
单机 PG 规则:
脏 block 刷盘前 → 对应 WAL 必须已 fsync
pgrac / Cache Fusion 扩展规则:
脏 block 跨节点传输前 → 对应 WAL 必须已 fsync(含接收方继续修改的场景)
具体实现上,Cache Fusion 发送路径(gcs_wa.c)在打包 block 数据后、调用 Interconnect 发送前,强制同步调用 XLogFlush(pi_lsn),等待本地 WAL 持久化完成。批量优化(group commit-like)允许多个待传 block 共享一次 fsync,减少 I/O 次数。
Write-Ahead 是同步阻塞操作:XLogFlush 完成前,Cache Fusion 发送路径挂起等待。LGWR 卡住超时会触发 reconfiguration,不允许降级跳过 WAL 同步(正确性优先于可用性)。
WAL fsync 延迟是 Cache Fusion 端到端延迟的主要分量之一。NVMe 共享存储下典型 WAL fsync < 200 μs,远低于 Interconnect 传输的 ~5 μs RDMA 往返——因此整体延迟瓶颈通常不在 Write-Ahead,而在 GRD master 协调轮次数。
每个 pgrac 节点维护一个 thread-local WAL writer(LGWR),独立管理本节点的 pg_wal_node_N/ 目录。WAL buffer 建议配置为 32 MB(vs PG 原生默认 16 MB),以容纳更多 record 类型的并发写入。xl_scn 单调不变式保证:本 thread 内 LSN 较大的 record,其 xl_scn 一定不小于 LSN 较小的 record。
崩溃后,存活节点(或新的 recovery 协调者)读取所有故障节点的 WAL stream,通过 K-way SCN merge replay 重建一致状态:
/* K-way merge — 优先队列按 xl_scn 升序 */
PriorityQueue pq; /* 最小堆,按 head_record.xl_scn 排序 */
for each thread t in merge_set:
t.head = read_next(t);
if (t.head) pq.push(t);
while (!pq.empty()) {
t = pq.pop_min(); /* 取全局 xl_scn 最小的 record */
dispatch_record(t.head);/* 按 block hash 分发 + redo apply */
t.head = read_next(t);
if (t.head) pq.push(t);
}
正确性依赖两个保证:per-thread xl_scn 单调(WAL insert lock 协议,§12.2);Cache Fusion serialization(同一 block 跨 thread 的 SCN 单调,Write-Ahead Rule,§12.4)。两者共同确保 merged order 与原始写入顺序一致。
Per-instance redo streams (各节点独立写):
Node 1: ●─●─●─●──────●── (thread A: SCN 42, 43; thread B: 44, 50; thread C: 61)
Node 2: ●─●─●────●─●───── (thread D: SCN 12; thread E: 44, 45; thread F: 55, 60)
Node 3: ●─●────────────── (thread G: SCN 8, 47)
↓ 按 SCN 排序合并 ↓
Merged: ●─●─●─●─●─●─●─●─●─● (replay order)
SCN: 8 12 42 43 44 45 47 50 55 60 61
Apply: 依序 redo each record。每条 record 仅 apply 到对应 block;
per-thread 顺序在 stream 内保留(thread_id 保证)。
Merged recovery 在集群 instance 故障(部分节点宕机)和 PITR 场景下均使用 K-way SCN merge。单机 crash recovery(仅一个 stream)退化为 PG 原生单线程 replay,增量仅为新 RMGR 的 redo handler(约 +5% 时间)。
深度设计细节与相关特性:
ClusterXLogRecord / ClusterXLogPageHeader 完整 C struct、7-8 个新 RMGR redo handler 实现、WAL insert critical section 伪代码、pg_cluster_wal_stats / pg_cluster_wal_scn_check 视图字段、WAL compression 扩展(lz4 2.1× ratio)xl_scn 语义(commit_scn vs local_scn)、Lamport 推进协议、BOC piggyback 更新路径、per-thread 单调不变式的形式证明pi_lsn 与 flushed_lsn 的关系约束、batch fsync 优化参数RM_CLUSTER_RECONFIG_ID records 在 Phase 切换时写入,recovery 重放后还原 Coordinator 状态