本文整理自青藤云安全工程师——文洲在青藤云技术团队内部分享
图数据库的性能和schema的设计息息相关,但是NebulaGraph官方本身对图schema的设计其实没有一个定论,唯一的共识就是面向性能去做schema设计。
而Neo4j在它的书籍上则阐述希望用户能够尊重本身业务领域实体的关系进行设计,这次的分享主要是为了解答下面这些问题:
什么时候用图数据库,什么时候用图计算
什么时候建实体,什么时候建关系
什么时候建实体,什么时候添加属性
什么时候属性加索引
什么时候属性加到图
图数据库最佳实践
希望能从原理上能够解释一下,如果当中有任何不妥当的地方欢迎一起交流。
背景知识
先来讲解下存储背景,再讲schema设计中会遇到的问题,最后讲下实践过程中我们能达成一致的最佳实践。
在使用图数据库之前,先了解下图数据库这个NoSQL数据库同关系型数据库不一样的地方。
关系型数据库存储结构以上图为例,存一个ID作为一个主键,然后它有个特征k,我们对k创建索引进行查询,对于左下角这份列表数据,内存中存储的话,会以一个B+树进行存储(上图右侧):一个主索引ID和一个从索引k。举个例子,现在我们要查询k=3的数据,它就先查询ID=然后经过回表后回到具体的值。
这体现了关系型数据库的一个特点,如果你要查询速度快,那就需要创建一个索引。假如你不创建索引,那数据库就会扫全表。
我们再来看下写过程。数据一般先写到内存Mem(这是一个常规的优化减小磁盘压力),写到一定程度再同步到磁盘中,这个过程我们叫原位刷盘,刷盘就是说找到这个地方的数据,然后修改掉数据,即原位修改。
如果你之前熟悉MySQL或者是其他关系型数据库,这套原理应该是比较熟悉的。
而相对应的,用传统的数据库来实现图功能的话,代价比较大,下图便展示了它的实现弊端:
现在有个场景,现在我们有某个人(上图Person表),我们要找朋友的朋友(上图的PersonFriend表),在关系型数据库中便是两级索引,先查Person表索引找到PersonID,再查PersonFriend表通过ID找到对应的人,就是一个JOIN查询过程。如果这里使用的是B+树,那么程序复杂度便是O(logn);如果是这里的多级大小表,在笛卡尔积上即O(n*m),都加索引有一定程度优化,但查询这种多级关系的话,到了一定程度会遇到系统“爆炸”,无法进行相关查询。
LSM存储模型本文主题是图的高性能设计,主要基于NebulaGraph来讲解。这里部分存储细节同Neo4j会略有不同。
NebulaGraph存储模型采用了LSM存储模型,同上面我们讲的原位修改不同,LSM模型是先写内存,写到一定程度之后再写入到对应磁盘中,每次都是增量顺序写。LSM模型是一个多级模型,第一层是L0,第二层是L1,一般默认是7层。
这里引用网图来讲解下LSM层级结构:
上图的L0层其实有重复数据,像上图的1-68的key在L0层的2-37,以及23-48,其实这两步数据是存在重叠的;但L1层的数据就不存在重叠情况了,1-12、15-25…要最大地发挥图性能的话,先得了解它的写入过程。LSM模型的写是顺序写,即不会进行上文说到的原位修改。不管是写入新数据还是更新原来数据,永远是在后面插入新的数据(参考上图右侧深蓝色数据)。这样设计的好处在于,写入数据就不需要找之前的数据,一旦涉及查找数据就会慢,这样设计提升了写速度。
但这也会带来一个问题:我们写入重复的数据,或是写入的数据越来越多,查询会不会受影响呢?我们来看看LSM是如何查数据的。LSM进行数据查询时,先查内存,内存里没有数据再查不可变区域(ImmutableMemtable),没有的话,再往下一级级地查(参考上图左侧部分)。所以,重复的数据越多,或者磁盘数据越多,便会越慢。
所以为了保证写入和查询性能,无论我们设计属性还是其他schema,都要控制写入量,也就是LSM的写入不能是无限制追加,它有一个定时的合并操作,定期地将重复数据进行合并,叫做Compaction。
Compaction过程也需要控制。合并数据能减小数据量,但同时Compaction会带来磁盘压力,磁盘压力过大,读操作速度也会变慢。综合来看,Compaction是一个写入平衡的过程。
NebulaGraph存储结构和索引下面再来了解下NebulaGraph本身的存储结构和索引。
NebulaGraph本身是分布式数据库,因为便于理解这里剔除了相关的分布式结构。简单来了解下NebulaGraph的结构,上面提到过的LSM其实是KV(keyvalue)存储,所以我们图里存储点、边、索引在磁盘上都是KV结构。我们可以看到上图左侧(紫色部分)有个vid带着出边(out)和入边(in)以及相关属性。再看下上图右侧部分(紫色部分),可以看到一条边的两个点是存储在一起的,对应的点属性序列化保存。相当于说,KV结构中的key便是我们的点的vid,然后value便是属性的序列化结构。因为是序列化的结构,所以你的属性名是什么便会存成什么,比如这里原始数据name字段,它改命名为family_name,实际存储就是序列化后的family_name,也就是属性名越长,存储量越大。除了属性名之外,其实属性值也会导致存储量增大。举个例子,现在有个人(点),他的生平介绍要不要放在属性里进行存储?答案是:不应该。因为你的生平介绍会很长,这就会导致LSM的存储压力会很大。无论是Compaction还是读写,都会有很大的压力。类似比如存储进程实体,对应的进程描述文本也较大,会带来较大存储压力。
再来说下我们的边,NebulaGraph中出边和入边保存在一个KV结构中(参考上图右侧橙色部分)。NebulaGraph中有个词叫做前缀扫描,具体来说便是现在要查找某个vid对应的边,它是如何查找的呢?先按照vid来前缀扫描,在内存中这个过程是个二分查找,所以NebulaGraph查询快就是在这里。在Neo4j里面这种叫做“免索引邻接”。像上面的朋友的朋友的场景,传统数据库是通过索引进行查找的,而在这里直接扫描找寻某个人便可。在物理存储这块,点(人和相关的人)都是存储在一起的,找到了某个人便找到了他的朋友。查询上速度非常快,这也是原生图数据库带来的好处。
除了上面的存储结构,索引也是高性能schema设计的一个作用因素。像上图的右侧部分,上面的紫色部分存储着点,这里有2个点:第一个点是vid1,name是wen,age是20;另外一个是vid2,name是wei,age是20。这里我们创建了2个索引,一个是针对name,一个是针对age。这两个索引的存储结构参考上图右侧下方的白色部分,查找name为wen的数据时,按照上面我们科普过的会进行二分查找,扫描到对应的name索引的wen数据,然后再从索引数据中找到对应的点(vid1)数据,再借助vid数据来找寻它的相关信息。这里vid找关联数据的原理同上面的存储结构描述。
小结小结下NebulaGraph存储结构和索引,在这里关系是一等公民,索引辅助查询(并非用来提速),重要的是抽象关系。
schema设计
进入本文的重点——schema的设计,schema设计的三大基本原则:
尊重领域实体关系
以性能为目标
考虑可视化分析
而三者并不冲突,上面三点其中某一点做得很好,另外两点也会做的不错。
Talkingischeap,下面我们来结合具体的例子来了解下三大原则。这些case图主要引用自Neo4j,但是对于NebulaGraph相关的schema设计也有参考意义。
实体和关系的选择上图是Neo4j图数据库书籍中的示例图。简单描述下这个场景,Bob和Charlie等人在发邮件。那你设计这么一个场景的schema是否很自然就会将发邮件变成关系边?因为Bob同Charlie发邮件,不是很明显就是发邮件关系吗?那我们来回顾下上面说的三大原则第一点:尊重领域实体关系。Bob和Charlie建立联系自然不是通过发邮件这个行为,而是通过邮件本身来建立联系,所以这里便缺少了一个实体。在考虑可视化分析原则这边,你要分析实体之间的关系,你思考它们是通过什么来建立的联系。这时候就会发生之前提到过的发邮件设置为边的情况(把邮件放置在边上),单看Bob的话(左图),我们可以清楚地看到发邮件这个动作。左图上面部分,BobEmailedCharlie。但如果这时候,要查看这个邮件抄送给了谁,还有这封邮件有哪些相关人,像左图的schema就不能很好地进行查询。因为缺少了Email这个实体。而上图右侧部分便能可以方便地找寻相关信息。
下面再来讲下如何进行实体和属性选择。实体和属性选择在这个部分,我将结合青藤云的情况来讲一个我们的case——进程之间的父子关系。
如上图左侧所示,md5为1的pid进程起了一个pid的子进程,这个子进程的md5是2。同时,md5也为1的pid也起了进程,pid为、md5为3。按照我们之前的实现方法,是在md5上创建索引,继而建立起跟pid、pid的联系。但这种做法,上面讲过性能并不高,免索引复杂是O(1),而这种做法的复杂度是O(logn)。所以说,我们这时候应该基于ProcessFile进程文件md5来建立关系(进程间是基于md5联系起来的):我们先抽取md5建立一个名叫ProcessFile的实体,属性是md5。如果我们要查询指定进程所关联的进程,很直观地去找寻和这个ProcessFile关联的进程就可以分析出来我们要的结果。举个例子,pid的进程是一个木马,我想找寻是哪个父进程释放的它,或者是同它父进程同md5文件的进程,该怎么找?
上图的展示了两种形式,第一种(左侧)的话就需要找索引;第二种(右侧)通过CREATE_PROCESS就可以直接找到pid的父进程pid,再通过PFILE_OF关系你可以找到它同md文件的进程pid。
好的,简单结合schema设计三大原则来回顾下这个case:
属性上创建索引会影响写入,此外属性放在ProcessFile还是放在Process中,存储性能是不一样的。这里主要涉及到写入量,因为Process进程是一直可以不停地启动,但是md5文件可能本身并不多。如果是放在Process中,进程起得越多,数据写入量也就会越大,进而查询压力也会增大查询变慢。
可视化探索这块主要和不定需求有关。因为一开始我们设计schema的时候可能并没有全方位考量,或者说像是一些安全、防作弊规则并未拟定,不知道它会是什么样。而这时你要根据这种不确定来设计schema,就需要将图“释放”给相关业务人员,让他在图里点击,设计他的关系,所以相对应的我们就不能通过索引来实现这种需求,因为业务人员可能没有相关的技术背景。
添加属性上图左边描述文字截自NebulaGraphv2.0的官方文档: