Neon LFC

发布时间:2026/7/24 17:21:40

Neon LFC LFCLocal File Cache是 Neon compute 端加速读性能的核心机制——把 pageserver 上缓存的页面落到 compute 本地的 SSD/NVMe 上避免每次读都走网络调用 pageserver。TPCC 是高并发随机读 workload没有本地缓存意味着每个 page request 都要通过网络 go 到 pageserver fetch热点数据无法在本地复用miss rate 接近 100%网络往返延迟和 pageserver 背压直接影响 tpmC快速验证SHOW neon.file_cache_size_limit; -- 应该是 0 (disabled)解决方向调大 neon.file_cache_size_limit单位 MB比如ALTER SYSTEM SET neon.file_cache_size_limit 4096; -- 4GB SELECT pg_reload_conf();或者更大如果有足够磁盘空间的话。TPCC 的 working set 通常在几 GB 到几十 GB 量级设一个合理的值能让 LFC 覆盖大部分热点页tpmC 会有显著提升。max_file_cache_size 是硬上限运行时只能通过 ALTER SYSTEM 缩小 file_cache_size_limit不能超过这个值。如果只配了 file_cache_size_limit4096 但 max_file_cache_size 默认也是 0那就设不进去会报错LFC: neon.file_cache_size_limit can not be larger than neon.max_file_cache_size两个都得上参数作用域生效时机neon.max_file_cache_sizePGC_POSTMASTER后加载需要重启 computeneon.file_cache_size_limitPGC_SIGHUP动态改pg_reload_conf() 即可正确做法先在 compute manifest / config 里把 max 设好或重启时传入运行时动态调 limit 就能调整缓存大小TPCC workload 建议把 max 至少设到 working set 的预估大小——通常从几 GB 起步跑一下 pg_stat_user_tables 看数据量再根据实际 cache hit ratio 往上加。neon.file_cache_pathneon.file_cache_path 就是 LFC 底层那个 sparse file 的存放路径。目前设的是 file.cache意味着 LFC 数据写在相对于 PG data directory 的 file.cache 这个文件里。比如 PG data dir 在 /var/lib/postgresql/16/main/那 LFC 文件实际就是/var/lib/postgresql/16/main/file.cache问题在于如果 /var/lib/postgresql/16/main/ 这个目录所在磁盘不够大通常是系统盘可能只有十几 GB而你的 max_file_cache_size 设了几十 GB那么磁盘很快就会被打满LFC 频繁 evict性能反而更差——cache 不驻留等于没有 cache。最佳实践把 file_cache_path 指向一个独立的、容量足够的 SSD/NVMe 盘比如ALTER SYSTEM SET neon.file_cache_path /mnt/lfc/file.cache; SELECT pg_reload_conf(); -- 需要重启 computePOSTMASTER 参数确保 /mnt/lfc/ 所在磁盘有足够空间至少跟 max_file_cache_size 相当。存储原理存储的是最近访问的数据LRU 淘汰格式就是一个稀疏文件里按 chunk 组织的原始 PostgreSQL data blocks。数据结构file.cache (一个巨型 sparse file) ┌─────────────────────────────────────┐ │ Hash Table (内存中) │ │ BufferTag (rel, fork, block) │ │ ↓ │ │ Chunk offset (1MB per chunk) │ └─────────────────────────────────────┘物理存储 (file.cache 文件)┌───chunk0───┬───chunk1───┬───chunk2───┬───hole───┬───chunkN───┐ │ 1024 pages │ 1024 pages │ 1024 pages │ (空洞) │ 1024 pages │ │ 8MB │ 8MB │ 8MB │ │ 8MB │ └────────────┴────────────┴────────────┴──────────┴────────────┘关键点维度细节块大小PostgreSQL BLCKSZ 8KB分配单元 (chunk)默认 1MB 128 个 block (PG_MAJORVERSION_NUM 16 时是 7 1MB 16 时可配置)缓存策略LRU 双链表淘汰组织方式所有数据存单个 file.cache 文件用 hash table 做 BufferTag → chunk offset 映射空间回收fallocate(FALLOC_FL_PUNCH_HOLE) —— 不 delete打空洞nominal size 不变实际磁盘占用减少写入方式preadv/pwritev 对齐 IO工作流程Postgres 读 page (smgrread) │ ▼ LFC 查找 hash(tag) → entry? │ │ yes no │ │ ▼ ▼ chunk 在文件中 去 pageserver fetch │ (通过 neon_getpage gRPC) yes ── pread → 返回 no ── pread pageserver → 写回 file.cache chunk │ │ │ ▼ │ 标记 AVAILABLE, LRU 移到头 │ │ ▼ ▼ 更新 LRU 头 (最近访问)淘汰时LRU 尾部的 chunk evict → FALLOC_FL_PUNCH_HOLE (释放磁盘) → hash entry 标记 UNAVAILABLE所以本质上是一个纯用户态的页面级 cache——不改变 Postgres 的任何存储语义只是在 Postgres buffer pool 和 pageserver 之间插了一层本地磁盘缓存。重启后就重建不持久化 mapping。重启行为文件中的数据不会清空。file.cache 作为磁盘上的 sparse file 一直保留着重启后文件还在、内容还在、磁盘空间占用也还在。内存中的 Hash 表重建了而且所有 entry 都标记为 UNAVAILABLE。这意味着LRU 链表是空的hash table 里有 hole entry空洞用于 reuse但没有数据 entry所有之前缓存的 block 对用户态来说都不存在了效果文件里那些 data blocks 成了废数据。下次读同一个 page 时hash lookup → entry 不存在 / UNAVAILABLE当成 cache miss重新从 pageserver fetch写回 chunk重新建 hash mapping相当于冷启动——性能退化和没有 LFC 一样。为什么设计上接受这个代码注释里其实写了原因 (file_cache.c:67)// Cache is always reconstructed at node startup, so we do not need to save mapping somewhere and worry about its consistency.设计者的权衡很清楚LFC 只是一个加速层不追求持久化。保存 mapping 再恢复的成本太高需要维护一致性、处理跨 restart 的 remote storage 变更等不如冷启动后逐步 warm up 来得简单可靠。实际上这也是 LFC 被称为 local file cache 而不是 local persistent cache 的原因——它更像 Redis 的 non-persistent mode。预热机制PostgreSQL 原生有一个 PREPARE更准确说是 pg_prewarm extensionNeon 扩展了它让 LFC 知道哪些页面应该预加载-- Neon 内置的预加载机制 -- compute 启动后、接受请求前执行 SELECT * FROM pg_prewarm(orders); -- 预加载整张表 SELECT * FROM pg_prewarm(orders, buffer); -- 只加载到 PG buffer pool SELECT * FROM pg_prewarm(orders, lfc); -- 加载到 LFC ← Neon 扩展实际调用链路是pg_prewarm(..., lfc) → LFC prewarm worker (file_cache.c: lfc_prewarm()) → 批量调用 neon_getpage gRPC 从 pageserver fetch → 写入 chunk → hash entry AVAILABLE和冷启动的关系没有 prewarm 的情况compute 启动 → 用户第一个请求 SELECT * FROM orders WHERE id1 → LFC miss → gRPC call to pageserver → 网络延迟 I/O → 后续类似请求继续 miss... └──→ 几秒到几十秒的冷启动抖动有 prewarm 的情况compute 启动 → 后台 prewarm worker 批量拉取热点表到 LFC → 用户请求到达 → LFC hit → 本地 pread 1ms └──→ 无冷启动抖动LFC 内部 prewarm 机制file_cache.c 中有专门的 prewarm worker 池n_prewarm_workers —— 并发 worker 数量prewarm_batch —— 每次批量 pre-fetch 的大小prewarm_limit —— 最多预加载多少 chunkslfc_prewarm() —— 从 persisted state 恢复上次缓存的文件映射避免全量重拉也就是说 file.cache 虽然重启后 hash table 清空但 LFC 有持久化的 state 记录如果有的话可以在重启后按需 prewarm 重新填充。这部分代码在 file_cache_state 结构里 (FileCacheState)。对比表格pg_prewarmPostgreSQL 标准扩展LFC prewarmNeon 内置机制目标PostgreSQL shared buffer poolNeon LFC (本地磁盘文件)来源contrib/pg_prewarmpgxn/neon/file_cache.c作用把数据从 disk/remote 加载到 PG 内存 buffer把数据从 pageserver 拉取到 file.cache调用SQL: SELECT pg_prewarm(tbl)SQL: SELECT get_local_cache_state(), prewarm_local_cache()持久化不涉及重启就没了FileCacheState → DSM segment → background worker 异步拉取LFC 的 prewarm 机制源码里有三件套-- 1. 导出当前 LFC 缓存状态 SELECT get_local_cache_state(10000); -- 最多导出 10000 chunks -- 2. 用导出的状态做 prewarm SELECT prewarm_local_cache(state_bytes, n_workers); -- 3. 查看 prewarm 进度 SELECT * FROM get_prewarm_info();lfc_get_state() 快照当前 LRU 链表上的 chunk mapping存入 FileCacheState 结构体通过 DSM segment 传给 prewarm worker 池异步批量拉取。所以它们是完全独立的两个子系统。如果你要加速 LFC 冷启动预热用 Neon 的 get_local_cache_state / prewarm_local_cache如果只加速 PG buffer pool用标准的 pg_prewarm。两者的共同点两者的共同点只是预热这个场景pg_prewarm: remote → disk → PG buffer pool (shared memory) LFC prewarm: pageserver → file.cache (sparse file) → hash table都是提前把数据拉到本地但路径、存储目标、数据结构全不同没有代码复用关系。

相关新闻