上一章(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 模型扩展为集群的四副本模型(current 的 XCUR / SCUR 两种 PCM 形态、按 SCN 派生的 CR、X 锁让出后留下的 PI),并通过 PCM 锁状态机(AD-002)与 Cache Fusion 协议(AD-005)维持全局一致性。BufferTag 字段保持 PG 原状不动——CR/PI 副本不进 BufTable hash,而是通过 current buffer 的 cr_chain_head / pi_buf_id 单链关联(buffer-pool-design.md §4.2)。
PG 原生 buffer pool 是单机 + 单版本设计:每 block 在内存中至多存在一个副本(current),所有节点内并发由 LWLock(content_lock)序列化,无跨 instance 一致性协议,无 CR / PI 概念。pgrac 在此之上引入跨节点副本语义,但保留 PG 的 BufTable hash 路径和 pin/unpin 机制,改动最小。
| 维度 | PG 原生 | pgrac |
|---|---|---|
| 每 block 内存副本数 | 1(current) | current(XCUR 或 SCUR)+ 0..N 个 CR(默认 chain 上限 8)+ 0..1 个 PI |
| 跨节点一致性 | ❌ 无 | PCM 锁状态机(N/S/X)+ Cache Fusion |
| 可见性副本 | heap dead tuple + CLOG | current + buffer 内 CR copy(按需构造,仅内存) |
| 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(单优先级) | 四池差异化:CR > PI > current 驱逐优先级递减 |
| 跨节点 block 访问 | ❌ 不支持 | Cache Fusion RDMA transfer(~5 μs Tier 1) |
| CR block | ❌ 不支持 | buffer pool 内独立 BufferDesc 槽位 + CR chain;不写盘 |
pgrac 每个 buffer 槽在任意时刻只承载一种副本类型。其中 current 副本的两种 PCM 形态(XCUR / SCUR)和 PI 副本由 pcm_state 与 pi_flags 字段派生(非独立字段,零冗余);CR 副本另起一类,通过 buffer_type = BUF_TYPE_CR 显式标记并加入 current 的 cr_chain:
| 类型 | 含义 | buffer 槽位 | 映射 / 持久化 |
|---|---|---|---|
| XCUR(Exclusive Current) | 独占写,全集群至多 1 节点持有 | current buffer,进 BufTable | pcm_state = X, has_pi = false;落盘 |
| SCUR(Shared Current) | 共享读,多节点可同时持有 | current buffer,进 BufTable | pcm_state = S, has_pi = false;落盘 |
| CR(Consistent Read) | 按 read_scn 构造的历史版本 | 独立 buffer 槽位(CRPool),通过 cr_chain 挂在 current 之后 | buffer_type = BUF_TYPE_CR, cr_scn;仅内存,不落盘 |
| PI(Past Image) | X 锁让出后保留的旧脏副本 | 独立 buffer 槽位(PIPool),通过 pi_buf_id 关联 current | has_pi = true;仅内存,TTL 5 min |
四种副本中,只有 current(XCUR / SCUR)进 BufTable hash(由 BufferTag 索引);CR 与 PI 都是 current 的"派生物",分别通过 cr_chain_head / cr_chain_next(CR 多版本链,按 cr_scn 降序)和 pi_buf_id(PI 单引用)关联。这使 BufTable hash 维度保持与 PG 原生一致,不需要为历史版本维护额外的 hash key(buffer-pool-design.md §4.2 / §6.1 / §7.2)。
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 │ 按 read_scn 构造
│ @SCN 99│ │ │ │ @SCN 99│ (独立 CRPool 槽位,仅内存)
│ │ │ │ │ │
block D: │ PI │ │ XCUR │ │ PI │ 传走脏页保留
│ @SCN 75│ │ @SCN 80│ │ @SCN 75│ (按 SCN 排序)
└────────┘ └────────┘ └────────┘
显式 buffer_type 枚举(buffer-pool-design.md §4.4):
typedef enum {
BUF_TYPE_CURRENT, /* 当前 block 副本,可读可写(按 PCM 锁 N/S/X)*/
BUF_TYPE_CR, /* Consistent Read 副本,只读,按 cr_scn 构造 */
BUF_TYPE_PI, /* Past Image,只读,X 锁让出后保留 */
} BufferType;
buffer_type 是显式字段(hot tail,offset 52),用于在 eviction / flush / Cache Fusion 等路径快速分流;XCUR 与 SCUR 不是独立的 buffer_type 值,而是 BUF_TYPE_CURRENT + pcm_state = X/S 的组合派生(per buffer-pool-design.md §5.2)。
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 使用(CR 构造时 = snapshot.read_scn)*/
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 状态转换(pcm-lock-protocol-design.md §4,源自 AD-002):
| # | 转换 | 触发场景 |
|---|---|---|
| 1 | N → S | 本节点首次读该 block(LOCK_REQUEST(S) → master) |
| 2 | N → X | 本节点首次写该 block(LOCK_REQUEST(X) → master) |
| 3 | S → X(自身升级) | 本节点持 S 后想写(LOCK_REQUEST(X_UPGRADE)) |
| 4 | X → S(保 PI) | 其他节点请求 S,本节点降级(DOWNGRADE(X→S, keep_pi=true)) |
| 5 | X → N(保 PI) | 其他节点请求 X,本节点完全让出(DOWNGRADE(X→N, keep_pi=true)) |
| 6 | X → N(不保 PI) | evict 前主动释放(RELEASE → master) |
| 7 | S → N(被 invalidate) | 其他节点请求 X,本节点收到 INVALIDATE |
| 8 | S → N(主动) | evict 前主动释放(RELEASE → master) |
| 9 | ITL cleanout S → X | reader 触发的 commit_scn 写回(AD-006 第四轮);cleanout 完成后立即 X → S downgrade |
pgrac 对 PG 的 clock-sweep eviction 做了四池差异化改造(buffer-pool-design.md §9.2)。current(XCUR / SCUR)获得最低驱逐优先级——hot 数据最珍贵;CR 副本最容易驱逐——构造成本可控(沿 undo chain 重建);PI 介于两者之间,但有 5 min TTL 保护,避免 Reconfig 期间被误删。
四池静态划分(默认,GUC cluster_cr_pool_pct / cluster_pi_pool_pct 可调):
| 池 | 默认占比 | 大小(shared_buffers = 16 GB) | 驱逐优先级 |
|---|---|---|---|
| CurrentPool(XCUR / SCUR) | 60% | 9.6 GB | 最低(最珍贵;dirty 需先 flush) |
| CRPool | 20% | 3.2 GB | 最高(按需 O(undo) 重建;不写盘) |
| PIPool | 10% | 1.6 GB | 中(TTL 5 min 后可驱逐) |
| Reserve | 10% | 1.6 GB | 动态调整 |
改造后的 StrategyGetBuffer 三段式(buffer-pool-design.md §9.3):
StrategyGetBuffer():
/* 1. 优先在 CRPool 找 victim(stale 的或 LRU 最老的 CR copy)*/
victim = sweep_cr_pool();
if (victim) return victim;
/* 2. 在 PIPool 找(过 TTL 的优先)*/
victim = sweep_pi_pool();
if (victim) return victim;
/* 3. CurrentPool 经典 clock-sweep 兜底(需先释放 PCM X 锁 + flush dirty)*/
victim = sweep_current_pool();
if (victim->dirty) flush(victim);
return victim;
每 current buffer 的 CR chain 长度上限默认 8(buffer-pool-design.md §6.3,cr_chain 超限时 evict 最老的 CR copy)。CR pool 总占用受 20% 配额约束;CR 命中率在 pg_cluster_cr_chain_stats 视图可观测。
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 四池结构使 current hot 数据得到优先保留,CRPool 吸收全表扫描,避免污染 OLTP 工作集。综合来看 OLTP TPS 影响 < 1%(设计分析结论;1.6 阶段实测验证中)。
BufferDesc 中的 grd_master_node / grd_master_seq 字段(cache line 2,offset 96–99)缓存的是该 block 在 GRD(Global Resource Directory)中的 master 路由信息——避免每次跨节点请求都重查 GRD。完整的 GRD 资源身份模型、分片路由、holders/waiters 表结构详见 第 3 章 · GES 概念。Reconfig 重建 master 后,本字段通过 sinval 失效(buffer-pool-design.md §11.3)。
深度设计细节与相关特性:
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 的刷盘顺序