LFC(Local 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_limit=4096 但 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_size | PGC_POSTMASTER(后加载) | 需要重启 compute |
| neon.file_cache_size_limit | PGC_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_path
neon.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(); -- 需要重启 compute(POSTMASTER 参数)
确保 /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 extension),Neon 扩展了它让 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 id=1"
→ 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 —— 最多预加载多少 chunks
- lfc_prewarm() —— 从 persisted state 恢复上次缓存的文件映射,避免全量重拉
也就是说 file.cache 虽然重启后 hash table 清空,但 LFC 有持久化的 state 记录(如果有的话),可以在重启后按需 prewarm 重新填充。这部分代码在 file_cache_state 结构里 (FileCacheState)。
对比表格
| pg_prewarm(PostgreSQL 标准扩展) | LFC prewarm(Neon 内置机制) | |
|---|---|---|
| 目标 | PostgreSQL shared buffer pool | Neon LFC (本地磁盘文件) |
| 来源 | contrib/pg_prewarm | pgxn/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
都是"提前把数据拉到本地",但路径、存储目标、数据结构全不同,没有代码复用关系。

296

被折叠的 条评论
为什么被折叠?



