neon源码分析(5)计算层使用slru的一些问题
1. PG 原生 SLRU 是什么SLRU 用来保存事务相关的小页面文件常见目录pg_xact pg_multixact/members pg_multixact/offsets一个 SLRU page 是 8KB。一个 SLRU segment 通常包含 32 个 page1 segment 32 * 8KB 256KB 例子pg_xact/0001 page 0 8KB page 1 8KB ... page 31 8KBPG 原生逻辑假设文件在本地磁盘上。2. Neon 为什么改造 SLRUNeon 不希望把完整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;segnopageno/SLRU_PAGES_PER_SEGMENT;bufferpalloc(BLCKSZ*SLRU_PAGES_PER_SEGMENT);n_blockssmgr_read_slru_segment(dummy_smgr_rel,path,segno,buffer);if(n_blocks0){fdOpenTransientFile(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 原生的差异原生 PGopen pg_xact/0001 失败errno ENOENT | -- recovery 中可能读 zero page | -- 非 recovery报错Neonopen 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_lsnGetXLogReplayRecPtr(NULL);if(request_lsnInvalidXLogRecPtr)request_lsnGetRedoStartLsn();request_lsnnm_adjust_lsn(request_lsn);}elserequest_lsnUINT64_MAX;not_modified_sincenm_adjust_lsn(GetRedoStartLsn());含义场景request_lsnrecovery 中当前 replay LSNrecovery 刚开始还没有 replay LSNredo start LSN非 recoveryUINT64_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, segno1) 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 blockslru_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三类 keykey 类型函数是否带 segno是否带 blkno作用directoryslru_dir_to_key(kind)否否记录某类 SLRU 有哪些 segmentsegment sizeslru_segment_size_to_key(kind, segno)是否记录某 segment 有多少 blockdata blockslru_block_to_key(kind, segno, blknum)是是保存实际 8KB page 或 WAL delta8.1 directory key源码pubfnslru_dir_to_key(kind:SlruKind)-Key{Key{field1:0x01,field2:matchkind{SlruKind::Clog0x00,SlruKind::MultiXactMembers0x01,SlruKind::MultiXactOffsets0x02,},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_blocksself.get_slru_segment_size(kind,segno,Version::at(lsn),ctx).await?;letkeyspaceKeySpace::single(slru_block_to_key(kind,segno,0)..slru_block_to_key(kind,segno,n_blocks),);letqueryVersionedKeySpaceQuery::uniform(batch,lsn);letblocksself.get_vectored(query,io_concurrency.clone(),ctx).await?;for(_key,block)inblocks{letblockblock?;segment.extend_from_slice(block[..BLCKSZasusize]);}流程GetSlruSegment(Clog, 1, lsnX) | 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 会写 CLOGmodification.put_slru_wal_record(SlruKind::Clog,segno,rpageno,NeonWalRecord::ClogSetCommitted{...},)?;MultiXact offsetsmodification.put_slru_wal_record(SlruKind::MultiXactOffsets,segno,rpageno,NeonWalRecord::MultixactOffsetCreate{...},)?;MultiXact membersmodification.put_slru_wal_record(SlruKind::MultiXactMembers,pageno/SLRU_PAGES_PER_SEGMENT,pageno%SLRU_PAGES_PER_SEGMENT,NeonWalRecord::MultixactMembersCreate{...},)?;最终写入 block keyself.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 创建、扩展、删除创建 segmentput_slru_segment_creation(kind,segno,nblocks,ctx)做两件事1. 更新 directory key把 segno 加入集合 2. 写 segment size keyvalue nblocks扩展 segmentput_slru_extend(kind,segno,nblocks)主要更新slru_segment_size_to_key(kind, segno) new_nblocks如果中间有 gap会补 zero page。删除 segmentdrop_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