一文带你了解 「图数据库」Nebula 的存储设计和思考

本文首发于 Nebula Graph Community 公众号
在上次的 nebula-storage on nLive 直播中 , 来自 Nebula 存储团队的负责人王玉珏(四王)同大家分享了 nebula storage 这块的设计思考 , 也解答了一些来自社区小伙伴的提问 。本文整理自该场直播 , 按照问题涉及的分类进行顺序调整 , 并非完全按照直播的时间先后排序 。
Nebula 的存储架构
一文带你了解 「图数据库」Nebula 的存储设计和思考

文章插图
 
整个 Storage 主要分三层 , 最下面是 Store Engine , 也 就是 RocksDB , 中间是 raft 一致性协议层 , 最上层 storage service 提供对外的 rpc 接口 , 比如取点属性 , 或者边属性 , 或者是去从某个点去找它的邻居之类的接口 。当我们通过语句CREATE SPACE IF NOT EXISTS my_space_2 (partition_num=15, replica_factor=1, vid_type=FIXED_STRING(30)); 创建 space 时 , 根据填写的参数将 space 划分为多个逻辑单元成为 partition , 各个 partition 会落到不同机器上 , 同一个 Partition 的多个副本会组成一个逻辑单元 , 并通过 raft 共识算法 raft 保证一致 。
Nebula 的存储数据格式
一文带你了解 「图数据库」Nebula 的存储设计和思考

文章插图
 

一文带你了解 「图数据库」Nebula 的存储设计和思考

文章插图
 
这里着重讲述为何 v2.x 会有这些数据格式的改动:在 v1.x 版本中 , Nebula VID 主要是 int 类型 , 所以大家可以看到上图 v1.x 中不管是点还是边 , 它的 VID 是定长的、占 8 个字节 。2.x 版本开始 , 为了支持 string 类型 VID , VertexID 就变成不定长的 n 个字节 。所以大家创建 Space 的时候需要指定 VID 的长度 , 这个是最主要的改动 , 其他的话还有一些小的改动 , 去掉了时间戳 。整体来说 , 目前的存储格式更贴近图的使用场景——从某个点开始找它的邻居 , 以 v2.x 这样 VertexID + EdgeType 存储格式来保存边的话 , 可以迅速地找到某个点出边 。
同时 , v2.x 也做了 key(Nebula 底层是 KV 存储的)编码格式上的改变 , 简单来说就是把点和边分开 。这样的话 , 取某一个点所有 tag 时通过一次 prefix 就可以直接扫到 , 避免了像 v1.x 那样扫描点的过程中夹杂多个边的问题 。
底层的数据存储针对用户提出的“Nebula 底层如何存储数据”的问题 , 四王了进行了回复:Nebula 的存储层使用 KV 进行存储点边数据 。对于一个点而言 , key 里面存储 VID 和它的 tag 类型 。点的 value 中 , 会根据 这个 tag 的 schema , 将 schema 中的各个属性进行编码并存在 value 中 。比如 , player 这个 tag 可能会有一个 age 这样一个整型年龄字段 , 使用存储的时候会把 age 字段的值 , 按某种编码保存在 value 中 。再来说下边 , 边的存储 key 会多几个字段 , 主要是边的起点 ID、边类型、ranking 及终点类型 , 通过这四元组确定唯一的边 。边的 value 和点的 value 类似 , 根据边的 Schema 字段定义 , 将各个字段进行编码存储 。这里要说一下 , Nebula 中存储边是存储两份:Nebula 中的边是有向边 , 存储层会存储正向边和反向边 , 这样的好处在于使用 GO FROM 进行遍历查找那些点指向点 A 或者点 A 指向哪些点可以快速通过双向查找实现 。
一般来说 , 图存储分为切边和切点两种方式 , 像上面说的 Nebula 其实采用了切边方式:一条边存储两份 KV 。
用户提问:为什么采用切边方式 , 切点和切边各自有啥利弊?
切边的话 , 每一份边存两份 , 数据总量会比切点大很多 , 因为图数据边的数量是远大于点的数量 , 造成边的大量冗余 , 相对好处是对起点和它的边进行映射时会映射到同一个 partition 上 , 这样进行一些从单个点触发的 query 时会很快速得到结果 。切点的话 , 由于点可能被分在多个机器上 , 更新数据时得考虑数据的一致性问题 , 一般在图计算里面切点的使用会更广泛 。
你问我答下面内容收集于之前活动预告的 AMA 环节 , 以及直播时弹幕中提出的问题 。


推荐阅读