Neon LFC

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_sizePGC_POSTMASTER(后加载)需要重启 compute
neon.file_cache_size_limitPGC_SIGHUP(动态改)pg_reload_conf() 即可

正确做法

  1. 先在 compute manifest / config 里把 max 设好(或重启时传入)
  2. 运行时动态调 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 时:

  1. hash lookup → entry 不存在 / UNAVAILABLE
  2. 当成 cache miss,重新从 pageserver fetch
  3. 写回 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 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

都是"提前把数据拉到本地",但路径、存储目标、数据结构全不同,没有代码复用关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值