上一章(Ch 10)讲解了 per-instance undo tablespace 的物理布局与跨节点可见性路径:undo records 存放于每实例独立的 segment,CR block 构造时沿 UBA 拉取 undo chain 反向 apply。本章深入 buffer pool 层——undo 和 heap block 最终驻留的地方——以及 pgrac 如何在 PG 原生 buffer pool 之上引入跨节点 buffer 协调。
pgrac buffer pool 的核心挑战是:在不破坏 PG 原生 hot path 性能的前提下,将 PG 的单机 single-copy 模型扩展为集群的三副本模型(XCUR / SCUR / PI),并通过 PCM 锁状态机(AD-002)与 Cache Fusion 协议(AD-005)维持全局 buffer 一致性。AD-006 PIVOT B 带来了一个重要简化:CR block 不再占用独立 buffer 槽,而是通过 undo chain 按需构造行级视图,使 BufTable hash 依然保持单一 BufferTag 维度。
PG 原生 buffer pool 是单机 + 单版本设计:每 block 在内存中至多存在一个副本(current),所有节点内并发由 LWLock(content_lock)序列化,无跨 instance 一致性协议,无 CR / PI 概念。pgrac 在此之上引入跨节点副本语义,但保留 PG 的 BufTable hash 路径和 pin/unpin 机制,改动最小。
| 维度 | PG 原生 | pgrac |
|---|---|---|
| 每 block 内存副本数 | 1(current) | 最多 3(XCUR / SCUR / PI),CR 按需构造不占槽 |
| 跨节点一致性 | ❌ 无 | PCM 锁状态机(N/S/X)+ Cache Fusion |
| 可见性副本 | heap dead tuple + CLOG | XCUR/SCUR current + undo chain 构造 |
| BufferTag | RelFileLocator + ForkNumber + BlockNumber(20 B) | 不变(CR/PI 通过 chain 关联,不进 BufTable) |
| BufferDesc 大小 | 64 B(1 cache line) | 128 B(2 cache lines;hot 字段全在前 64 B) |
| Eviction 策略 | clock-sweep(单优先级) | 三池差异化:PI > SCUR > XCUR 驱逐优先级递减 |
| 跨节点 block 访问 | ❌ 不支持 | Cache Fusion RDMA transfer(~5 μs Tier 1) |
| CR block | ❌ 不支持 | AD-006 下不占 buffer 槽,由 undo chain 构造行级视图 |
pgrac 每个 buffer 槽在任意时刻属于且仅属于一种副本类型,由 pcm_state 和 pi_flags 字段派生(非独立字段,零冗余):
| 类型 | 含义 | 集群唯一性 | 映射 |
|---|---|---|---|
| XCUR(Exclusive Current) | 独占写,全集群唯一 | 全集群至多 1 个节点持有 | pcm_state = X, has_pi = false |
| SCUR(Shared Current) | 共享读,多节点可同时持有 | 多节点共存 | pcm_state = S, has_pi = false |
| PI(Past Image) | X 锁让出后保留的旧脏副本 | 多节点各自持有独立 PI | has_pi = true, pcm_state = 任意 |
| CR(Consistent Read) | 按需构造,不占 buffer 槽 | — | AD-006 下由 undo chain 构造(#121) |
AD-006 PIVOT B 是一项重要简化:CR block 不再像 Oracle 那样占用独立 buffer 槽位。pgrac 的 CR 构造通过 cluster_visibility.c(#121)对 undo chain 做行级回放,buffer pool 始终只存储 current 版本的 block。这使 BufTable hash 维度保持与 PG 原生一致,不需要为历史版本维护额外的 hash key。
cluster-wide buffer state
Node 1 Node 2 Node 3
┌────────┐ ┌────────┐ ┌────────┐
│ pool │ │ pool │ │ pool │
│ │ │ │ │ │
block A: │ XCUR │ ──── X ──── │ · │ ─── X ──── │ · │ 独占写
│ │ │ │ │ │
block B: │ SCUR │ ──── S ──── │ SCUR │ ─── S ──── │ SCUR │ 共享读
│ │ │ │ │ │
block C: │ CR │ │ · │ │ CR │ 按需构造
│ @SCN 99│ │ │ │ @SCN 99│ (不占独立槽位)
│ │ │ │ │ │
block D: │ PI │ │ XCUR │ │ PI │ 传走脏页保留
│ @SCN 75│ │ @SCN 80│ │ @SCN 75│ (按 SCN 排序)
└────────┘ └────────┘ └────────┘
副本类型的 C 宏派生:
typedef enum {
BCT_FREE, /* 空 / freelist */
BCT_INVALID, /* 有 tag 但内容无效,等待 Cache Fusion 拉取 */
BCT_XCUR, /* pcm_state=X, has_pi=false */
BCT_SCUR, /* pcm_state=S, has_pi=false */
BCT_PI, /* has_pi=true, pcm_state=任意(通常 N 或 S) */
} BufferCopyType;
#define BUFFER_COPY_TYPE(bd) \
((bd)->pi_flags.has_pi ? BCT_PI : \
((bd)->pcm_state == PCM_MODE_X ? BCT_XCUR : \
((bd)->pcm_state == PCM_MODE_S ? BCT_SCUR : BCT_INVALID)))
副本类型是派生视图,任何时刻都可以从 pcm_state 和 pi_flags 无歧义地计算出来,不引入额外字段维护负担。
pgrac 将 PG 原生 BufferDesc(64 B)扩展为 128 B,通过 USE_PGRAC_CLUSTER 编译守卫追加 cluster 字段。与 Ch 9 的 PageHeaderData 扩展、Ch 10 的 undo segment header 扩展同模式:扩展 PG 原有 struct,不引入并行结构体。
/* BufferDesc — PG 16.13 实测布局(USE_PGRAC_CLUSTER 模式,128 B)
* 概念名 ClusterBufferDesc;代码层保留 PG 原名 BufferDesc + 编译守卫追加字段。
*/
typedef struct BufferDesc {
/* === Cache line 1 前半:PG 原字段 [0, 52),HOT,与 PG vanilla 兼容 === */
BufferTag tag; /* 20 B: RelFileLocator(12) + ForkNumber(4) + BlockNumber(4) */
int buf_id; /* 4 B */
pg_atomic_uint32 state; /* 4 B: refcount + usage_count + flags */
int wait_backend; /* 4 B */
int freeNext; /* 4 B */
LWLock content_lock; /* 16 B; ends at offset 52 */
/* === Cache line 1 cluster hot tail [52, 64),12 B;hot path access === */
uint8 buffer_type; /* offset 52: BUF_TYPE_CURRENT / CR / PI(派生;冗余快照)*/
uint8 pcm_state; /* offset 53: N / S / X */
uint8 pi_flags; /* offset 54: has_pi 及相关 bit */
uint8 _pad; /* offset 55: 1 B padding for 8 B alignment of block_scn */
SCN block_scn; /* offset 56: 8 B; ends at 64 = cache line 1 boundary */
/* === Cache line 2 cold body [64, 128),64 B;cluster-specific paths only === */
int cr_chain_head; /* offset 64: PIVOT B — moved here (CR construction is cold path) */
int cr_chain_next; /* offset 68 */
SCN cr_scn; /* offset 72: 仅 CR buffer(AD-006 下暂不使用独立槽)*/
int pi_buf_id; /* offset 80 */
XLogRecPtr pi_lsn; /* offset 88: 仅 PI buffer */
uint16 grd_master_node; /* offset 96 */
uint16 grd_master_seq; /* offset 98 */
uint8 cf_state; /* offset 100: Cache Fusion 协议状态 */
uint8 cf_owner_node; /* offset 101 */
uint16 cf_request_count; /* offset 102 */
LWLock pcm_lock; /* offset 104: 锁转换时才访问 */
TimestampTz pi_created_at; /* offset 120: ends at 128 */
/* total: 128 B(BUFFERDESC_PAD_TO_SIZE = 128 in USE_PGRAC_CLUSTER mode)*/
} BufferDesc;
v1.2(2026-05-02)在编码途中发现了一个关键实测结果:PG 16.13 的 sizeof(BufferTag) = 20 B(RelFileLocator 12 B + ForkNumber 4 B + BlockNumber 4 B = 20 B),而非早期设计文档假设的 16 B。这使 PG 原字段实际占到 offset [0, 52),cluster hot tail 只剩 12 B,无法同时容纳 cr_chain_head(4 B)和 block_scn(8 B)并保持 block_scn 在 cache line 1 之内。
PIVOT B 取舍:block_scn 是 Stage 2–3 可见性 hot path 的关键字段(每次 buffer access 都需要对比 block_scn 与 snapshot.read_scn),必须驻留 cache line 1。cr_chain_head 仅在 CR 构造时访问(cold path),牺牲它让出 cache line 1 的位置,移至 cache line 2 起点(offset 64)。
hot path 访问模式(cache line 1 only = 前 64 B):
BufTableLookup → IncreaseRefcount → 读 pcm_state → 读 block_scn → LWLockAcquire(content_lock)
全程不触碰 cache line 2,与 PG 原生 hot path 开销相同(1 cache miss)
cold path(cache line 2,仅在新场景触发):
CR 构造 → 访问 cr_chain_head / cr_chain_next / cr_scn
PI 创建 → 访问 pi_buf_id / pi_lsn / pi_created_at
Cache Fusion → 访问 cf_state / cf_owner_node / pcm_lock
编译期由 5 个 StaticAssertDecl 用语义约束锁定布局不变量,例如 offsetof(block_scn) + sizeof(SCN) <= 64(block_scn 在 cache line 1 内)和 offsetof(cr_chain_head) >= 64(cr_chain_head 在 cache line 2 起点),而非硬编码 magic offset 数字——未来 PG 版本若再次扩展 BufferTag,断言会在编译期报错,而不是静默误算。
pgrac buffer pool 的并发安全由两个正交且独立的维度共同保障,不可合并:
维度一:Pin(refcount)
refcount > 0 防止 buffer 被 evict维度二:PCM Lock(N/S/X)
pcm_state 字段存储在 ClusterBufferDesc hot tail(offset 53)/* 两维度的合法组合示例 */
/* Pin + S:backend 持有 buffer 引用,本节点持共享 PCM 锁,可本地读 */
/* Pin + X:backend 持有 buffer 引用,本节点持独占 PCM 锁,可本地写 */
/* Unpinned + X:无 backend 引用但节点仍持 X 锁 → 不可立即 evict(见下文)*/
/* Pin + N:PCM 锁转换中间状态 → 罕见但合法 */
evict 与 PCM X 锁的关键约束:持有 PCM X 锁的 buffer,即使 refcount = 0(unpinned),也不能直接 evict。原因是 PCM X 锁代表 GRD 已知"该 block 的 master 在本节点",直接驱逐会让 GRD 状态与本地 buffer 状态脱节。正确路径是先通知 GRD 释放 X 锁(pcm_release_x_lock),本节点 pcm_state → N,dirty block 先 flush,再从 BufTable 删除并归还 buffer 槽。
访问顺序:PCM 锁与 content_lock 严格按 "先 PCM 后 content" 顺序获取,防止死锁(§5 AD-002 设计文档对此做出完整形式证明)。
9 条合法 PCM 状态转换(从 AD-002):
| 转换 | 触发场景 |
|---|---|
| N → S | 本节点首次读该 block |
| N → X | 本节点首次写该 block |
| S → X | 本节点升级:持 S 的 block 需要写入 |
| X → S | 其他节点请求读,本节点降级(保留 PI) |
| X → N | 收到 invalidate(不留 PI) |
| S → N | 收到 invalidate |
| X → S(保 PI) | has_pi = true 正交标记随降级生效 |
| N → S(有 GRD PI fast-path) | 跳过 disk read,从 PI holder 获取 |
| ITL cleanout 触发 S → X | reader 执行 delayed cleanout,需短暂升级写 |
pgrac 对 PG 的 clock-sweep eviction 做了三池差异化改造,使 XCUR(写热数据)获得最高保留优先级,PI 在必要时可驱逐(但有 TTL 保护),SCUR 介于两者之间。
三池静态划分(默认,GUC 可调):
| 池 | 默认占比 | 大小(shared_buffers = 16 GB) | 驱逐优先级 |
|---|---|---|---|
| CurrentPool(XCUR / SCUR) | 60% | 9.6 GB | 最低(最珍贵) |
| PIPool | 10% | 1.6 GB | 中(TTL 5 min 后可驱逐) |
| Reserve | 10% | 1.6 GB | 动态调整 |
CR block 不占独立 buffer 槽(AD-006),原设计中的 CRPool(20%)在 PIVOT B 后不需要实体池分区;该空间合并入 CurrentPool 供 OLTP 热数据使用。
改造后的 StrategyGetBuffer 三段式:
StrategyGetBuffer():
/* 1. 优先在 PIPool 找过 TTL(> 5 min)的 PI 副本 */
victim = sweep_pi_pool_expired();
if (victim) return victim;
/* 2. 在 CurrentPool 找 unpinned + pcm_state = N 的 SCUR(已降级)*/
victim = sweep_current_pool_shared();
if (victim) return victim;
/* 3. 经典 clock-sweep 兜底(XCUR,需先释放 PCM X 锁 + flush dirty)*/
victim = sweep_current_pool_clock();
if (victim->pcm_state == X) {
pcm_release_x_lock(victim);
}
if (victim->dirty) flush_to_disk(victim);
return victim;
PI TTL 与驱逐:PI buffer 的 pi_created_at 字段(offset 120,cache line 2)记录创建时间戳,默认 5 分钟(cluster_pi_ttl_sec = 300)后标为可驱逐候选。PI 还会在以下情况下提前清理:本节点对同一 block 重新拿到 X 锁(PI 已无意义);Reconfig 期间 Phase 4 完成 master 重建后;cluster_undo_retention_sec 窗口关闭导致对应 undo 数据失效。
OLTP 影响:PIVOT B 后 hot path 仅读 cache line 1(前 64 B),比 PG 原生多 1 byte pcm_state 读 + branch(~5 ns)。Buffer pool 三池结构使 XCUR hot 数据得到优先保留,避免全表扫描污染工作集。综合来看 OLTP TPS 影响 < 1%(设计分析结论;1.6 阶段实测验证中)。
深度设计细节与相关特性:
ClusterBufferDesc 完整 C struct、5 个 StaticAssertDecl 语义约束、三池 GUC 参数(cluster_cr_pool_pct / cluster_pi_pool_pct)、pg_cluster_buffer_pool_stats 视图字段定义、内存预算(BufferDesc 数组 128 MB 增量 = +0.8% shared_buffers)pcm_lock(LWLock at offset 104)的获取顺序约束BCT_INVALID buffer 触发 CF transfer 的完整 3-way 消息流、RDMA zero-copy 路径与 cf_state 字段生命周期FlushBuffer 路径、pi_lsn 与 WAL truncation point 的关系、checkpoint 如何协调三池 dirty buffer 的刷盘顺序