分布式图数据库 Nebula Graph 的 Index 实践

简介: 索引是数据库系统中不可或缺的一个功能,数据库索引好比是书的目录,能加快数据库的查询速度,其实质是数据库管理系统中一个排序的数据结构。不同的数据库系统有不同的排序结构,目前常见的索引实现类型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各种索引类型都有各自的排序算法。

导读

索引是数据库系统中不可或缺的一个功能,数据库索引好比是书的目录,能加快数据库的查询速度,其实质是数据库管理系统中一个排序的数据结构。不同的数据库系统有不同的排序结构,目前常见的索引实现类型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各种索引类型都有各自的排序算法。

虽然索引可以带来更高的查询性能,但是也存在一些缺点,例如:

  • 创建索引和维护索引要耗费额外的时间,往往是随着数据量的增加而维护成本增大
  • 索引需要占用物理空间
  • 在对数据进行增删改的操作时需要耗费更多的时间,因为索引也要进行同步的维护

Nebula Graph 作为一个高性能的分布式图数据库,对于属性值的高性能查询,同样也实现了索引功能。本文将对 Nebula Graph的索引功能做一个详细介绍。

图数据库 Nebula Graph 术语

开始之前,这里罗列一些可能会使用到的图数据库和 Nebula Graph 专有术语:

  • Tag:点的属性结构,一个 Vertex 可以附加多种 tag,以 TagID 标识。(如果类比 SQL,可以理解为一张点表)
  • Edge:类似于 Tag,EdgeType 是边上的属性结构,以 EdgeType 标识。(如果类比 SQL,可以理解为一张边表)
  • Property:tag / edge 上的属性值,其数据类型由 tag / edge 的结构确定。
  • Partition:Nebula Graph 的最小逻辑存储单元,一个 StorageEngine 可包含多个 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 可以创建多个索引。目前(2020.3)暂不支持跨 tag 的复合索引,因此一个索引只可以基于一个 tag。
  • EdgeIndex:基于 Edge 创建的索引。同样,一个 Edge 可以创建多个索引,但一个索引只可以基于一个 edge。
  • Scan Policy:Index 的扫描策略,往往一条查询语句可以有多种索引的扫描方式,但具体使用哪种扫描方式需要 Scan Policy 来决定。
  • Optimizer:对查询条件进行优化,例如对 where 子句的表达式树进行子表达式节点的排序、分裂、合并等。其目的是获取更高的查询效率。

索引需求分析

Nebula Graph 是一个图数据库系统,查询场景一般是由一个点出发,找出指定边类型的相关点的集合,以此类推进行(广度优先遍历)N 度查询。另一种查询场景是给定一个属性值,找出符合这个属性值的所有的点或边。在后面这种场景中,需要对属性值进行高性能的扫描,查出与此属性值对应的边或点,以及边或点上的其它属性。为了提高属性值的查询效率,在这里引入了索引的功能。对边或点的属性值进行排序,以便快速的定位到某个属性上。以此避免了全表扫描。

可以看到对图数据库 Nebula Graph 的索引要求:

  • 支持 tag 和 edge 的属性索引
  • 支持索引的扫描策略的分析和生成
  • 支持索引的管理,如:新建索引、重建索引、删除索引、list | show 索引等。

系统架构概览

图数据库 Nebula Graph 存储架构

从架构图可以看到,每个Storage Server 中可以包含多个 Storage Engine, 每个 Storage Engine中可以包含多个Partition, 不同的Partition之间通过 Raft 协议进行一致性同步。每个 Partition 中既包含了 data,也包含了 index,同一个点或边的 data 和 index 将被存储到同一个 Partition 中。

业务具体分析

数据存储结构

为了更好的描述索引的存储结构,这里将图数据库 Nebula Graph 原始数据的存储结构一起拿出来分析下。

点的存储结构

点的 Data 结构

点的 Index 结构

Vertex 的索引结构如上表所示,下面来详细地讲述下字段:

PartitionId:一个点的数据和索引在逻辑上是存放到同一个分区中的。之所以这么做的原因主要有两点:

  1. 当扫描索引时,根据索引的 key 能快速地获取到同一个分区中的点 data,这样就可以方便地获取这个点的任何一种属性值,即使这个属性列不属于本索引。
  2. 目前 edge 的存储是由起点的 ID Hash 分布,换句话说,一个点的出边存储在哪是由该点的 VertexId 决定的,这个点和它的出边如果被存储到同一个 partition 中,点的索引扫描能快速地定位该点的出边。

IndexId:index 的识别码,通过 indexId 可获取指定 index 的元数据信息,例如:index 所关联的 TagId,index 所在列的信息。

Index binary:index 的核心存储结构,是所有 index 相关列属性值的字节编码,详细结构将在本文的 #Index binary# 章节中讲解。

VertexId:点的识别码,在实际的 data 中,一个点可能会有不同 version 的多行数据。但是在 index 中,index 没有 Version 的概念,index 始终与最新 Version 的 Tag 所对应

上面讲完字段,我们来简单地实践分析一波:

假设 _PartitionId_ 为 100,TagId 有 tag_1 _和 tag_2,_其中 _tag_1_ 包含三列 :col_t1_1、col_t1_2、col_t1_3,_tag_2_ 包含两列:col_t2_1、col_t2_2。

现在我们来创建索引:

  • i1 = tag_1 (col_t1_1, col_t1_2) ,假设 i1 的 ID 为 1;
  • i2 = tag_2(col_t2_1, col_t2_2),  假设 i2 的 ID 为 2;

可以看到虽然 tag_1 中有 col_t1_3 这列,但是建立索引的时候并没有使用到 col_t1_3,因为在图数据库 Nebula Graph 中索引可以基于 Tag 的一列或多列进行创建

插入点
// VertexId = hash("v_t1_1"),假如为 50 
INSERT VERTEX tag_1(col_t1_1, col_t1_2, col_t1_3), tag_2(col_t2_1, col_t2_2) \
   VALUES hash("v_t1_1"):("v_t1_1", "v_t1_2", "v_t1_3", "v_t2_1", "v_t2_2");

从上可以看到 VertexId 可由 ID 标识对应的数值经过 Hash 得到,如果标识对应的数值本身已经为 int64,则无需进行 Hash 或者其他转化数值为 int64 的运算。而此时数据存储如下:

此时点的 Data 结构

此时点的 Index 结构

说明:index 中 row 和 key 是一个概念,为索引的唯一标识;

边的存储结构

边的索引结构和点索引结构原理类似,这里不再赘述。但有一点需要说明,为了使索引 key 的唯一性成立,索引的 key 的生成借助了不少 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,这也是为什么点索引中并没有 TagId 字段(边索引中也没有 EdgeType 字段),这是因为 IndexId 本身带有 VertexId 等信息可直接区分具体的 tagId 或 EdgeType

边的 Data 结构

边的 Index 结构

Index binary 介绍

Index binary 是 index 的核心字段,在 index binary 中区分定长字段和不定长字段,int、double、bool 为定长字段,string 则为不定长字段。由于 index binary 是将所有 index column 的属性值编码连接存储,为了精确地定位不定长字段,Nebula Graph 在 index binary 末尾用 int32 记录了不定长字段的长度。

举个例子:

我们现在有一个 index binary 为 index1,是由 int 类型的索引列1 c1、string 类型的索引列 c2,string 类型的索引列 c3 组成:

index1 (c1:int, c2:string, c3:string)

假如索引列 c1、c2、c3 某一行对应的 property 值分别为:23、"abc"、"here",则在 index1 中这些索引列将被存储为如下(在示例中为了便于理解,我们直接用原值,实际存储中是原值会经过编码再存储):

  • length = sizeof("abc") = 3
  • length = sizeof("here") = 4

所以 index1 该 row 对应的 key 则为 23abchere34;

回到我们 Index binary 章节开篇说的 index binary 格式中存在 Variable-length field lenght 字段,那么这个字段的的具体作用是什么呢?我们来简单地举个例:

现在我们又有了一个 index binary,我们给它取名为 index2,它由 string 类型的索引列1 c1、string 类型的索引列 c2,string 类型的索引列 c3 组成:

index2 (c1:string, c2:string, c3:string)

假设我们现在 c1、c2、c3 分别有两组如下的数值:

  • row1 : ("ab", "ab", "ab")
  • row2: ("aba", "ba", "b")

可以看到这两行的 prefix(上图红色部分)是相同,都是 "ababab",这时候怎么区分这两个 row 的 index binary 的 key 呢?别担心,我们有 Variable-length field lenght 。

若遇到 where c1 == "ab" 这样的条件查询语句,在 Variable-length field length 中可直接根据顺序读取出 c1 的长度,再根据这个长度取出 row1 和 row2 中 c1 的值,分别是 "ab" 和 "aba" ,这样我们就精准地判断出只有 row1 中的 "ab" 是符合查询条件的。

索引的处理逻辑

Index write

当 Tag / Edge中的一列或多列创建了索引后,一旦涉及到 Tag / Edge 相关的写操作时,对应的索引必须连同数据一起被修改。下面将对索引的write操作在storage层的处理逻辑进行简单介绍:

INSERT——插入数据

当用户产生插入点/边操作时,insertProcessor 首先会判断所插入的数据是否有存在索引的 Tag 属性 / Edge 属性。如果没有关联的属性列索引,则按常规方式生成新 Version,并将数据 put 到 Storage Engine;如果有关联的属性列索引,则通过原子操作写入 Data 和 Index,并判断当前的 Vertex / Edge 是否有旧的属性值,如果有,则一并在原子操作中删除旧属性值。

DELETE——删除数据

当用户发生 Drop Vertex / Edge 操作时,deleteProcessor 会将 Data 和 Index(如果存在)一并删除,在删除的过程中同样需要使用原子操作。

UPDATE——更新数据

Vertex / Edge 的更新操作对于 Index 来说,则是 drop 和 insert 的操作:删除旧的索引,插入新的索引,为了保证数据的一致性,同样需要在原子操作中进行。但是对应普通的 Data 来说,仅仅是 insert 操作,使用最新 Version 的 Data 覆盖旧 Version 的 data 即可。

Index scan

在图数据库 Nebula Graph 中是用 LOOKUP 语句来处理 index scan 操作的,LOOKUP 语句可通过属性值作为判断条件,查出所有符合条件的点/边,同样 LOOKUP 语句支持 WHERE 和 YIELD 子句。 

LOOKUP 使用技巧

正如根据本文#数据存储结构#章节所描述那样,index 中的索引列是按照创建 index 时的列顺序决定。

举个例子,我们现在有 tag (col1, col2),根据这个 tag 我们可以创建不同的索引,例如:

  • index1 on tag(col1)
  • index2 on tag(col2)
  • index3 on tag(col1, col2)
  • index4 on tag(col2, col1)

我们可以对 clo1、col2 建立多个索引,但在 scan index 时,上述四个 index 返回结果存在差异,甚至是完全不同,在实际业务中具体使用哪个 index,及 index 的最优执行策略,则是通过索引优化器决定。

下面我们再来根据刚才 4 个 index 的例子深入分析一波:

lookup on tag where tag.col1 ==1  # 最优的 index 是 index1
lookup on tag where tag.col2 == 2 # 最优的 index 是index2
lookup on tag where tag.col1 > 1 and tag.col2 == 1 
# index3 和 index4 都是有效的 index,而 index1 和 index2 则无效

在上述第三个例子中,index3 和 index4 都是有效 index,但最终必须要从两者中选出来一个作为 index,根据优化规则,因为 tag.col2 == 1 是一个等价查询,因此优先使用 tag.col2 会更高效,所以优化器应该选出 index4 为最优 index。

实操一下图数据库 Nebula Graph 索引

在这部分我们就不具体讲解某个语句的用途是什么了,如果你对语句不清楚的话可以去图数据库 Nebula Graph 的官方论坛进行提问:https://discuss.nebula-graph.io/

CREATE——索引的创建

(user@127.0.0.1:6999) [(none)]> CREATE SPACE my_space(partition_num=3, replica_factor=1);
Execution succeeded (Time spent: 15.566/16.602 ms)

Thu Feb 20 12:46:38 2020

(user@127.0.0.1:6999) [(none)]> USE my_space;
Execution succeeded (Time spent: 7.681/8.303 ms)

Thu Feb 20 12:46:51 2020

(user@127.0.0.1:6999) [my_space]> CREATE TAG lookup_tag_1(col1 string, col2 string, col3 string);
Execution succeeded (Time spent: 12.228/12.931 ms)

Thu Feb 20 12:47:05 2020

(user@127.0.0.1:6999) [my_space]> CREATE TAG INDEX t_index_1 ON lookup_tag_1(col1, col2, col3);
Execution succeeded (Time spent: 1.639/2.271 ms)

Thu Feb 20 12:47:22 2020

DROP——删除索引

(user@127.0.0.1:6999) [my_space]> DROP TAG INDEX t_index_1;
Execution succeeded (Time spent: 4.147/5.192 ms)

Sat Feb 22 11:30:35 2020

REBUILD——重建索引

如果你是从较老版本的 Nebula Graph 升级上来,或者用 Spark Writer 批量写入过程中(为了性能)没有打开索引,那么这些数据还没有建立过索引,这时可以使用 REBUILD INDEX 命令来重新全量建立一次索引。这个过程可能会耗时比较久,在 rebuild index 完成前,客户端的读写速度都会变慢。

REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE]

LOOKUP——使用索引

需要说明一下,使用 LOOKUP 语句前,请确保已经建立过索引(CREATE INDEX 或 REBUILD INDEX)。

(user@127.0.0.1:6999) [my_space]> INSERT VERTEX lookup_tag_1(col1, col2, col3) VALUES 200:("col1_200", "col2_200", "col3_200"),  201:("col1_201", "col2_201", "col3_201"), 202:("col1_202", "col2_202", "col3_202");
Execution succeeded (Time spent: 18.185/19.267 ms)

Thu Feb 20 12:49:44 2020

(user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200";
============
| VertexID |
============
| 200      |
------------
Got 1 rows (Time spent: 12.001/12.64 ms)

Thu Feb 20 12:49:54 2020

(user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200" YIELD lookup_tag_1.col1, lookup_tag_1.col2, lookup_tag_1.col3;
========================================================================
| VertexID | lookup_tag_1.col1 | lookup_tag_1.col2 | lookup_tag_1.col3 |
========================================================================
| 200      | col1_200          | col2_200          | col3_200          |
------------------------------------------------------------------------
Got 1 rows (Time spent: 3.679/4.657 ms)

Thu Feb 20 12:50:36 2020
作者有话说:Hi,我是 bright-starry-sky,是图数据 Nebula Graph 研发工程师,对数据库存储有浓厚的兴趣,希望本次的经验分享能给大家带来帮助,如有不当之处也希望能帮忙纠正,谢谢~
相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
目录
相关文章
|
1天前
|
关系型数据库 OLAP API
非“典型”向量数据库AnalyticDB PostgreSQL及RAG服务实践
本文介绍了非“典型”向量数据库AnalyticDB PostgreSQL及其RAG(检索增强生成)服务的实践应用。 AnalyticDB PostgreSQL不仅具备强大的数据分析能力,还支持向量查询、全文检索和结构化查询的融合,帮助企业高效构建和管理知识库。
35 19
|
12天前
|
数据采集 人工智能 分布式计算
MaxFrame:链接大数据与AI的高效分布式计算框架深度评测与实践!
阿里云推出的MaxFrame是链接大数据与AI的分布式Python计算框架,提供类似Pandas的操作接口和分布式处理能力。本文从部署、功能验证到实际场景全面评测MaxFrame,涵盖分布式Pandas操作、大语言模型数据预处理及企业级应用。结果显示,MaxFrame在处理大规模数据时性能显著提升,代码兼容性强,适合从数据清洗到训练数据生成的全链路场景...
40 5
MaxFrame:链接大数据与AI的高效分布式计算框架深度评测与实践!
|
1月前
|
弹性计算 安全 关系型数据库
活动实践 | 自建数据库迁移到云数据库
通过阿里云RDS,用户可获得稳定、安全的企业级数据库服务,无需担心数据库管理与维护。该方案使用RDS确保数据库的可靠性、可用性和安全性,结合ECS和DTS服务,实现自建数据库平滑迁移到云端,支持WordPress等应用的快速部署与运行。通过一键部署模板,用户能迅速搭建ECS和RDS实例,完成数据迁移及应用上线,显著提升业务灵活性和效率。
|
1月前
|
运维 Kubernetes 调度
阿里云容器服务 ACK One 分布式云容器企业落地实践
阿里云容器服务ACK提供强大的产品能力,支持弹性、调度、可观测、成本治理和安全合规。针对拥有IDC或三方资源的企业,ACK One分布式云容器平台能够有效解决资源管理、多云多集群管理及边缘计算等挑战,实现云上云下统一管理,提升业务效率与稳定性。
|
14天前
|
运维 监控 Cloud Native
云原生之运维监控实践:使用 taosKeeper 与 TDinsight 实现对 时序数据库TDengine 服务的监测告警
在数字化转型的过程中,监控与告警功能的优化对保障系统的稳定运行至关重要。本篇文章是“2024,我想和 TDengine 谈谈”征文活动的三等奖作品之一,详细介绍了如何利用 TDengine、taosKeeper 和 TDinsight 实现对 TDengine 服务的状态监控与告警功能。作者通过容器化安装 TDengine 和 Grafana,演示了如何配置 Grafana 数据源、导入 TDinsight 仪表板、以及如何设置告警规则和通知策略。欢迎大家阅读。
35 0
|
1月前
|
机器学习/深度学习 存储 运维
分布式机器学习系统:设计原理、优化策略与实践经验
本文详细探讨了分布式机器学习系统的发展现状与挑战,重点分析了数据并行、模型并行等核心训练范式,以及参数服务器、优化器等关键组件的设计与实现。文章还深入讨论了混合精度训练、梯度累积、ZeRO优化器等高级特性,旨在提供一套全面的技术解决方案,以应对超大规模模型训练中的计算、存储及通信挑战。
81 4
|
2月前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
77 8
|
2月前
|
关系型数据库 MySQL Linux
Linux环境下MySQL数据库自动定时备份实践
数据库备份是确保数据安全的重要措施。在Linux环境下,实现MySQL数据库的自动定时备份可以通过多种方式完成。本文将介绍如何使用`cron`定时任务和`mysqldump`工具来实现MySQL数据库的每日自动备份。
176 3
|
2月前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第21天】本文探讨了MongoDB Atlas的核心特性、实践应用及对云原生数据库未来的思考。MongoDB Atlas作为MongoDB的云原生版本,提供全球分布式、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了云原生数据库的未来趋势,如架构灵活性、智能化运维和混合云支持,并分享了实施MongoDB Atlas的最佳实践。
|
3月前
|
NoSQL Cloud Native atlas
探索云原生数据库:MongoDB Atlas 的实践与思考
【10月更文挑战第20天】本文探讨了MongoDB Atlas的核心特性、实践应用及对未来云原生数据库的思考。MongoDB Atlas作为云原生数据库服务,具备全球分布、完全托管、弹性伸缩和安全合规等优势,支持快速部署、数据全球化、自动化运维和灵活定价。文章还讨论了实施MongoDB Atlas的最佳实践和职业心得,展望了云原生数据库的发展趋势。