neon源码分析(5)计算层使用slru的一些问题
1. PG 原生 SLRU 是什么
SLRU 用来保存事务相关的小页面文件,常见目录:
pg_xact pg_multixact/members pg_multixact/offsets一个 SLRU page 是 8KB。一个 SLRU segment 通常包含 32 个 page:
1 segment = 32 * 8KB = 256KB 例子:pg_xact/0001 page 0 8KB page 1 8KB ... page 31 8KBPG 原生逻辑假设文件在本地磁盘上。
2. Neon 为什么改造 SLRU
Neon 不希望把完整pg_xact/pg_multixact文件都放进 basebackup,否则 endpoint 启动和传输成本会变大。
所以 Neon 的策略是:
本地 SLRU segment 不存在 | v 从 pageserver 按需下载该 segment | v 写成本地 SLRU 文件 | v 继续走 PG 原生 SLRU 读逻辑也就是说,Neon 改造的是compute 本地 SLRU 文件缺失时的 fallback 路径。
3. Compute 侧关键改造
关键文件:
neon/vendor/postgres-v17/src/backend/access/transam/slru.cNeon 新增核心函数:
staticintSimpleLruDownloadSegment(SlruCtl ctl,intpageno,charconst*path)关键逻辑:
/* page 超过 latest_page_number,不尝试下载 */if(ctl->PagePrecedes(pg_atomic_read_u64(&shared->latest_page_number),pageno))return-1;segno=pageno/SLRU_PAGES_PER_SEGMENT;buffer=palloc(BLCKSZ*SLRU_PAGES_PER_SEGMENT);n_blocks=smgr_read_slru_segment(&dummy_smgr_rel,path,segno,buffer);if(n_blocks>0){fd=OpenTransientFile(path,O_RDWR|O_CREAT|PG_BINARY);pg_pwrite(fd,buffer,n_blocks*BLCKSZ,0);}含义:
- 由
pageno算出segno。 - 调用 Neon 的
smgr_read_slru_segment()。 - 从 pageserver 获取该 SLRU segment 的内容。
- 写成本地文件,比如
pg_xact/0001。 - 返回 fd,后续 PG 继续用普通
pread()读目标 page。
和 PG 原生的差异
原生 PG:
open pg_xact/0001 失败,errno == ENOENT | +-- recovery 中:可能读 zero page | +-- 非 recovery:报错Neon:
open pg_xact/0001 失败,errno == ENOENT | v SimpleLruDownloadSegment() | +-- 下载成功:写本地文件,然后继续读 | +-- 下载失败:回到 PG 原生处理逻辑影响的主要路径:
SimpleLruDoesPhysicalPageExist() SlruPhysicalReadPage()4. SLRU 请求路径
端到端调用链:
SlruPhysicalReadPage() | | 本地 SLRU segment 文件不存在 v SimpleLruDownloadSegment() | v smgr_read_slru_segment() | v neon_read_slru_segment() | | 计算 request_lsn / not_modified_since v communicator_read_slru_segment() | v pageserver GetSlruSegment(kind, segno, lsn) | v pageserver 按 8KB block reconstruct segment | v 返回 n_blocks * 8KB | v compute 写成本地 pg_xact/0001关键 compute 文件:
neon/pgxn/neon/pagestore_smgr.c neon/pgxn/neon/communicator.c5. SLRU 请求 LSN 怎么算
关键函数:
neon/pgxn/neon/pagestore_smgr.c neon_read_slru_segment(...)关键逻辑:
if(RecoveryInProgress()){request_lsn=GetXLogReplayRecPtr(NULL);if(request_lsn==InvalidXLogRecPtr)request_lsn=GetRedoStartLsn();request_lsn=nm_adjust_lsn(request_lsn);}elserequest_lsn=UINT64_MAX;not_modified_since=nm_adjust_lsn(GetRedoStartLsn());含义:
| 场景 | request_lsn |
|---|---|
| recovery 中 | 当前 replay LSN |
| recovery 刚开始,还没有 replay LSN | redo start LSN |
| 非 recovery | UINT64_MAX,表示取最新 |
not_modified_since使用 basebackup 的 redo start LSN。
原因:未下载到本地的 SLRU segment,如果要被本地修改,必须先下载;下载后也不会被本地淘汰。所以对于尚未下载的 segment,可以认为它自 basebackup LSN 以来没有被本地改过。
6. SLRU 页面有没有版本
有,但不是存在 SLRU page header 里。
普通 heap/index page 自身有 page header,例如pd_lsn。SLRU page 没有类似普通 page 的pd_lsn。Neon 的 SLRU 版本保存在 pageserver timeline/layer 中:
slru_block_to_key(kind, segno, blkno) @ LSN这里的 LSN 是 WAL LSN。
更具体地说,某条 WAL record 修改了 SLRU,那么该修改在 pageserver 中一般以WAL record 的 end LSN作为版本点。
WAL record 范围:[start_lsn, end_lsn) record 修改 CLOG page | v pageserver 写入: slru_block_to_key(Clog, segno, blkno) @ end_lsn所以之前说的:
SLRU 的版本 LSN 是 WAL LSN这里的 LSN 可以理解成:
该 WAL record 的最后位置 + 1 也就是 WAL record end LSN例子
WAL record R: start_lsn = 0/5000100 end_lsn = 0/5000188 R 修改了 pg_xact/0001 的第 5 个 SLRU page pageserver 中的版本: slru_block_to_key(Clog, 1, 5) @ 0/5000188当 compute recovery 到0/5000188,请求:
GetSlruSegment(Clog, segno=1) @ 0/5000188应该可以看到这条 WAL record 对 CLOG 的修改。
7. Neon 按什么粒度保存 SLRU
需要区分两个粒度:
compute <-> pageserver 请求粒度:segment pageserver 内部保存粒度:8KB block/page也就是说,compute 请求时是:
GetSlruSegment(kind, segno)返回大小是:
n_blocks * 8KB 通常最大 32 * 8KB = 256KB但 pageserver 内部不是把完整 256KB 文件作为一个 value 保存,而是拆成 8KB block:
slru_block_to_key(kind, segno, 0) -> 8KB slru_block_to_key(kind, segno, 1) -> 8KB ... slru_block_to_key(kind, segno, 31) -> 8KB例子
compute 要读 pg_xact/0001 | v 请求 GetSlruSegment(Clog, 1) | v pageserver 内部读取: size key 得到 n_blocks = 32 block key 0..31 各取 8KB | v 拼成 256KB 返回 compute8. pageserver 为什么有三类 SLRU key
关键文件:
neon/libs/pageserver_api/src/key.rs三类 key:
| key 类型 | 函数 | 是否带 segno | 是否带 blkno | 作用 |
|---|---|---|---|---|
| directory | slru_dir_to_key(kind) | 否 | 否 | 记录某类 SLRU 有哪些 segment |
| segment size | slru_segment_size_to_key(kind, segno) | 是 | 否 | 记录某 segment 有多少 block |
| data block | slru_block_to_key(kind, segno, blknum) | 是 | 是 | 保存实际 8KB page 或 WAL delta |
8.1 directory key
源码:
pubfnslru_dir_to_key(kind:SlruKind)->Key{Key{field1:0x01,field2:matchkind{SlruKind::Clog=>0x00,SlruKind::MultiXactMembers=>0x01,SlruKind::MultiXactOffsets=>0x02,},field3:0,field4:0,field5:0,field6:0,}}说明:
每个 SlruKind 一个 directory key。 key 形状基本固定,不带 segno / blkno。 value 是 segment 集合。例子:
slru_dir_to_key(Clog) -> value = {0, 1, 2, 3}8.2 segment size key
源码:
pubfnslru_segment_size_to_key(kind:SlruKind,segno:u32)->Key{Key{field1:0x01,field2:kind_code,field3:1,field4:segno,field5:0,field6:0xffff_ffff,}}说明:
每个 kind + segno 一个 size key。 field6 = 0xffffffff 是 sentinel,表示这是 size key,不是普通 block key。 value 是 nblocks。例子:
slru_segment_size_to_key(Clog, 1) -> value = 328.3 data block key
源码:
pubfnslru_block_to_key(kind:SlruKind,segno:u32,blknum:BlockNumber)->Key{Key{field1:0x01,field2:kind_code,field3:1,field4:segno,field5:0,field6:blknum,}}说明:
每个 kind + segno + blknum 一个 data key。 value 是 8KB page image 或 WAL delta。例子:
slru_block_to_key(Clog, 1, 0) -> pg_xact/0001 第 0 个 8KB page slru_block_to_key(Clog, 1, 31) -> pg_xact/0001 第 31 个 8KB page三类 key 的关系图
pg_xact/0001 in pageserver Directory key: slru_dir_to_key(Clog) value contains segment 1 Segment size key: slru_segment_size_to_key(Clog, 1) value = 32 Data block keys: slru_block_to_key(Clog, 1, 0) -> 8KB slru_block_to_key(Clog, 1, 1) -> 8KB ... slru_block_to_key(Clog, 1, 31) -> 8KB回答前面的问题:
前两种 key 是不是写死的? 不是完全写死: - directory key:只跟 kind 有关,所以对某个 kind 来说是固定的。 - segment size key:跟 kind + segno 有关,不带 blkno。 - data block key:跟 kind + segno + blkno 有关。9. pageserver 如何拼出 SLRU segment
关键文件:
neon/pageserver/src/pgdatadir_mapping.rs核心函数:
get_slru_segment(kind,segno,lsn,ctx)关键代码:
letn_blocks=self.get_slru_segment_size(kind,segno,Version::at(lsn),ctx).await?;letkeyspace=KeySpace::single(slru_block_to_key(kind,segno,0)..slru_block_to_key(kind,segno,n_blocks),);letquery=VersionedKeySpaceQuery::uniform(batch,lsn);letblocks=self.get_vectored(query,io_concurrency.clone(),ctx).await?;for(_key,block)inblocks{letblock=block?;segment.extend_from_slice(&block[..BLCKSZasusize]);}流程:
GetSlruSegment(Clog, 1, lsn=X) | v 读 slru_segment_size_to_key(Clog, 1) @ X | | n_blocks = 32 v 读 block key 0..31 @ X | v 拼成 segment bytes | v 返回 compute10. WAL ingest 如何写 SLRU
关键文件:
neon/pageserver/src/walingest.rs neon/pageserver/src/pgdatadir_mapping.rs事务 commit / abort 会写 CLOG:
modification.put_slru_wal_record(SlruKind::Clog,segno,rpageno,NeonWalRecord::ClogSetCommitted{...},)?;MultiXact offsets:
modification.put_slru_wal_record(SlruKind::MultiXactOffsets,segno,rpageno,NeonWalRecord::MultixactOffsetCreate{...},)?;MultiXact members:
modification.put_slru_wal_record(SlruKind::MultiXactMembers,pageno/SLRU_PAGES_PER_SEGMENT,pageno%SLRU_PAGES_PER_SEGMENT,NeonWalRecord::MultixactMembersCreate{...},)?;最终写入 block key:
self.put(slru_block_to_key(kind,segno,blknum),Value::WalRecord(rec),);zero page / full image 则写:
self.put(slru_block_to_key(kind,segno,blknum),Value::Image(img));11. segment 创建、扩展、删除
创建 segment
put_slru_segment_creation(kind,segno,nblocks,ctx)做两件事:
1. 更新 directory key,把 segno 加入集合 2. 写 segment size key,value = nblocks扩展 segment
put_slru_extend(kind,segno,nblocks)主要更新:
slru_segment_size_to_key(kind, segno) = new_nblocks如果中间有 gap,会补 zero page。
删除 segment
drop_slru_segment(kind,segno,ctx)做两件事:
1. 从 directory key 的 segment 集合里移除 segno 2. 删除该 segment 的 size key 和所有 block key删除范围:
slru_segment_key_range(kind,segno)覆盖:
slru_block_to_key(kind, segno, 0..) slru_segment_size_to_key(kind, segno)12. 总结
一句话:
Neon compute 缺 SLRU 文件时,按 segment 从 pageserver 下载; pageserver 内部按 8KB SLRU block 做版本化存储; 版本点是 WAL record end LSN。关键结论:
1. compute 拉取粒度:segment,通常最大 256KB。 2. pageserver 保存粒度:8KB SLRU block。 3. SLRU page 没有自身 pd_lsn,版本在 pageserver key @ LSN 中。 4. SLRU 的 LSN 是 WAL LSN,通常是 WAL record end LSN。 5. 三类 key: - dir key:记录有哪些 segment - segment size key:记录某 segment 有几个 block - data block key:记录实际 8KB 数据或 WAL delta最终图:
WAL ingest | v pageserver timeline | | dir key @ LSN | size key @ LSN | block key @ LSN v compute 读 pg_xact/0001 | | 本地不存在 v SimpleLruDownloadSegment() | v GetSlruSegment(Clog, 1, request_lsn) | v pageserver 读取 size + block keys @ request_lsn | v 拼出 segment bytes | v compute 写本地 pg_xact/0001 | v PG 原生 SLRU 继续读目标 page