第 03–04 篇把一次 hop 写成指针或主块路径。那些路径只有落进 page cache 才变成可运维的延迟:命中则是内存里的 pin;未命中则是 page fault + 磁盘读。本篇不写伪造的毫秒数,只钉:缓存什么、页有多大、计量看什么、两种 store format 如何改变 fault 形态。
本文是「图数据库内核」系列第 5 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 3–4 篇 · Record / Block 逻辑布局与 hop 剧本 第 5 篇 · Page cache 页、配置、计量、布局→I/O 第 6 篇 · 写入路径 改链 / 升 dense 时的写放大边界
版本锚定:Operations Manual(2026.07 / current)Performance → Memory configuration 与 Monitoring → Metrics reference(page cache 表)。实现锚点:
neo4j/neo4j5.26.0org.neo4j.io.pagecache.PageCache.PAGE_SIZE、impl.muninn.MuninnPageCache。配置名以 5.x / current 的server.memory.pagecache.size为准(旧文档中的dbms.memory.pagecache.size为历史名)。
一、内存地图里 page cache 占哪一格
手册 Memory configuration 把服务器 RAM 拆成多块。与本系列直接相关的三格:
| 区域 | 配置 / 行为 | 放什么 |
|---|---|---|
| JVM Heap | server.memory.heap.initial_size /
max_size(建议二者相同) |
查询算子、事务中间态、计划等 Java 对象 |
| Page cache | server.memory.pagecache.size |
磁盘上的图数据与 native 索引页 |
| OS 及其它 | 剩量;手册建议专用机先留约 1 GB 给 OS | OS、Lucene 索引、向量索引(手册明确:vector index 不在 Neo4j page cache,而在 OS 内存侧) |
关键分工句:
Page cache is used to cache the Neo4j data stored on disk. The caching of graph data and indexes into memory helps avoid costly disk access…
这里的 indexes 与 capacity planning 段落交叉读:data + native indexes 对齐 page cache;Lucene 体积要和「heap + page cache 分完后的剩余」对照。向量索引另文(第 8 篇边界);不要把「把图库调大 page cache」当成向量检索变快。
事务内存(dbms.memory.transaction.total.max
等)与 page cache 正交:expand 爆炸可以先把
heap/事务限额打满,此时 hit_ratio
仍可能漂亮——第 10、15 篇会回来。本篇默认假设「慢在读盘 /
缓存颠簸」。
站内对照:SQLite Pager 是连接级 page cache;Neo4j page cache 是进程内、跨查询共享的存贮映射缓存(实现类 Muninn)。词汇类似,共享范围不同。
二、页是 8192 字节;布局决定一页里装什么
PageCache.PAGE_SIZE = 8192(5.26.0
接口常量)。store 文件被切成文件页,经 page cache
映射进内存;访问路径是 pin →(命中或 fault)→
读记录/块 → unpin。指标里的 pins /
unpins / page_faults /
hits 描述的就是这层。
2.1 一页能装多少「逻辑邻居」
用第 03–04
篇的定长尺寸做上界心智(aligned
的页对齐填充会使每页记录数略少;此处按逻辑
RECORD_SIZE / 主块估算):
| 布局对象 | 单对象尺寸 | \(8192 / \mathrm{size}\) 约 |
|---|---|---|
| Node record | 15 B | \(\approx 546\) |
| Relationship record | 34 B | \(\approx 241\) |
| Property record | 41 B | \(\approx 199\) |
| Block x1 主块 | 128 B | 恰好 64 |
连续 id 的记录落在同一文件页的概率高;指针追逐跳到任意 id 时,每次跳转都是一次独立的页查找。大 O 仍是 \(\Theta(d)\) 条边信息,fault 次数却可以在「\(\ll d\)」到「\(\sim d\)」之间摇摆——差别全是局部性。
2.2 Record 系:链把局部性拆开
Sparse expand(第 03 篇):
Node 页 → Relᵢ 页 → Relⱼ 页 → … → (可选)Prop 页
若关系 id 在创建时大致单调,短时间内写入的边可能挤在相邻页,热写入后的热遍历会好看。图变旧、删除复用、多类型交错之后,逻辑链上的 next 指针在文件里可以跳得很散——page cache 再大,也在缓存「一锅随机页工作集」。
Dense + group:先读 group 相关页,再只扫目标类型的 Rel 链——减少无效类型的页触碰,但不保证同类型百万边落在少数页上。
2.3 Block 系:共置压缩「每次 hop 的页集合」
手册对 block 的主张可以直接翻译成缓存语言:相关数据共置 → 服务一次查询需要装入的页更少 → fault 机会下降。
| 节点形态 | 理想页触碰 |
|---|---|
| 数据全在 x1 | 该 nodeId 主块所在的 1
个文件页(同页可有最多 64 个主块;邻居点另算) |
| 关系在 xd | x1 页 + 动态关系记录所在页 |
| 类型在 dense | x1(+xd)+ dense B+ 树上的路径页 |
对端节点若也是「小度数、属性内联」,下一跳再次偏向单主块;这是第 02 篇 \(\mathrm{Cost}_{\mathrm{block}}\) 在缓存层的对应物。对端若是超节点,工作集立刻换成「一棵树的热路径 + 大叶集」——格式帮不了输出基数。
flowchart LR
Q["Expand hop"] --> PC{"Page in cache?"}
PC -->|hit| Pin["pin + parse"]
PC -->|miss| Fault["page_fault + disk read"]
Fault --> Pin
Pin --> Next{"More pointers / tree steps?"}
Next -->|yes| PC
Next -->|no| Done["neighbors"]
三、配置与容量:先显式,再按 store 体积估
3.1 显式配置
手册建议:始终在 neo4j.conf
里显式写出 page cache 与
heap;否则启动时按机器资源做启发式估算,行为更难预期。
server.memory.pagecache.size=42GB
server.memory.heap.initial_size=5g
server.memory.heap.max_size=5g
(数值仅为手册示例形态;按你的 store 体积改。)
初始分配可跑:
bin/neo4j-admin server memory-recommendation
# 可选:--memory=16g
命令会打印启发式推荐,并汇报(文档示例口径)全部库的 Lucene 索引总量与 data + native indexes 总量。手册 sanity check:
data + native indexes↔︎server.memory.pagecache.size- Lucene ↔︎ heap 与 page cache 分完后的剩余内存
- 许多场景倾向「尽量把 data + native indexes 放进 page cache」,并留增长余量(示例用 \(\times 1.2\))
这是容量规划启发式,不是 SLA。图查询的工作集常常远小于全库——但超节点与全图扫描式分析会逼近「需要的页 ≈ 很大一部分 store」。
3.2 和 OS page cache 的关系
Neo4j page cache 是进程内受管缓存;OS 仍可能缓存文件页。手册强调:OS 内存不够会 swap,专用机宜关 swap。运维上不要「把 page cache 设到吃光物理内存再指望 OS」——heap、page cache、Lucene/向量、OS 抢同一块 RAM。
Kubernetes
文档补充同一不等式心智:heap + pagecache + headroom <
容器限额(示例 headroom 约 1 GB)。
四、计量:什么叫「缓存健康」
Metrics reference(database page cache 表)给出生产可读信号。名称前缀随导出方式变化;语义如下。
| 指标 | 类型 | 手册要点 |
|---|---|---|
page_cache.hit_ratio |
gauge | hits / lookups;应稳定在 98%–100%;明显更低 = 过于频繁读盘 |
page_cache.usage_ratio |
gauge | 已用页 / 可用页;到 100% 时 hit_ratio 容易随之掉,考虑加大 page cache |
page_cache.page_faults |
counter | 持续爬升可能表示 cache 不够;EE 启动 warmup 造成的大量 fault 属正常 |
page_cache.hits / pins /
unpins |
counter | 访问强度 |
page_cache.evictions /
evictions.cooperative |
counter | 驱逐压力;cooperative = 可用页不足时的协作驱逐 |
page_cache.bytes_read /
bytes_written / iops |
counter | 缓存层实际 I/O 量 |
Essential metrics 页用同一口径强调:miss →
慢磁盘;hit_ratio 理想高于
98%;usage_ratio 顶满则考虑加内存。
读图时的解释纪律:
- 全局 hit_ratio 高 ≠ 某条 Cypher 不痛:一条查询可以专打冷区或超节点叶页。
- fault 高要分时段:对照启动/warmup、批量导入、分析型扫库。
- usage_ratio 低且 hit_ratio 低:可能是工作集切换极快,或测的是刚启动;不要只看一个瞬时点。
- 只扩 page cache 救不了:计划扇出估计错误导致的中间结果爆炸(heap),或向量/Lucene 路径。
实现侧:MuninnPageCache(5.26.0)在可用页耗尽时走
cooperative eviction,并有后台 eviction
线程——与指标中的 evictions.cooperative
对应。本篇不展开 Muninn
内部时钟细节;运维先读指标,再下钻源码。
五、三种故障形态(布局 × 缓存)
5.1 随机指针颠簸(record sparse / 破损局部性)
症状:中等大小 page cache,hit_ratio
上不去;bytes_read 随 QPS 线性涨;PROFILE
显示大量 DB hits(若你有环境观察)。
机制:关系链/属性链跨页跳跃,工作集页数 \(\approx\)
触碰记录数量级。
杠杆:加大
cache(治标);减少无谓属性读取;建模降低平均链长;EE 评估迁
block(治本方向,见第 04
篇);查询侧避免无索引的大起点 + 深 expand。
5.2 超节点扫叶(dense 链或 dense 树)
症状:全局 hit_ratio 仍可能可接受,但单查询延迟与
pins 尖峰绑定个别点;扩 cache
直到塞进该点的全部边页之前,收益阶梯状。
机制:类型过滤已做对,输出边集合本身的页数就是下界。
杠杆:建模拆超节点;限制变长路径;业务避免「从门户点出发无界遍历」;第
15 篇清单。
5.3 缓存够、堆不够(伪 I/O 问题)
症状:hit_ratio
优秀,查询被事务内存限制杀掉或 GC 抖动。
机制:页都在内存,算子物化了巨大中间路径集合。
杠杆:计划与查询重写(第 10 篇),不是加
pagecache.size。
六、和代价模型的闭合
第 02 篇的四种布局,在 page cache 语言下重述:
| 布局 | 缓存友好条件 | 典型恶化 |
|---|---|---|
| 边表 + 索引 | 二级索引叶页顺序扫 | 多跳反复探测不同索引范围 |
| CSR | 邻接数组顺序页 | 更新结构;OLTP 少见纯 CSR |
| Record 指针链 | 短链、id 局部、热子图 | 长链随机;属性链加倍 |
| Block 内联 / dense 树 | 中小度数共置;按类型树探 | 固定 128 B 税;单类型超大叶集 |
原生图是否「更快」在缓存层变成可检验命题:同一工作负载下,触碰的互异页数与
page_faults
增量是否下降。没有本机对比实验时,正文只保留该命题,不写名次。
七、争论与开放问题
- 「尽量全库进 cache」vs「按工作集 sizing」:手册 capacity planning 偏向前者加增长余量;多租户与成本压力逼出后者。图负载的工作集随查询起点漂移——固定全库缓存是运维上界,不是查询下界。
- Block 减少 fault vs 固定主块放大
store:store 变大 → 想维持「全库缓存」需要更大
pagecache.size。局部性收益与容量税之间的净效应依赖度数分布,需实测(本站未跑)。 - 开放问题:dense B+ 树页在 Muninn
中的扫描模式与 vectored fault(指标已有
page_vectored_faults)如何服务顺序叶扫;多数据库共享同一 page cache 池时的干扰。写入路径如何弄脏页并触发 flush/eviction——第 6 篇。
八、来源与实验台账(本篇)
| 结论 | 等级 | 来源 |
|---|---|---|
| page cache 缓存 data +(native)索引;配置键;显式配置建议;与 Lucene/向量内存分工 | A | Operations Manual Memory configuration(current) |
hit_ratio
98–100%、usage_ratio、faults/warmup、cooperative
eviction 等指标语义 |
A | Operations Manual Metrics reference / Essential metrics |
PAGE_SIZE = 8192;Muninn 为 page cache
实现;cooperative eviction 路径存在 |
A | neo4j 5.26.0
PageCache.java /
MuninnPageCache.java |
| 每页记录数估算 | 方法推导 | 第 03–04 篇尺寸 ÷ 8192 |
| 本机 hit_ratio / PROFILE 对照 | 未跑 | 不伪造曲线 |
九、小结
- 布局决定触碰哪些页;page cache 决定这些页是否读盘。 二者缺一不可解释「原生图快不快」。
- 运维旋钮:显式
server.memory.pagecache.size;用memory-recommendation对齐 store 体积;用hit_ratio/usage_ratio/page_faults看健康,并排除 warmup 与堆爆炸伪影。 - Record 怕随机链;block 赌共置;超节点都怕叶集。 扩缓存是杠杆之一,不是唯一杠杆。
- 下一篇:写入时如何改关系链、升 dense、弄脏页,以及空间回收边界如何反咬读路径局部性。
→ 系列目录 · 上一篇:Block format · 下一篇:写入路径
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【图数据库内核】Neo4j Block format:128 B 主块、动态溢出与 dense B+ 树
对照第 03 篇 record 指针链,钉住 Enterprise 推荐的 block 布局:block.x1.db 128 B 主块、node/relationship 动态 store、big_values、relationship dense 多根 B+ 树与实体上限;重写一次 hop 的读路径。
【图数据库内核】图引擎全景:属性图作为一等公民的存储与遍历
定位属性图相对行存/LSM/向量/GraphRAG 的生态位;钉住邻接代价、Neo4j store format(record→block)、page cache、索引与 Expand、Cypher 计划五条坐标系,并以 Angles & Gutierrez 谱系与原生图争论收束系列路线。
【图数据库内核】邻接的代价模型:边表 JOIN、CSR 与原生指针为何不是同一件事
把同一逻辑图落成边表+索引、CSR、原生关系链与 Neo4j block 内联四条路径,用统一代价语言比较一次 hop 与 k 跳扩张;钉住幂律超节点与局部性,为后续 record/block 布局篇垫底座。
【图数据库内核】Neo4j record 系布局:节点、关系链、属性链与 dense group
以 Neo4j 5.26 record-storage-engine 源码钉住 standard/aligned 固定记录:15B 节点、34B 关系、41B 属性、25B relationship group;讲清 sparse 链、dense 阈值与一次 hop 的指针路径。