本文整顿自 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 的最佳实际
在这个局部次要分为:建模、数据导入、查问等三大内容。
数据建模
数据收缩
这是社区用户在交换群里反馈的一个问题:
- 数据导入之后占用硬盘空间极大,60 MB 的文件导入之后,Storage 占用了 3.5 G;
- Storage 服务占用内存很高
- 并发量大的时候,会呈现 ConnectionPool 不够的状况;
- 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_subcompactions
和 max_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 性能;
- 数值范畴:(0,1],默认 0.8 且为开启状态。大多数的用户的 storage 和 graph 节点都存在混部状况,这时候就会倡议调低
此外,你如果要调试 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,有什么曲线救国的法子?这里提供一些办法,仅供参考:
- 把数组放到 string 里,进行查问时,将数据读取进去进行解析,尽管有点不优雅,然而能解决问题;
- 转化成 bitmap,将不同的类型组成 bitmap,尽管导致代码会简单点,但能够取得比拟快的过滤;
- 边上的 array 转换成两点之间的平行边,相当于一条边就是一个属性,能够不便地进行属性过滤,当然它会带了额定的边数量减少问题;
- 点上的 array 转化成自环边,弊病第 3 种形式,会产生大量本人指向本人的平行边;
- 把属性作为 tag,比方我当初有个商品,它在北京、上海、杭州都有仓库,这时候能够将这个货点变成一个 tag 属性,从而不便地对其进行查问。这里须要留神的是,这个形式容易产生超级节点,这里就须要留神防止超级节点的产生。
性能更强了 UDF
用户自定义函数(User-defined Function,UDF),用户能够在 nGQL 中调用函数。与从 nGQL 中调用的内置函数一样,UDF 的逻辑通常扩大或加强了 nGQL 的性能,使其具备 nGQL 没有或不善于解决的性能。UDF 被定义后能够重复使用。
这里简述下 UDF 的应用过程:
- 筹备编译环境 & 下载源码:https://docs.nebula-graph.com.cn/3.5.0/4.deployment-and-installation/1.resource-preparations/
- 进入到 NebulaGraph 代码仓库,创立 UDF 相干源码文件。以后有两个示意文件
standard_deviation.cpp
/standard_deviation.h
能够参考 - 编译 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
- 加载 UDF 至 graphd 服务
编辑 graphd 服务配置文件:关上 /usr/local/nebula/etc/nebula-graphd.conf
文件,增加或批改以下配置项:
# UDF C++
--enable_udf=true
# UDF .so
--udf_path=/home/foobar/dev/nebula/udf/
- 重启 graphd
udo /usr/local/nebula/scripts/nebula.service restart graphd
- 连贯到 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 有些问题:
- so 包地位只反对扫描本地,也就是如果你是分布式集群的话,每个机器上都得有个包;
- 函数只在 graphd 层,无奈下推到存储;
- 暂不反对 Java(性能思考),将来版本会反对;
待解决的问题
这里列举下将来的产品可优化点:
- 全文索引:v3.4 之前的全文索引性能都不太好用,束缚比拟多,且有些 bug。v3.4 版本做了精简和优化,更加稳固。但实际上,v3.4 及之前的全文索引性能精确讲并不是真正意义的全文索引,次要是反对前缀搜寻、通配符搜寻、正则表达式搜寻和含糊搜寻等。并不反对分词、以及查问的分数,v3.6 版本(行将公布)做了全文索引的优化,从新设计了全文索引性能(能够更好的反对 Neo4j 替换)。不过与原有的全文索引不兼容,须要重建索引;
- 对于悬挂边的产生:设计理念,不隐式的对数据进行变更导致(删除点的时候不隐式删除边),当然由此会带来悬挂边和孤儿点的问题,前面这块会思考进行相干的优化;
- 事务的反对,大多数人对 NebulaGraph 的事务需要来源于,他认为 NebulaGraph 是一款 TP 产品,TP 产品是肯定具备事务性的,这并非是业务场景的需要。当然,事务这块撇开这种某款产品必须具备的个性之外这点,一些生产链路下面,事务还是一个强需要,因而在后续的开发中也会新增事务个性。
其余问题交换
上面问题整顿自本次分享的 QA 局部:
Q:上文提到 VID 的设定,是越短越好。短的 VID 会带来什么结果么?
方扬:VID 实践上是越短越好,没有任何的副作用。不过它的设定是在满足你既有的业务需要,不要呈现反复的 VID 状况下,尽可能的短即可;
Q:截断的话,是对返回的数据量做限度,这个返回的话是有序的么?
训焘:目前数据的返回是 random,随即返回的,随机依据你的 range 来返回一些数据量。
谢谢你读完本文 (///▽///)
如果你想尝鲜图数据库 NebulaGraph,记得去 GitHub 下载、应用、(^з^)-☆ star 它 -> GitHub;和其余的 NebulaGraph 用户一起交换图数据库技术和利用技能,留下「你的名片」一起游玩呀~