乐趣区

关于图数据库:使用秘籍|如何实现图数据库-NebulaGraph-的高效建模快速导入性能优化

本文整顿自 NebulaGraph PD 方扬在「NebulaGraph x KubeBlocks」meetup 上的演讲,次要包含以下内容:

  • NebulaGraph 3.x 倒退历程
  • NebulaGraph 最佳实际

    • 建模篇
    • 导入篇
    • 查问篇

NebulaGraph 3.x 的倒退历程

NebulaGraph 自 2019 年 5 月开源公布第一个 alpha 版本以来,陆陆续续公布了 2.0 GA,到当初 v3.6.0,曾经是 v3.x 版本中比拟前期的版本了。从 3.x 开始,基本上放弃了三个月(一个季度)发一个 y 版本的节奏,从 v3.1 到 v3.2,到当初的 v3.6。(演讲时 v3.6 尚未公布,所以没有相干内容展现)

而这几个版本次要是在齐备性、性能,稳定性以及易用性上的优化。从 NebulaGraph 作为一款图数据库的产品定位上来说,外围利用场景是 TP 场景,而对 TP 型数据库产品来说,有几个个性是十分重要的:

  • 稳固:数据库作为底层的基础设施,许多业务是基于数据库运行的,是在线的零碎,因而稳定性是一个十分重要的个性。在 NebulaGraph v3.4 版本,包含最新版本的 v3.6 版本中,零碎的稳定性失去了十分大的晋升和改善;
  • 性能:因为 NebulaGraph 定位是一个 TP 的数据库产品,因而性能,包含高吞吐量是至关重要的;
  • 易用:一个好的产品要如何让用户更好地用起来,升高用户的学习老本,也是须要思考到的;

针对这些个性,咱们在 v3.x 这 5 个版本中做了这些尝试:

稳定性

在 v3.x 开始,NebulaGraph 引入 fuzzer,极大晋升测试效率。fuzzer 可基于 nGQL(NebulaGraph 的查询语言)的语法进行灵便组合,生成人为不能拟定的查问语句,由此让测试更加欠缺,从而进步了稳定性。

此外,版本新个性局部还新增 Memory Tracker。图数据库不同于其余数据库,数据始终处于继续地遍历、迭代中,因为即使是数据量不大的状况下,数据的迭代会导致它的后果异样大,这就造成了内存的治理压力。在 v3.4 版本中,NebulaGraph 引入了 Memory Tracker 机制,从论坛的用户反馈上,能够看得出来相干的 OOM 问题大幅度缩小了。这里能够浏览下《内存治理实际之 Memory Tracker》;

性能晋升

图的经典查问个别包含 K 跳(K-hop)、子图和门路查问。K 跳就是从一个点登程,比如说从我登程,去找寻我好友(一跳)的好友(两跳),这种查问,可能社交或者反欺诈的场景中应用会比拟多。此外,就是子图,比如说我当初从一个点登程,找到他的四周的关联的一群人,以及这一群人关联的另外一群人,这时候就可能会用到子图的性能。还有就是门路查问,像是企业和企业之间的关联关系之类的,就比拟适宜用门路,来找寻二者的关联。在 v3.x 中,这几个图的经典查问性能都有大幅度的晋升的,具体大家能够看论坛的性能报告:自测和他测报告合集;

下面提到过内存治理的难点,除了 Memory Tracker 机制之外,属性裁剪能晋升内存的利用率,在 NebulaGraph 中如果查问不须要用到某个属性,就会将其裁剪掉,从而晋升内存利用率。

易用性

NebulaGraph 在 v2.x 开始反对 openCypher,一开始是比拟根底的 openCypher 语法;在 v3.x 开始,NebulaGrpah 做了一个语法欠缺,像是 OPTIONAL MATCH、多 MATCH 等语法反对,全面笼罩了国内图基准测试之一的 LDBC-SNB 反对的图查问。

此外,在 v3.5 开始反对了 UDF 性能,这个性能是由社区用户 zhaojunnan 提供反对的,它能够用来帮忙实现一些内核临时不反对的性能。这里就不具体开展 UDF 的阐明了,具体大家能够看《NebulaGraph UDF 性能的设计与背地的思考》;

最初一点是全文索引优化,这个在前面章节会具体讲述。

NebulaGraph 的最佳实际

在这个局部次要分为:建模、数据导入、查问等三大内容。

数据建模

数据收缩

这是社区用户在交换群里反馈的一个问题:

  1. 数据导入之后占用硬盘空间极大,60 MB 的文件导入之后,Storage 占用了 3.5 G;
  2. Storage 服务占用内存很高
  3. 并发量大的时候,会呈现 ConnectionPool 不够的状况;
  4. 3 跳查问原本就很慢么?我看官网的 Benchmark 数据规模大的时候会慢,然而以后咱们数据量也不是很大;

目前的数据状况:点 594,952,边 798,826。同时,边和点均建了 1 个索引:

所以它有什么问题呢?

在图数据库 NebulaGraph 中,有个概念叫做 Space,Space 里有一个概念是 VID type,VID 是 NebulaGraph 中十分要害、重要的概念,所有的数据字段都是通过 VID 来进行惟一索引,相似主键的概念。比方上图的两头局部:点构造和边构造,能够看到构造中都有 VID,用来进行字段查问;边构造还分终点 srcId 和起点 dstId,就是上图的两处 VertexID。

如上图所示,这个 Space 中配置的 VID 类型是 String,而 NebulaGraph 反对的 VID 类型有两种:一种是 INT,数值类型,像手机号之类的能够用 INT 来存储;一种是 String,比如说人名之类的,当然你要用 String 来存储像是身份证号之类的数值信息也能够。然而,用 VID 的查问效率从教训看是 INT 类型是远高于用 String 作为 VID 类型的。

回到下面的这个例子,一开始用户创立 VID 时,间接选取了 FIXED_STRING 类型,设定为了 256 位的定长 String。然而这里会导致一个问题:

  • 594,952(点数)*256(VID 大小) (1 + 1) + 798,826(边数) 256(VID 大小)* (2 + 2 + 2 + 2) = 1.80 GB

下面的例子是数据存储的大小计算过程,点的数量乘以定长的长度(这里是 256),再乘以占据的字节大小,以及边的数量乘以对应 VID 的长度,再乘以对应边 VID 占据的空间大小,算进去是 1.8 GB。由此,咱们能够想到一个事件:是不是能够精简下 VID 的定长长度,设置一个正当的数值,比如说是 32,那它空间占据量就是:

  • 594,952(点数)*32(VID 大小) (1 + 1) + 798,826(边数) 32(VID 大小)* (2 + 2 + 2 + 2) = 0.23 GB

批改 VID 的定长长度之后,整个空间使用量就是之前的 1/8,还是十分可观的一个磁盘容量优化。如果是更多的点和边数据量的话,缩减的磁盘空间会更主观。由此,咱们有个倡议:VID 的定长长度尽可能短,同理,属性类型设置亦如是

超级节点

图数据库实际中,超级节点是一个比拟常遇到的性能问题。那么,什么是超级节点(浓密点)呢?图论给出的解释是:一个点有着超级多的相邻边,相邻边就是出边(从这个点指向另外一个点)或者是入边(某个点指向这个点)。像是社交网络中 KOL、网红大 V 之类的人,或是证券市场的热门股票,交通网络中的枢纽站、银行零碎中的四大行、互联网中的高流量站点,或者是电商平台的爆款商品,等等都是整个关系网络中的超级节点。一旦查问中有超级节点,查问速率就会变得异样的迟缓,甚至有时候内存都耗尽了,查问后果还没跑进去。

上面就来讲讲,现阶段你要用 NebulaGraph 能够如何解决或是绕开超级节点:

要在建模环节躲避掉超级节点的问题,“拆点”是可行的形式之一。如上图左侧所示,在未优化建模之前,A 通过 transfer 边关系连贯到 B1、B2,如果 A 频繁的转账,势必会导致它成为一个超级节点。这时候,你能够将 A 拆分成 A1 和 A2,依照某种约定的形式,比如说转账的日期,或者是由繁多客户拆分成对公客户、对私客户,从而达到拆点、避开超级节点造成的目标。不过,这里会波及到一个 VID 变更的问题,将 A 拆分成 A1 和 A2,会导致对应的 VID 发生变化,当然你能够命名 A1 为 A0721,A2 为 A0722,加上日期数字来标识它们。

绝对应拆点,还有拆 / 合边的形式。在两个点之间,有许多同一类型的边,比如说转账关系,这时候,能够依据业务的逻辑来进行判断,比方取最短边、最新边、最大边、最小边等,在一些不须要明细的场景里,只体现关系进去,这样就能晋升查问效率。除了合并之外,拆边也是一种形式,如上图右侧所示,两个点之前有十分多的关系,它们都是交易类型,可能有一部分是发红包,有一部分是转账,这时候,你就能够依照拆点的逻辑,将边进行拆解。

此外,还有截断,NebulaGraph 有个配置参数是 max_edge_returned_per_vertex,用来应答多邻边的超级节点问题。比方我当初 max_edge_returned_per_vertex 设置成 1,000,那零碎从点 A 登程,遍历 1,000 个点之后就不再遍历了,便将后果返回给零碎。这里会存在一个问题,退出 A 和 B1 之间存在 1 千多条边,A 和 B2 存在 3 条边,依照这种遍历 1,000 条边之后就不再遍历的设定,可能返回后果中 A 和 B2 的关系边就不会返回了,因为这个遍历返回是随机的。

其余的话,同相干的社区用户交换,我发现在许多业务场景中,超级节点并没有太大的理论业务价值。这里就要提下“超级节点的检测”,比方:通过度核心性算法(DegreeCentrality)计算出出入度大小,这个图算法 nebula-algorithm 和 nebula-analytics 都反对。当这个算法跑完之后,失去的二维表就能通知你哪些是超级节点,提前让用户晓得哪些点会影响查问效率。

此外,如果当初你有一个已知的超级节点,且不不便解决,那查问的时候就要尽量避免逆向查问,即从这个超级节点登程,查问其余节点。

数据导入

社区用户常常遇到的还有一类问题:数据导入慢的问题。个别新的社区用户都会问:你们的导入性能如何?这时候咱们个别会说:导入性能老牛逼了,而且咱们是间接用 INSERT 形式导入的,速度贼快,之前遇到最快的是 600MB/s。

这时候用户个别会反诘:为什么我测试进去,导入速度没有官网说的那么快。

这里就开展说说如何晋升你的数据导入性能。

相熟 NebulaGraph 的小伙伴都晓得,它的底层存储是基于 RocksDB 实现的,而 RocksDB 有 wal_ttl 这么一个配置项,如果你的导入数据量十分大,对应的 wal 日志也会绝对应的变大。因而,倡议在进行数据导入时,将 wal_ttl 工夫设短一点,以避免收缩的 wal 日志适度地占用磁盘,能够失去及时的清理。此外,就是 Compaction 相干的配置项,次要是 max_subcompactionsmax_background_jobs 这两个参数项,个别倡议将其设置为 CPU 核数的一半。而这个一半的参数倡议,次要来源于用户的反馈以及一些教训数据,不同的场景还是须要不同的配置,HDD 和 SSD 的配置也有所不同,大家能够前面看着状况进行调试。

除了配置参数之外,在做数据导入之前,倡议大家执行下 SHOW HOSTS 操作,查看 leader 是否散布平均:NebulaGraph 会将数据分为若干个 partition,每个 partition 会随机散布在节点上,现实状态天然是 partition 的 leader 是平均地散布在各个节点的。如果 leader 散布不均的话,能够执行 BALANCE LEADER 操作,确保其均匀分布。

在工具配置方面,可能就是数据导入的重头戏了,配置你的数据导入工具参数:

  • 配置项 concurrency,示意导入工具连贯多少个 graphd(查问)节点,个别设置为导入工具 nebula-importer 所在机器的 CPU 核数;
  • manager.batch,尽管 NebulaGraph 反对你通过 INSERT 来一个个点插入到数据库中,然而这个有些低效。因而,设立了 batch 字段用来将一批数据导入到数据库中,默认参数设置是 128,不过这里要依据你本身的数据个性来进行优化。如果你的属性值很多,那么倡议将 batch 调小;反之,将 batch 值调大即可。整个 batch 的大小,倡议小于 4MB;
  • manager.readerConcurrency 是数据读取的并发数,即,从数据源读取数据的并发数。默认参数是 50,个别倡议设置为 30-50 即可;
  • manager.importerConcurrency,数据读取之后,会依据肯定的规定拼接成 batch 块,这里就波及到这个参数项。manager.importerConcurrency 指的是生成待执行的 nGQL 语句的协程数,一般来说它会设置成 manager.readerConcurrency 字段的 10 倍,默认值是 512;

软件说完了,来说下硬件方面的配置。NebulaGrpah 优先举荐应用 SSD,当然 HDD 也是能够的,不过性能绝对会差点。此外,在 data_path 下多配置几块盘,每个门路配置一个盘,这个也是之前的实际经验总结进去的。而机器和机器之间,举荐应用万兆网卡。最初一点是,nebula-importer 之类的导入工具有条件的话尽量独自部署,和集群隔离开,不然的话在一台机器人会存在资源抢占的问题。

软硬件都说完了,剩下就是数据自身的问题。图数据库的定位是关系剖析,同此无关的事件,例如:全文搜寻(ES 善于的场景),要看状况是否将该局部数据放入到 NebulaGraph 中。因为 NebulaGraph 进行数据导入时,不存在导入的先后顺序,即点和边一起混合导入,这样设计的益处是,数据无需做预处理,害处是数据导入之后可能会产生悬挂边,不利于后续的查问。最初要注意终点,或起点为空的数据,或者是异样数据,这些数据在异样解决时很容易一不小心造成超级节点。

查问指南

上面来讲讲如何搞定 NebulaGraph 的查问篇。这里是一些 tips:

  • MATCH 性能比 GO 略慢,但 MATCH 是咱们优化的重点。如果没有强性能需求的话,举荐还是尽量应用 MATCH,表白能加丰盛之外,它同将要出炉的 ISO GQL(图查询语言)是匹配的;
  • 慎用函数 (无奈下推),在 NebulaGraph 中并没有将函数下推到 storage。因而,像 src(edge)dst(edge)rank(edge)properties($$)` 之类的函数,性能都不如 `edge_.src`、`edge._dst`、`edge._rank`、`$$.tag.prop 这些下推到 storage 的表白;
  • 遇到聚合且须要取属性的状况,先聚合再取属性,因为取属性耗时较长;
  • MATCH 如果只是最初须要返回 count,那么对于 count 的变量最好采纳 count(id(v)) 相似的模式,这样会利用到属性裁剪,缩小内存耗费;
  • 能不带门路尽量不要带门路,带门路须要进行门路结构,属性裁剪会生效,此外,还会减少很多额定的内存开销。

总的来说, 缩小含糊、减少确定,越早越好

内存保护试试 Memory Tracker

在 v3.4 版本中,引入的一个大性能是:Memory Tracker,用来爱护内存,避免内存占用过大导致的 OOM 问题。

  • 预留内存:memory_tracker_untracked_reserved_memory_mb(默认 50 MB)。Memory Tracker 机制会治理通过 new/delete 申请内存,但过程除了通过此种形式申请内存外,还可能存在其余形式占用的内存;比方通过调用底层的 malloc/free 申请,这些内存通过此 flag 管制,在计算时会扣除此局部未被 track 的内存,所以这里预留了 50 MB;
  • 内存比例:memory_tracker_limit_ratio,就是理论可用内存的比例占用多少的状况下,会限度它再申请应用内存。个别默认是 0.8,就是这个内存占用小于 0.8 的状况下,是能够随便应用内存的;当零碎内存占用超过 80% 时,零碎便会回绝掉新的查问语句;

    • 数值范畴:(0,1],默认 0.8 且为开启状态。大多数的用户的 storage 和 graph 节点都存在混部状况,这时候就会倡议调低 memory_tracker_limit_ratio,顺便说一句,这个参数项是反对在线调整的;
    • 数值配置成 2,则会对其进行动静调整,这个动态分配的内存占用比例可能会不大精准;
    • 数值配置成 3,则敞开 Memory Tracker 性能;

此外,你如果要调试 Memory Tracker 的话,能够开启 memory_tracker_detail_log 来取得调试日志,这个参数项默认是敞开的。

经测试,Memory Tracker 对性能有 1% 左右的影响,然而对于下层为平台类产品或者交互式剖析类产品,强烈建议关上。为什么呢?因为下层的业务同学不大理解 NebulaGrpah 运行机制的状况下,容易将服务打满,导致内存爆炸,因而开启这个性能之后,至多能保证系统的稳固运行。

最初,如果动静申请内存时,返回报错 GRAPH_MEMORY_EXCEEDED/STORAGE_MEMORY_EXCEEDED 阐明这个内存曾经不够用,这条查问语句将不会执行(被杀掉)。

语句调试得用 PROFILE

在任意一条 nGQL 查问语句后面退出 PROFILE,并能失去这条语句的执行打算。

上图一条语句的整个生命周期,Planner 是执行打算(Execution Plan)生成器,它会依据 Validator 校验过、语义非法的查问语法树生成可供执行器(Executor)执行的未经优化的执行打算,而该执行打算会在之后交由 Optimizer 生成一个优化的执行打算,并最终交给 Executor 执行。执行打算由一系列节点(PlanNode)组成。而下图则是一些常见的算子,上图每一个 Plan 节点对应了一个算子:

算子 介绍
GetNeighbor 依据指定的 vid,从存储层获取起始点和边的属性
Traverse 仅用于 MATCH 匹配 ()-[e:0..n]-() 模式,获取拓展过程中的起始点和边的属性
AppendVertices MATCH 应用,同算子 Traverse 配合获取点的属性
GetEdge 获取边的属性
GetVertices 获取点的属性,FETCH PROP 或者 GO 语句中。
ScanEdge 全表扫描边,例如 MATCH ()-[e]->() RETURN e LIMIT 3
ScanVertices 全表扫描点,例如 MATCH (v) return v LIMIT 3
IndexScan MATCH 语句中找到起始点的索引查问
TagIndexPrefixScan LOOKUP 语句中前缀扫描 LOOKUP ON player where player.name == "Steve Nash" YIELD player.name
TagIndexRangeScan LOOKUP 语句中范畴扫描 LOOKUP ON player where player.name > "S" YIELD player.name
TagIndexFullScan LOOKUP 语句中全扫描 LOOKUP ON player YIELD player.name
Filter 按条件过滤,例如 WHERE 语句
Project 获取上一步算子的列
Dedup 去重
LeftJoin 合并后果
LIMIT 限度输入行数

上面这个是一个例子,咱们能够联合例子解说下。一般来说 PROFILE 会生成一个执行打算,同 EXPLAIN 生成执行打算不同,PROFILE 生成的执行打算中会有绝对应的执行工夫在外面,比如说上面这张图:

一般来说,咱们看执行打算不只是看高低的调用关系,还须要去看外面的具体执行细节:

  • execTime:graphd 的解决工夫;
  • totalTime:graphd 算子起到到算子退出工夫;
  • total_rpc_time:graphd 调用 storage client 发出请求到接管到申请工夫;
  • exec:storaged 的解决工夫,同下面的 graphd 解决工夫的 execTime
  • total:storage client 接管到 graphd 申请到 storage client 发送申请的工夫,即 storaged 自身的解决工夫加上序列化和反序列化的工夫;

除了查看工夫之外,咱们还要查看 row 就能看到 graphd 和 storaged 的具体通信量大小。上图有 3 个 Partition,每个 Partition 返回 1 个 limit 1,总共就 3 条数据。

此外,还得查看执行打算中是否蕴含计算下推:

下面两条查问语句的差别,下面提到过,就是将函数改成其余调用形式,将 properties(edge).degree 改为 follow.degree,很显著地看到计算下推了。

某个性能不反对 array

因为产品布局的问题,NebulaGraph 可能有些性能没法间接反对。比方用户反馈的:

以后属性仅反对根本类型 long、string,来构建索引。是否能够反对多值,比方:long[]string[] 来构建索引?

确实目前不反对 array,有什么曲线救国的法子?这里提供一些办法,仅供参考:

  1. 把数组放到 string 里,进行查问时,将数据读取进去进行解析,尽管有点不优雅,然而能解决问题;
  2. 转化成 bitmap,将不同的类型组成 bitmap,尽管导致代码会简单点,但能够取得比拟快的过滤;
  3. 边上的 array 转换成两点之间的平行边,相当于一条边就是一个属性,能够不便地进行属性过滤,当然它会带了额定的边数量减少问题;
  4. 点上的 array 转化成自环边,弊病第 3 种形式,会产生大量本人指向本人的平行边;
  5. 把属性作为 tag,比方我当初有个商品,它在北京、上海、杭州都有仓库,这时候能够将这个货点变成一个 tag 属性,从而不便地对其进行查问。这里须要留神的是,这个形式容易产生超级节点,这里就须要留神防止超级节点的产生。

性能更强了 UDF

用户自定义函数(User-defined Function,UDF),用户能够在 nGQL 中调用函数。与从 nGQL 中调用的内置函数一样,UDF 的逻辑通常扩大或加强了 nGQL 的性能,使其具备 nGQL 没有或不善于解决的性能。UDF 被定义后能够重复使用。

这里简述下 UDF 的应用过程:

  1. 筹备编译环境 & 下载源码:https://docs.nebula-graph.com.cn/3.5.0/4.deployment-and-installation/1.resource-preparations/
  2. 进入到 NebulaGraph 代码仓库,创立 UDF 相干源码文件。以后有两个示意文件 standard_deviation.cpp / standard_deviation.h 能够参考
  3. 编译 UDF
g++ -c -I ../src/ -I ../build/third-party/install/include/ -fPIC standard_deviation.cpp -o standard_deviation.o
g++ -shared -o standard_deviation.so standard_deviation.o
  1. 加载 UDF 至 graphd 服务

编辑 graphd 服务配置文件:关上 /usr/local/nebula/etc/nebula-graphd.conf 文件,增加或批改以下配置项:

#  UDF  C++
--enable_udf=true
#  UDF .so
--udf_path=/home/foobar/dev/nebula/udf/
  1. 重启 graphd
udo /usr/local/nebula/scripts/nebula.service restart graphd
  1. 连贯到 graphd 后验证
GO 1 TO 2 STEPS FROM“player100”OVER follow YIELD properties(edge).degree AS d | yield collect($-.d) AS d | yield standard_deviation($-.d)

不过,目前 UDF 有些问题:

  1. so 包地位只反对扫描本地,也就是如果你是分布式集群的话,每个机器上都得有个包;
  2. 函数只在 graphd 层,无奈下推到存储;
  3. 暂不反对 Java(性能思考),将来版本会反对;

待解决的问题

这里列举下将来的产品可优化点:

  1. 全文索引:v3.4 之前的全文索引性能都不太好用,束缚比拟多,且有些 bug。v3.4 版本做了精简和优化,更加稳固。但实际上,v3.4 及之前的全文索引性能精确讲并不是真正意义的全文索引,次要是反对前缀搜寻、通配符搜寻、正则表达式搜寻和含糊搜寻等。并不反对分词、以及查问的分数,v3.6 版本(行将公布)做了全文索引的优化,从新设计了全文索引性能(能够更好的反对 Neo4j 替换)。不过与原有的全文索引不兼容,须要重建索引;
  2. 对于悬挂边的产生:设计理念,不隐式的对数据进行变更导致(删除点的时候不隐式删除边),当然由此会带来悬挂边和孤儿点的问题,前面这块会思考进行相干的优化;
  3. 事务的反对,大多数人对 NebulaGraph 的事务需要来源于,他认为 NebulaGraph 是一款 TP 产品,TP 产品是肯定具备事务性的,这并非是业务场景的需要。当然,事务这块撇开这种某款产品必须具备的个性之外这点,一些生产链路下面,事务还是一个强需要,因而在后续的开发中也会新增事务个性。

其余问题交换

上面问题整顿自本次分享的 QA 局部:

Q:上文提到 VID 的设定,是越短越好。短的 VID 会带来什么结果么?

方扬:VID 实践上是越短越好,没有任何的副作用。不过它的设定是在满足你既有的业务需要,不要呈现反复的 VID 状况下,尽可能的短即可;

Q:截断的话,是对返回的数据量做限度,这个返回的话是有序的么?

训焘:目前数据的返回是 random,随即返回的,随机依据你的 range 来返回一些数据量。


谢谢你读完本文 (///▽///)

如果你想尝鲜图数据库 NebulaGraph,记得去 GitHub 下载、应用、(^з^)-☆ star 它 -> GitHub;和其余的 NebulaGraph 用户一起交换图数据库技术和利用技能,留下「你的名片」一起游玩呀~

退出移动版