土法炼钢兴趣小组的算法知识备份

【图数据库内核】Page cache 与指针追逐:布局如何变成命中率

文章导航

分类入口
databasegraph
标签入口
#graph-database#neo4j#page-cache#muninn#locality#pointer-chasing#hit-ratio#memory

目录

0304 篇把一次 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 configurationMonitoring → Metrics reference(page cache 表)。实现锚点:neo4j/neo4j 5.26.0 org.neo4j.io.pagecache.PageCache.PAGE_SIZEimpl.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:

  1. data + native indexes ↔︎ server.memory.pagecache.size
  2. Lucene ↔︎ heap 与 page cache 分完后的剩余内存
  3. 许多场景倾向「尽量把 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 顶满则考虑加内存。

读图时的解释纪律

  1. 全局 hit_ratio 高 ≠ 某条 Cypher 不痛:一条查询可以专打冷区或超节点叶页。
  2. fault 高要分时段:对照启动/warmup、批量导入、分析型扫库。
  3. usage_ratio 低且 hit_ratio 低:可能是工作集切换极快,或测的是刚启动;不要只看一个瞬时点。
  4. 只扩 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 增量是否下降。没有本机对比实验时,正文只保留该命题,不写名次。


七、争论与开放问题

  1. 「尽量全库进 cache」vs「按工作集 sizing」:手册 capacity planning 偏向前者加增长余量;多租户与成本压力逼出后者。图负载的工作集随查询起点漂移——固定全库缓存是运维上界,不是查询下界。
  2. Block 减少 fault vs 固定主块放大 store:store 变大 → 想维持「全库缓存」需要更大 pagecache.size。局部性收益与容量税之间的净效应依赖度数分布,需实测(本站未跑)。
  3. 开放问题: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 对照 未跑 不伪造曲线

九、小结

  1. 布局决定触碰哪些页;page cache 决定这些页是否读盘。 二者缺一不可解释「原生图快不快」。
  2. 运维旋钮:显式 server.memory.pagecache.size;用 memory-recommendation 对齐 store 体积;用 hit_ratio / usage_ratio / page_faults 看健康,并排除 warmup 与堆爆炸伪影。
  3. Record 怕随机链;block 赌共置;超节点都怕叶集。 扩缓存是杠杆之一,不是唯一杠杆。
  4. 下一篇:写入时如何改关系链、升 dense、弄脏页,以及空间回收边界如何反咬读路径局部性。

系列目录 · 上一篇:Block format · 下一篇:写入路径

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .