关于数据库:一文带你了解-图数据库Nebula-的存储设计和思考

4次阅读

共计 8436 个字符,预计需要花费 22 分钟才能阅读完成。

本文首发于 Nebula Graph Community 公众号

在上次的 nebula-storage on nLive 直播中,来自 Nebula 存储团队的负责人王玉珏(四王)同大家分享了 nebula storage 这块的设计思考,也解答了一些来自社区小伙伴的发问。本文整顿自该场直播,依照问题波及的分类进行程序调整,并非齐全依照直播的工夫先后排序。

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 的存储数据格式

这里着重讲述为何 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 环节,以及直播时弹幕中提出的问题。

问题目录

  • 边的 value 存储边属性吗?
  • 强 Schema 的设计起因
  • 存一份边的设计
  • 图空间如何做物理隔离
  • Meta 如何存储 Schema
  • 存储将来布局
  • VID 遍历点和边的原理
  • 数据预校验
  • Nebula 监测
  • Nebula 的事务
  • 数据收缩问题
  • 磁盘容量自身不均怎么解决
  • Nebula 的 RocksDB“魔改”

边的 value 存储边属性吗?

和下面底层存储里讲的那样,创立 Edge 的 schema 时候会指定边类型上的属性,这些属性会作为底层 RocksDB key 的 value 存储起来,这个 value 的占位是定长的,和上面这个问题相似:

强 Schema 的设计起因

强 schema 是因为技术起因还是产品起因? 因为思考到 string 类型是变长的,每行长度自身就不固定,感觉跟无 schema 无区别。如果非定长,那查问时怎么晓得该查问到哪里呢? 是有标记位么?

其实实质上起因是用强 Schema 的益处是快,先说下常见的简略数据类型,比方:int 和 double,这样的数据类型长度是固定的,咱们会间接在 value 相应的地位进行编码。再说下 string 类型,在 Nebula 中有两种 string:一种是定长 string,长度是固定,和后面的简略数据类型一样,在 value 的固定地位进行编码。另外一种是变长的 string,通常来说大家都会比拟偏向于变长 string(灵便),非定长 string 会以指针模式存储。

举个例子,schema 中有个属性是变长 string 类型,咱们不会和简略数据类型一样间接编码保留,而是在相应地位保留一个 offset 指针,理论指向 value 中的第 100 个字节,而后在 100 这个地位才保留这个变长 string。所以读取变长 string 的时候,咱们须要在 value 中读两次,第一次获取 offset,第二次能力真正把 string 读出来。通过这样的模式,把所有类型的属性都转化成 ” 定长 ”,这样设计的益处是,依据要读取的属性和它后面所有字段的占用字节大小,能够间接计算出要读取的字段在 value 中存储的地位,并把它读出来。读取过程中,不须要读取无关的字段,防止了弱 schema 须要对整个 value 进行解码的问题。

像 Neo4j 这种图数据库,个别是 No Schema,这样写入的时候会比拟灵便,但序列化和反序列化时都会耗费一些 CPU,并且读取的时候须要从新解码。

诘问:如果有变长 string,会不会导致每行数据长度不一样

可能 value 长度会不一样,因为自身是变长嘛。

诘问:如果每行长度不一样,为什么要强 schema?
Nebula 底层存储用的 RocksDB,以 block 的模式组织,每个 block 可能是 4K 大小,读取的时候也是按 block 大小进行读取,而每个 block 中的各个 value 长度可能是不一样的。强 schema 的益处在于读单条数据的时候会快。

存一份边的设计

Nebula 存边是存储了两份,能够只存储一份边吗?存一份边反向查问是否存在问题?

其实这是一个比拟好的问题,其实在 Nebula  最晚期设计中是只存一份边的属性,这实用于局部业务场景。举个例子,你不须要任何的反向遍历,这种状况下是齐全不须要存反向边。目前来说,存反向边最大的意义是不便于咱们做反向查问。其实在 Nebula 比拟早的版本中,精确说它是只存了反向边的 key,边类型的属性值是没有存,属性值只存在正向边上。它可能带来一些问题,双向遍历或者反向查问时,整个代码逻辑包含解决流程都会比较复杂。
如果只存一份边,反向查问确实存在问题。

图空间如何做物理隔离

大家在用 Nebula 时,首先会建图空间 CREATE SPACE,在建图空间时,零碎会调配一个惟一图空间 ID 叫 spaceId,通过 DESCRIBE SPACE 能够获取 spaceId。而后 Storage 发现某台机器要保留 space 局部数据时,会先独自建一个额定的目录,再建独自 RocksDB 在这个下面起 Rocks 的 instance(实例)用来保留数据,通过这样形式进行物理隔离。这样设计的话,有个弊病:尽管 rocksdb 的 instance,或者说整个 space 目录是相互隔离,但有可能存在同一块盘上,目前资源隔离还做的不够好。

Meta 如何存储 Schema

咱们以 CREATE TAG 为例子,当咱们建 tag 时,首先会往 meta 发一个申请,让它把这个信息写进去。写入模式非常简单,先获取 tagId,再保留 tag name。底层 RocksDB 存储的 key 便是 tagId 或者是 tag name,value 是它每一个字段外面的定义,比如说,第一个字段是年龄,类型是整型 int;第二个字段是名字,类型是 string。schema 把所有字段的类型和名字全副存在 value 里,以某种序列化模式写到 RocksDB 中。

这里说下,meta 和 storage 两个 service 底层都是 RocksDB 采纳 kv 存储,只不过提供了不一样的接口,比如说,meta 提供的接口,可能就是保留某个 tag,以及 tag 上有哪些属性;或者是机器或者 space 之类的元信息,包含像用户权限、配置信息都是存在 meta 里。storage 也是 kv 存储,不过存储的数据是点边数据,提供的接口是取点、取边、取某个点所有出边之类的图操作。整体上,meta 和 storage 在 kv 存储层代码是截然不同,只不过往上裸露的对外接口是不一样的。

最初,storage 和 meta 是离开存储的,二者不是一个过程且存的目录在启动的时指定的也不一样。

诘问:meta 机器挂了,该怎么办?

是这样,通常来说 Nebula 倡议 meta 以三正本形式部署。这样的话,只挂一台机器是没有问题的。如果单正本部署 meta 挂了的话,是无奈对 schema 进行任何操作,包含不能创立 space。因为 storage 和 graph 是不强依赖 meta 的,只有在启动时会从 meta 获取信息,之后都是定期地获取 meta 存储的信息,所以如果你在整个集群跑的过程中,meta 挂了而又不做 schema 批改的话,对 graph 和 storage 是不会有任何影响的。

存储将来布局

Nebula 前面在存储层有什么布局吗?性能,可用性,稳定性方面

性能这块,Nebula 底层采纳了 RocksDB,而它的性能次要取决于应用形式,和调参的熟练程度,坦率来说,即使是 Facebook 外部员工来调参也是一门玄学。再者,方才介绍了 Nebula 的底层 key 存储,比如说 VID 或者是 EdgeType 在底层存储的绝对地位某种程度上决定了局部 Query 会有性能影响。从抛开 RocksDB 自身来说,其实还有很多性能上的事件可做:一是写点或者写边时,有些索引须要解决,这会带来额定性能开销。此外,Compaction 和理论业务 workload 也会对性能有很大影响。

稳定性上,Nebula 底层采纳 raft 协定,这是保障 Nebula Graph 不丢数据一个十分要害的点。因为只有这层稳固了,再往下面的 RocksDB 写入数据才不会呈现数据不统一或者数据失落的状况产生。此外,Nebula 自身是依照通用型数据库来设计的,会遇到一些通用型数据库独特面临的问题,比如说 DDL 扭转;而自身 Nebula 是一款分布式图数据库,也会面临分布式系统所遇到的问题,像网络隔离、网络中断、各种超时或者因为某些起因节点挂了。下面这些问题的话,都须要有应答机制,比方 Nebula 目前反对动静扩缩容,整个流程非常复杂,须要在 meta 上、以及挂掉的节点、残余“活着”的节点进行数据迁徙工作。在这个过程中,两头任何一步失败都要做 Failover 解决。

可用性方面,咱们后续会引入主备架构。在有些场景下所波及的数据量会比拟少,不太须要存三正本,单机存储即可。这种全副数据就在单机上的状况,能够减去不必要的 RPC 调用,间接换成本地调用,性能可能会有很大的晋升。因为,Nebula 部署一共起 3 个服务:meta、graph 和 storage,如果是单机部署的话,graph + storage 能够放在同一台机器上,原先 graph 须要通过 RPC 调用 storage 接口去获取数据再回到 graph 进行运算。如果你的查问语句是多跳查问,从 graph 发送申请到 storage 这样的调用链路重复执行屡次,这会导致网络开销、序列化和反序列化的这些损耗进步。当 2 个过程(storaged 和 graphd)合在一起,便没有了 RPC 调用,所以性能会有个大晋升。此外,这种单机状况下 CPU 利用率会很高,这也是目前 Nebula 存储团队在做的事件,会在下一个大版本同大家见面。

VID 遍历点和边的原理

能够根据 VID 遍历点和边?

从上图你能够看到存储了个 Type 类型,在 v1.x 版本中无论点和边 Type 类型都是一样的,所以就会产生下面说到过的扫描点会夹杂多个边的问题。在 v2.x 开始,将点和边的 Type 进行辨别,前缀 Type 值就不一样了,给定一个 VID,无论是查所有 tag 还是所有边,都只须要一次前缀查问,且不会扫描额定数据。

数据预校验

Nebula 是强 Schema 的,插入数据时如何去判断这个字段是否合乎定义?

是否合乎定义的话,大略是这样,创立 Schema 时会要求指定某个字段是 nullable 或者是有默认值,或者既不是 nullable 也不带默认值。当咱们插入一条数据的时候,插入语句会要求你“写明”各个字段的值别离是什么。而这条插入 Query 发到存储层后,存储层会查看是不是所有字段值都有设置,或者写入值的字段是否有默认值或者是 nullable。而后程序会去查是不是所有的字段都能够填上值。如果不是的话,零碎会报错,告知用户 Query 有问题无奈写入。如果没有报错,storage 就会对 value 进行编码,而后通过 raft 最初写到 RocksDB 里,整个流程大略是这样的。

Nebula 监测

Nebula 能够针对 space 来进行统计吗?因为我记得如同针对机器。

这个是十分好的问题,目前答案是不能。这块咱们在布局,这个问题的次要起因是 metrics 较少,目前咱们反对的 metrics 只有 latency、qps 还有报错的 qps 这三类。每个指标有对应的平均值、最大值、最小值,sum 和 count,以及 p99 之类参数。目前是机器级别的 metrics,后续的话会做两个优化:一个增多 metrics;二是按 space 级别进行统计,对于每个空间来说,咱们会提供诸如 fetch、go、lookup 之类语句的 qps。下面是 graph 这边的 metrics,而 storage 这块因为没有强资源隔离能力,还是提供集群或者单个机器级别的 metrics 而不是 space 级别的。

Nebula 的事务

nebula 2.6.0 的边事务是怎么实现的呢?

先说下边事务的背景,背景是下面提到的 Nebula 是存了两份边 2 个 kv,这 2 个 kv 可能会存在不同的节点上,这会导致如果有台机器挂了,其中有一条边可能是没有胜利写入。所谓边事务或者叫 TOSS,它次要解决的问题就是当咱们遇到其中有一台机器宕机时,存储层可能保障这两个边(出边和入边)的最终统一。这个一致性级别是最终统一,没有抉择强统一是因为研发过程中碰到一些报错信息以及数据处理流程上的问题,最初抉择了最终一致性。

再来说下 TOSS 解决的整体流程,先往第一个要写入数据的机器发正向边信息,在机器上写个标记,看标记有没有写胜利,如果胜利了进入到下一步,如果失败间接报错。第二步的话,把反向边信息从第一台机器发给第二台机器,能让存正向边的机器向第二台机器发送反向边信息的起因是,Nebula 中正反向边只有终点和起点调换了一个地位,所以存正向边的机器是齐全能够拼出反向边。存反向边的机器收到之后,会间接写入边,并将它的写入后果胜利与否通知第一台机器。第一台机器收到这个写入后果之后,假如它是胜利的,它就会把之前第一步写的标记删掉,同时换成失常的边,这时整个边的失常写入流程就实现了,这是一个链式的同步机制。

简略说下失败的流程,一开始第一台机器写失败了间接就报错;第一台机器胜利之后,第二台机器写失败了,这种状况下机器一会有背景线程,会始终一直尝试修复第二台机器的边,保障和第一台机器一样。当中比较复杂的是,第一台机器会依据第二台机器返回的错误码进行解决。目前来说,所有的流程都会间接把标记删掉,间接换成失常的正向边,同时写些更额定的标记来示意当初须要复原的失败边,让它们最终保持一致。

诘问:点没有事务吗?

是这样,因为点是只存了一份,所以它是不须要事务的。一般来说,问这个问题的人是想强调点和边之间的事务,像插入边时看点是否存在,或者删除点时删除对应边。目前 Nebula 的悬挂点的设计是出于性能上的思考。如果要解决下面的问题的话,会引入残缺的事务,但这样性能会有个数量级的递加。顺便提下,刚说到 TOSS 是链式模式同步信息,下面也提到能这样做的起因是因为第一个节点能残缺拼出第二个节点的数据。但链式的话对残缺的事务而言,性能降落会更重大,所以将来事务这块的设计不会驳回这种形式。

数据收缩问题

首次导入数据是怎么存储的,因为我发现首次导入数据磁盘占用会较多?

大家发现如果磁盘占用高,一般来说是 WAL 文件比拟多。因为咱们导入的数据量个别比拟大,这会产生大量的 wal,在 Nebula 中默认的 wal ttl 是 4 个小时,在这 4 个小时中零碎的 WAL 日志是齐全不会删除的,这就导致占用的磁盘空间会十分大。此外,RocksDB 中也会写入一份数据,相比后续集群失常运行一段时间,这时候磁盘占用会很高。对应的解决办法也比较简单,导入数据时调小 wal ttl 工夫,比方只存半小时或者一个小时,这样磁盘占用率就会缩小。当然磁盘空间够大你不做任何解决应用默认 4 小时也 ok。因为过了若干个小时后,有一个背景线程会一直去查看哪些 wal 能够删掉了,比如说默认值 4 个小时之后,一旦发现时过期的 wal 零碎便会删掉。

除了首次导入会有个峰值之外,线上业务实时写入数据量并不会很大,wal 文件也绝对小。这里不倡议手动删 wal 文件,因为可能会出问题失常依照 ttl 来主动删除就行。

compact 都做了什么事能够进步查问,也减小了数据存储占用?
能够看下 RocksDB 介绍和文章,简略说下 Compaction 次要是多路归并排序。RocksDB 是 LSM-Tree 树结构,写入是 append-only 只会追加地写,这会导致数据存在肯定的冗余。Compaction 就是用来升高这种冗余,以 sst 作为输出,进行归并排序和去除冗余数据,最初再输入一些 sst。在这个输入输出过程中,Compaction 会查看同一个 key 是否呈现在 LSM 中的不同层,如果同一个 key 呈现了屡次会只保留最新的 key,老 key 删掉,这样进步了 sst 有序的水平,同时 sst 数量和 LSM-Tree 的层数可能会减小,这样查问时候须要读取的 sst 数量就会缩小,进步查问效率。

磁盘容量自身不均怎么解决

不同大小的磁盘是否思考按百分比占用,因为我应用两块不同大小的磁盘,一块占满之后导数就呈现问题了

目前是不太好做,次要起因是存储 partition 散布查找是依照轮循模式进行的,另外一个起因是 Nebula 进行 Hash 分片,各个数据盘数据存储大小趋近。这会导致如果两个数据盘大小不统一,一个盘先满了前面的数据就写入不进去。解决办法能够从零碎层进行解决,间接把两块盘绑成同一块盘,以同样一个门路挂载。

Nebula 的 RocksDB“魔改”

Nebula 的 RocksDB 存储中,是通过列 column family 来区别 vertex 属性吗?

目前来说,其实咱们齐全没有用 column family,只用了 default column family。后续可能会用,然而不会用来辨别 vertex 属性,而是把不同 partition 数据分到不同 column family,这样的益处是间接物理隔离。

Nebula 的魔改 wal 如同是全局 multi-raft 的 wal,然而在目录上体现进去的如同每个图空间都是独自的 wal,这个原理是啥?
首先,Nebula 确实是 multi-raft,但没有全局 wal 的概念。Nebula 的 wal 是针对 partition 级别的,每个 partition 有本人的 wal,并不存在 space 的 wal。至于为啥这么设计,相对来说当初实现形式比拟容易,尽管会存在性能损耗,像多个 wal 的话磁盘写入就是个随机写入。然而对 raft 而言,写入瓶颈并不是在这而是零碎的网络开销,用户的复制操作 replication 开销是最大的。


Nebula 社区首届征文活动进行中!🔗 奖品丰富,全场景笼罩:撸码机械键盘⌨️、手机无线充🔋、衰弱小助手智能手环⌚️,更有数据库设计、常识图谱实际书籍📚 等你来领,还有 Nebula 粗劣周边送不停~🎁

欢送对 Nebula 有趣味、喜钻研的小伙伴来书写本人和 Nebula 乏味的故事呀~

交换图数据库技术?退出 Nebula 交换群请先填写下你的 Nebula 名片,Nebula 小助手会拉你进群~~

关注公众号

正文完
 0