本文首发于 Nebula Graph 公众号 NebulaGraphCommunity ,Follow 看大厂图数据库技术实践。
## 1 背景
Nebula 2.0 中已经支持了基于外部全文搜索引擎的文本查询功能。在介绍这个功能前,我们先简单回顾一下 Nebula Graph 的架构设计和存储模型,更易于下边章节的描述。
### 1.1 Nebula Graph 架构简介
如图所示,Storage Service 共有三层,最底层是 Store Engine,它是一个单机版 local store engine,提供了对本地数据的 get
/ put
/ scan
/ delete
操作,相关的接口放在 KVStore / KVEngine.h 文件里面,用户完全可以根据自己的需求定制开发相关 local store plugin,目前 Nebula 提供了基于 RocksDB 实现的 Store Engine。
在 local store engine 之上,便是我们的 Consensus 层,实现了 Multi Group Raft,每一个 Partition 都对应了一组 Raft Group,这里的 Partition 便是我们的数据分片。目前 Nebula 的分片策略采用了 静态 Hash
的方式,具体按照什么方式进行 Hash,在下一个章节 schema 里会提及。用户在创建 SPACE 时需指定 Partition 数,Partition 数量一旦设置便不可更改,一般来讲,Partition 数目要能满足业务将来的扩容需求。
在 Consensus 层上面也就是 Storage Service 的最上层,便是我们的Storage Interfaces,这一层定义了一系列和图相关的 API。 这些 API 请求会在这一层被翻译成一组针对相应 Partition 的 KV 操作。正是这一层的存在,使得我们的存储服务变成了真正的图存储,否则,Storage Service 只是一个 KV 存储罢了。而 Nebula 没把 KV 作为一个服务单独提出,其最主要的原因便是图查询过程中会涉及到大量计算,这些计算往往需要使用图的 Schema,而 KV 层是没有数据 Schema 概念,这样设计会比较容易实现计算下推。
### 1.2 Nebula Graph 存储介绍
Nebula Graph 在 2.0 中,对存储结构进行了改进,其包含点、边和索引的存储结构,接下来我们将简单回顾一下 2.0 的存储结构。通过存储结构的解释,大家基本也可以简单了解 Nebula Graph 的数据和索引扫描原理。
#### 1.2.1 Nebula 数据存储结构
Nebula 数据的存储包含“点”和“边”的存储,“点” 和 “边” 的存储均是基于 KV 模型存储,这里我们主要介绍其 Key 的存储结构,其结构如下所示
Type
: 1 个字节,用来表示 key 的类型,当前的类型有 vertex、edge、index、system 等。PartID
: 3 个字节,用来表示数据分片 partition,此字段主要用于 partition 重新分布(balance)时方便根据前缀扫描整个 partition 数据VertexID
: n 个字节, 出边里面用来表示源点的 ID, 入边里面表示目标点的 ID。Edge Type
: 4 个字节, 用来表示这条边的类型,如果大于 0 表示出边,小于 0 表示入边。Rank
: 8 个字节,用来处理同一种类型的边存在多条的情况。用户可以根据自己的需求进行设置,这个字段可 存放交易时间 、 交易流水号 、或 某个排序权重 。PlaceHolder
: 1 个字节,对用户不可见,未来实现分布式做事务的时候使用。TagID
:4 个字节,用来表示 tag 的类型。##### 1.2.1.1 点的存储结构
Type (1 byte) | PartID (3 bytes) | VertexID (n bytes) | TagID (4 bytes) |
---|
##### 1.2.1.2 边的存储结构
Type (1 byte) | PartID (3 bytes) | VertexID (n bytes) | EdgeType (4 bytes) | Rank (8 bytes) | VertexID (n bytes) | PlaceHolder (1 byte) |
---|
#### 1.2.2 Nebula 索引存储结构
##### 1.2.2.1 tag index 存储结构
Type (1 byte) | PartID (3 bytes) | IndexID (4 bytes) | props binary (n bytes) | nullable bitset (2 bytes) | VertexID (n bytes) |
---|
##### 1.2.2.2 edge index 存储结构
Type (1 byte) | PartID (3 bytes) | IndexID (4 bytes) | props binary (n bytes) | nullable bitset (2 bytes) | VertexID (n bytes) | Rank (8 bytes) | VertexID (n bytes) |
---|
### 1.3 借用第三方全文搜索引擎的原因
由以上的存储结构推理可以看出,如果我们想要对某个 prop 字段进行文本的模糊查询,都需要进行一个 full table scan
或 full index scan
,然后逐行过滤,由此看来,查询性能将会大幅下降,数据量大的情况下,很有可能还没扫描完毕就出现内存溢出的情况。另外,如果将 Nebula 索引的存储模型设计为适合文本搜索的倒排索引模型,那将背离 Nebula 索引初始的设计原则。经过一番调研和讨论,所谓术业有专攻,文本搜索的工作还是交给外部的第三方全文搜索引擎来做,在保证查询性能的基础上,同时也降低了 Nebula 内核的开发成本。
### 2 目标
### 2.1 功能
2.0 版本我们只对 LOOKUP
支持了文本搜索功能。也就是说基于 Nebula 的内部索引,借助第三方全文搜索引擎来完成 LOOKUP
的文本搜索功能。对于第三方全文引擎来说,目前只使用了一些基本的数据导入、查询等功能。如果是要做一些复杂的、纯文本的查询计算的话,Nebula 目前的功能还有待完善和改进,期待广大的社区用户提出宝贵的建议。目前所支持的文本搜索表达式如下:
### 2.2 性能
这里所说的性能,指数据同步性能和查询性能。
LOOKUP
中通过第三方全文引擎支持了文本搜索,不可避免的性能会慢于 Nebula 原生的索引扫描,有时甚至第三方全文引擎自身的查询都会很慢,此时我们需要有一个 时效机制 来保证查询性能。即 LIMIT
和 TIMEOUT
,将在下列章节中详细介绍。## 3 名词解释
名称 | 说明 |
---|---|
Tag | 用于点上的属性结构,一个 vertex 可以附加多个 tag,以 tagId 标示。 |
Edge | 类似于 tag,edge 是用于边上的属性结构,以 edgetype 标示。 |
Property | tag 或 edge 上的属性值,其数据类型由 tag 或 edge 的结构确定。 |
Partition | Nebula Graph 的最小逻辑存储单元,一个 Storage Engine 可包含多个 partition。Partition 分为 leader 和 follower 的角色,raftex 保证了 leader 和 follower 之间的数据一致性。 |
Graph space | 每个 graph space 是一个独立的业务 graph 单元,每个 graph space 有其独立的 tag 和 edge 集合。一个 Nebula Graph 集群中可包含多个 graph space。 |
Index | 下文中出现的 index 指 Nebula Graph 中点和边上的属性索引。其数据类型依赖于 tag 或 edge。 |
TagIndex | 基于 tag 创建的索引,一个 tag 可以创建多个索引。因暂不支持复合索引,因此一个索引只可以基于一个 tag。 |
EdgeIndex | 基于 edge 创建的索引。同样,一个 edge 可以创建多个索引,但一个索引只可以基于一个 edge。 |
Scan Policy | index 的扫描策略,往往一条查询语句可以有多种索引的扫描方式,但具体使用哪种扫描方式需要 scan policy 来决定。 |
Optimizer | 对查询条件进行优化,例如对 WHERE 子句的表达式树进行子表达式节点的排序、分裂、合并等。其目的是获取更高的查询效率。 |
## 4 实现逻辑
目前我们兼容的第三方全文搜索引擎是 ElasticSearch,此章节中主要围绕 ElasticSearch 来进行描述。
### 4.1 存储结构
#### 4.1.1 DocID
partId(10 bytes) | schemaId(10 bytes) | encoded_columnName(32 bytes) | encoded_val(max 344 bytes) |
---|
#### 4.1.2 Doc Fields
### 4.2 数据同步逻辑
Leader & Listener
上边的章节中简单介绍了数据异步同步的逻辑,此逻辑将在本章节中详细介绍。介绍之前,先让我们认识一下 Nebula 的 Leader 和 Listener。
nebula-stoage.conf
决定。nebula-stoage-listener.conf
决定。Listener 作为一个监听者,会被动的接收来自于 Leader 的 WAL,并定时的将 WAL 进行解析,并调用第三方全文引擎的数据插入 API 将数据同步到第三方全文搜索引擎中。对于 ElasticSearch,Nebula 支持 PUT
和 BULK
接口。接下来我们介绍一下数据同步逻辑:
INSERT
请求发送到相关 Partition 的 LeaderINSERT
请求,并将 WAL 同步到 Listener 中PUT
或 BULK
接口写入到 ElasticSearch 中。在以上步骤中,如果因为 ElasticSearch 集群挂掉,或 Listener 进程挂掉,则停止 WAL 同步。当系统恢复后,会接着上次成功的 Log ID 继续进行数据同步。在这里有一个建议,需要 DBA 通过外部监控工具实时监控 ES 的运行状态,如果 ES 长期处于无效状态,会导致 Listener 的 log 日志暴涨,并且无法做正常的查询操作。
### 4.3 查询逻辑
由上图可知,其文本搜索的关键步骤是 “Send Fulltext Scan Request” → "Fulltext Cluster" → "Collect Constant Values" → "IndexScan Optimizer"。
C1 == "A1" OR C1 == "A2"
。LIMIT
和 TIMEOUT
机制,实时中断 ES 端的查询。## 5 演示
### 5.1 部署外部ES集群
对于 ES 集群的部署,这里不再详细介绍,相信大家都很熟悉了。这里需要说明的是,当 ES 集群启动成功后,我们需要对 ES 集群创建一个通用的 template,其结构如下:
{
"template": "nebula*",
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 1
}
},
"mappings": {
"properties" : {
"tag_id" : { "type" : "long" },
"column_id" : { "type" : "text" },
"value" :{ "type" : "keyword"}
}
}
}
### 5.2 部署 Nebula Listener
nebula-storaged-listener.conf
./bin/nebula-storaged --flagfile ${listener_config_path}/nebula-storaged-listener.conf
### 5.3 注册 ElasticSearch 的客户端连接信息
nebula> SIGN IN TEXT SERVICE (127.0.0.1:9200);
nebula> SHOW TEXT SEARCH CLIENTS;
+-------------+------+
| Host | Port |
+-------------+------+
| "127.0.0.1" | 9200 |
+-------------+------+
| "127.0.0.1" | 9200 |
+-------------+------+
| "127.0.0.1" | 9200 |
+-------------+------+
### 5.4 创建 Nebula Space
CREATE SPACE basketballplayer (partition_num=3,replica_factor=1, vid_type=fixed_string(30));
USE basketballplayer;
### 5.5 添加 Listener
nebula> ADD LISTENER ELASTICSEARCH 192.168.8.5:46780,192.168.8.6:46780;
nebula> SHOW LISTENER;
+--------+-----------------+-----------------------+----------+
| PartId | Type | Host | Status |
+--------+-----------------+-----------------------+----------+
| 1 | "ELASTICSEARCH" | "[192.168.8.5:46780]" | "ONLINE" |
+--------+-----------------+-----------------------+----------+
| 2 | "ELASTICSEARCH" | "[192.168.8.5:46780]" | "ONLINE" |
+--------+-----------------+-----------------------+----------+
| 3 | "ELASTICSEARCH" | "[192.168.8.5:46780]" | "ONLINE" |
+--------+-----------------+-----------------------+----------+
### 5.6 创建 Tag、Edge、Nebula Index
此时建议字段 “name” 的长度应该小于 256,如果业务允许,建议 player 中字段 name 的类型定义为 fixed_string 类型,其长度小于 256。
nebula> CREATE TAG player(name string, age int);
nebula> CREATE TAG INDEX name ON player(name(20));
### 5.7 插入数据
nebula> INSERT VERTEX player(name, age) VALUES \\
"Russell Westbrook": ("Russell Westbrook", 30), \\
"Chris Paul": ("Chris Paul", 33),\\
"Boris Diaw": ("Boris Diaw", 36),\\
"David West": ("David West", 38),\\
"Danny Green": ("Danny Green", 31),\\
"Tim Duncan": ("Tim Duncan", 42),\\
"James Harden": ("James Harden", 29),\\
"Tony Parker": ("Tony Parker", 36),\\
"Aron Baynes": ("Aron Baynes", 32),\\
"Ben Simmons": ("Ben Simmons", 22),\\
"Blake Griffin": ("Blake Griffin", 30);
### 5.8 查询
nebula> LOOKUP ON player WHERE PREFIX(player.name, "B");
+-----------------+
| _vid |
+-----------------+
| "Boris Diaw" |
+-----------------+
| "Ben Simmons" |
+-----------------+
| "Blake Griffin" |
+-----------------+
## 6 问题跟踪与解决技巧
对于系统环境的搭建过程中,可能某个步骤错误导致功能无法正常运行,在之前的用户反馈中,我总结了三类可能发生的错误,对分析和解决问题的技巧概况如下
IP:Port
不和已有的 nebula-storaged 冲突IP:Port
正确,这个要和 nebula-storaged 中保持一致nebula-storaged-listener.conf
配置文件中 –listener_path
的目录下是否有文件。UPDATE CONFIGS storage:v=3
),并关注 log 中 CURL 命令是否执行成功,如果有错误,可能是 ES 配置或 ES 版本兼容性错误UPDATE CONFIGS graph:v=3
),关注 graph 的 log,检查 CURL 命令是什么原因执行失败## 7 TODO
交流图数据库技术?加入 Nebula 交流群请先 填写下你的 Nebula 名片 ,Nebula 小助手会拉你进群~~
想要和其他大厂交流图数据库技术吗?NUC 2021 大会等你来交流: NUC 2021 报名传送门
如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!
赞0
添加新评论0 条评论