5.1 存储引擎内幕:免索引邻接的磁盘真相


文档摘要

5.1 存储引擎内幕:免索引邻接的磁盘真相 本节摘要:免索引邻接不是一个抽象卖点,而是一组具体的磁盘布局决策:节点与关系各占一条定长记录,关系记录用双向链表挂在两端节点上,属性拆成条块按需续接。本节拆开这层布局,推演出三个工程结论——遍历为何与总量无关、写入为何有固定开销、内存为何该优先喂页缓存。 第 1 章我们用"节点旁挂着小纸条"比喻免索引邻接。本节把纸条翻过来,看看上面到底写了什么、占几个字节、为什么这样设计。 一、存储分区:五类记录文件 Neo4j 的原生图存储把数据按类型分文件存放,每类都是定长记录的数组: 节点记录里最重要的字段是指向第一条关系的指针;

5.1 存储引擎内幕:免索引邻接的磁盘真相

本节摘要:免索引邻接不是一个抽象卖点,而是一组具体的磁盘布局决策:节点与关系各占一条定长记录,关系记录用双向链表挂在两端节点上,属性拆成条块按需续接。本节拆开这层布局,推演出三个工程结论——遍历为何与总量无关、写入为何有固定开销、内存为何该优先喂页缓存。

第 1 章我们用"节点旁挂着小纸条"比喻免索引邻接。本节把纸条翻过来,看看上面到底写了什么、占几个字节、为什么这样设计。

一、存储分区:五类记录文件

Neo4j 的原生图存储把数据按类型分文件存放,每类都是定长记录的数组

neostore 结构(示意): neostore.nodestore.db 节点记录 定长(含首关系指针、标签指针) neostore.relationshipstore.db 关系记录 定长(前后指针 + 两端节点 id) neostore.propertystore.db 属性记录 条块式(内联短值 + 动态块) neostore.labelscanstore.db 标签索引 加速按标签扫描 neostore.countsstore.db 计数缓存 加速 count 估计

节点记录里最重要的字段是指向第一条关系的指针;关系记录则是整张布局的枢纽,它同时记录着:

关系记录 #8412 (类型:ACTED_IN) ├─ 起点节点 id 与 起点侧的前驱/后继关系 ├─ 终点节点 id 与 终点侧的前驱/后继关系 └─ 首个属性记录指针

每个节点在每种方向/类型组合上最多挂两条"最近使用"的关系指针,其余关系通过关系记录里的前后继指针串成双向链表

图:一次两程遍历的记录跳转

图:一次两程遍历的记录跳转

二、三个工程结论

结论一:遍历成本与图总量无关。 从节点记录出发,沿关系链跳两步就到邻居,全程不查任何全局索引。这解释了 2.4 里"索引 Seek 后每一步 Expand 都很便宜"的底层原因:Seek 只发生在锚点定位那一次。

结论二:写入有固定的记录开销。 建一条关系要写:关系记录本身、两端节点记录的指针维护、可能的属性记录。批量导入前"先建约束后导数据"依然有效,但"先建全部索引再导"在 admin import 路线上反而是负担——它不走事务引擎,索引在导入完成后统一构建更快。

结论三:内存优先喂页缓存。 定长记录让任意记录都能算出物理页位置——页缓存命中率接近 100% 时,每次"跳转"都是内存操作而非磁盘读。这就是 3.4 节"页缓存应装下整个图"的物理依据。

三、估算你的图有多大

定长布局的另一个红利是可估算。用记录开销口径算一笔账:

假设:1 亿节点、10 亿关系、属性总量 20 GB 节点:1e8 × 15 B ≈ 1.5 GB 关系:1e9 × 34 B ≈ 34 GB 属性:约 20 GB 索引与标签存储:约 5 GB 合计 ≈ 60 GB → 页缓存目标 60 GB,磁盘预留 84 GB(40% 余量)

数字是量级示意,方法论是重点:存储体积在写入前就能推算,这比"导完了再看磁盘"的被动方式好用得多。3.4 节的容量规划用的正是这套口径。

四、与关系库存储的对照

维度 Neo4j 记录式 关系库 B+ 树
关联定位 记录内指针直跳 索引查找或表扫描
热点数据 邻接天然聚在附近页 行分布在整张表
写入开销 记录 + 指针维护 行 + 全部二级索引维护
点查 需索引(锚点一次) 索引高度内命中

两类引擎没有绝对高下:图存储赢在"顺藤摸瓜",B+ 树赢在"按条件大海捞针"。Neo4j 的解法是两者兼收——锚点定位用 B+ 树索引,之后的全程用指针。2.4 节那张对比图的物理本质,就是"索引用一次,指针走全程"。

五、属性是怎么存的:条块与内联

属性存储值得单独一段,因为它的设计直接影响"节点该挂多少属性"。属性记录采用条块式布局:

属性记录结构(示意): [属性块头] [key 引用] [类型] [值] 短值(数字、短字符串)直接内联在记录内 长字符串 / 大列表 拆成动态块链,跨记录续接

推论很直接:短小的属性读写最便宜;一个节点塞几 MB 的 JSON 大字段,读取时要多跳好几块动态块,页缓存的效率也被拉低。1.2 建模惯例里"只存查得着、看得懂的属性"在这里找到了物理依据——不是风格洁癖,是省指针跳转。

六、写放大:为什么批量导入有专门工具

每条关系的写入要动多处记录:关系记录本体、两端节点的指针、可能的关系属性。这意味着图存储存在天然写放大——写入一条边,实际落盘不止一条记录。

这解释了 3.3 的一个设计:admin import 不走事务引擎,直接按最终布局写文件,绕开逐条写放大的路径,所以快出两个数量级。也解释了为什么在线写入适合"持续中小批"而不是"一次性洪峰"——洪峰时写放大叠加上锁排队,延迟曲线会非常难看。

写入形态 路径 适合
逐条在线写 事务引擎,逐记录落盘 业务事件、持续小批
UNWIND 分批 事务引擎,批内合并提交 定时同步(每分钟级)
admin import 直写存储文件 初始建库、全量迁移

七、对日常使用的三点回望

内核知识若不能回流向日常决策,就只是谈资。三点回望:

回望建模:属性短小、关系精炼的图,页缓存效率天然更高——5.1 给了 2.1 的建模纪律一个物理注脚。

回望调优:2.4 里"锚点 Seek + 指针 Expand"的计划形状,现在你知道它的每一个算子在磁盘上做什么。看到 Expand 行数爆炸,你想的不该是"加机器",而是"这一步的模式是不是太宽"。

回望容量:存储可估算、页缓存可规划、内存该给谁,3.4 与本节互相引用的口径完全一致——运维与内核讲的是同一件事的两面。

本节要点回顾

  • 存储 = 定长记录的数组:节点、关系、属性分文件存放;
  • 关系记录是枢纽:两端节点 id + 前后继指针 + 属性指针;
  • 三个推论:遍历与总量无关、写入有记录开销、页缓存决定一切;
  • 存储体积可按"记录数 × 定长开销 + 属性"预先估算;
  • 引擎分工:索引管锚点定位,指针管之后的每一步。

单机的故事讲完了。当一台机器装不下或扛不住时,下一节把多台机器组成一个逻辑整体。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U