浅谈图数据库

image

本文主要讨论图数据库背后的设计思路、原理还有一些适用的场景,以及在生产环境中使用图数据库的具体案例。node

从社交网络谈起

下面这张图是一个社交网络场景,每一个用户能够发微博、分享微博或评论他人的微博。这些都是最基本的增删改查,也是大多数研发人员对数据库作的常见操做。而在研发人员的平常工做中除了要把用户的基本信息录入数据库外,还需找到与该用户相关联的信息,方便去对单个的用户进行下一步的分析,好比说:咱们发现张三的帐户里有不少关于 AI 和音乐的内容,那么咱们能够据此推测出他多是一名程序员,从而推送他可能感兴趣的内容。git

image

这些数据分析每时每刻都会发生,但有时候,一个简单的数据工做流在实现的时候可能会变得至关复杂,此外数据库性能也会随着数据量的增长而锐减,好比说获取某管理者下属三级汇报关系的员工,这种统计查询在如今的数据分析中是一种常见的操做,而这种操做每每会由于数据库选型致使性能产生巨大差别。程序员

传统数据库的解决思路

传统数据库的概念模型及查询的代码

传统解决上述问题最简单的方法就是创建一个关系模型,咱们能够把每一个员工的信息录入表中,存在诸如 MySQL 之类的关系数据库,下图是最基本的关系模型:github

image

可是基于上述的关系模型,要实现咱们的需求,就不可避免地涉及到不少关系数据库 JOIN 操做,同时实现出来的查询语句也会变得至关长(有时达到上百行):sql

(SELECT T.directReportees AS directReportees, sum(T.count) AS count
FROM (
SELECT manager.pid AS directReportees, 0 AS count
	FROM person_reportee manager
	WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
UNION
	SELECT manager.pid AS directReportees, count(manager.directly_manages) AS count
FROM person_reportee manager
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
UNION
SELECT manager.pid AS directReportees, count(reportee.directly_manages) AS count
FROM person_reportee manager
JOIN person_reportee reportee
ON manager.directly_manages = reportee.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
UNION
SELECT manager.pid AS directReportees, count(L2Reportees.directly_manages) AS count
FROM person_reportee manager
JOIN person_reportee L1Reportees
ON manager.directly_manages = L1Reportees.pid
JOIN person_reportee L2Reportees
ON L1Reportees.directly_manages = L2Reportees.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
) AS T
GROUP BY directReportees)
UNION
(SELECT T.directReportees AS directReportees, sum(T.count) AS count
FROM (
SELECT manager.directly_manages AS directReportees, 0 AS count
FROM person_reportee manager
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
UNION
SELECT reportee.pid AS directReportees, count(reportee.directly_manages) AS count
FROM person_reportee manager
JOIN person_reportee reportee
ON manager.directly_manages = reportee.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
UNION
SELECT depth1Reportees.pid AS directReportees,
count(depth2Reportees.directly_manages) AS count
FROM person_reportee manager
JOIN person_reportee L1Reportees
ON manager.directly_manages = L1Reportees.pid
JOIN person_reportee L2Reportees
ON L1Reportees.directly_manages = L2Reportees.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
) AS T
GROUP BY directReportees)
UNION
(SELECT T.directReportees AS directReportees, sum(T.count) AS count
	FROM(
	SELECT reportee.directly_manages AS directReportees, 0 AS count
FROM person_reportee manager
JOIN person_reportee reportee
ON manager.directly_manages = reportee.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
UNION
SELECT L2Reportees.pid AS directReportees, count(L2Reportees.directly_manages) AS
count
FROM person_reportee manager
JOIN person_reportee L1Reportees
ON manager.directly_manages = L1Reportees.pid
JOIN person_reportee L2Reportees
ON L1Reportees.directly_manages = L2Reportees.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
GROUP BY directReportees
) AS T
GROUP BY directReportees)
UNION
(SELECT L2Reportees.directly_manages AS directReportees, 0 AS count
FROM person_reportee manager
JOIN person_reportee L1Reportees
ON manager.directly_manages = L1Reportees.pid
JOIN person_reportee L2Reportees
ON L1Reportees.directly_manages = L2Reportees.pid
WHERE manager.pid = (SELECT id FROM person WHERE name = "fName lName")
)

这种 glue 代码对维护人员和开发者来讲就是一场灾难,没有人想写或者去调试这种代码,此外,这类代码每每伴随着严重的性能问题,这个在以后会详细讨论。数据库

传统关系数据库的性能问题

性能问题的本质在于数据分析面临的数据量,假如只查询几十个节点或者更少的内容,这种操做是彻底不须要考虑数据库性能优化的,但当节点数据从几百个变成几百万个甚至几千万个后,数据库性能就成为了整个产品设计的过程当中最需考虑的因素之一。设计模式

随着节点的增多,用户跟用户间的关系,用户和产品间的关系,或者产品和产品间的关系都会呈指数增加。跨域

如下是一些公开的数据,能够反映数据、数据和数据间关系的一些实际状况:缓存

  • 推特:用户量为 5 亿,用户之间存在关注、点赞关系
  • 亚马逊:用户量 1.2 亿,用户和产品间存在购买关系
  • AT&T(美国三大运营商之一): 1 亿个号码,电话号码间可创建通话关系

以下表所示,开源的图数据集每每有着上千万个节点和上亿的边的数据:性能优化

Data set name nodes edges
YahooWeb 1.4 Billion 6 Billion
Symantec Machine-File Graph 1 Billion 37 Billion
Twitter 104 Million 3.7 Billion
Phone call network 30 Million 260 Million

在数据量这么大的场景中,使用传统 SQL 会产生很大的性能问题,缘由主要有两个:

  1. 大量 JOIN 操做带来的开销:以前的查询语句使用了大量的 JOIN 操做来找到须要的结果。而大量的 JOIN 操做在数据量很大时会有巨大的性能损失,由于数据自己是被存放在指定的地方,查询自己只须要用到部分数据,可是 JOIN 操做自己会遍历整个数据库,这样就会致使查询效率低到让人没法接受。
  2. 反向查询带来的开销:查询单个经理的下属不须要多少开销,可是若是咱们要去反向查询一个员工的老板,使用表结构,开销就会变得很是大。表结构设计得不合理,会对后续的分析、推荐系统产生性能上的影响。好比,当关系从_老板 -> 员工 _变成 用户 -> 产品,若是不支持反向查询,推荐系统的实时性就会大打折扣,进而带来经济损失。

下表列出的是一个非官方的性能测试(社交网络测试集,一百万用户,每一个大概有 50 个好友),体现了在关系数据库里,随着好友查询深度的增长而产生的性能变化:

levels RDBMS execution time(s)
2 0.016
3 30.267
4 1543.595

传统数据库的常规优化策略

策略一:索引

索引:SQL 引擎经过索引来找到对应的数据。

常见的索引包括 B- 树索引和哈希索引,创建表的索引是比较常规的优化 SQL 性能的操做。B- 树索引简单地来讲就是给每一个人一个可排序的独立 ID,B- 树自己是一个平衡多叉搜索树,这个树会将每一个元素按照索引 ID 进行排序,从而支持范围查找,范围查找的复杂度是 O(logN)  ,其中 N 是索引的文件数目。

可是索引并不能解决全部的问题,若是文件更新频繁或者有不少重复的元素,就会致使很大的空间损耗,此外索引的 IO 消耗也值得考虑,索引 IO 尤为是在机械硬盘上的 IO 读写性能上来讲很是不理想,常规的 B- 树索引消耗四次 IO 随机读,当 JOIN 操做变得愈来愈多时,硬盘查找更可能发生上百次。

策略二:缓存

缓存:缓存主要是为了解决有具备空间或者时间局域性数据的频繁读取带来的性能优化问题。一个比较常见的使用缓存的架构是 lookaside cache architecture。下图是以前 Facebook 用 Memcached  + MySQL 的实例(现已被 Facebook 自研的图数据库 TAO 替代):

image

在架构中,设计者假设用户创造的内容比用户读取的内容要少得多,Memcached 能够简单地理解成一个分布式的支持增删改查的哈希表,支持上亿量级的用户请求。基本的使用流程是当客户端需读数据时,先查看一下缓存,而后再去查询 SQL 数据库。而当用户须要写入数据时,客户端先删除缓存中的 key,让数据过时,再去更新数据库。可是这种架构有几个问题:

  • 首先,键值缓存对于图结构数据并非一个好的操做语句,每次查询一条边,须要从缓存里把节点对应的边所有拿出来;此外,当更新一条边,原来的全部依赖边要被删除,继而须要从新加载全部对应边的数据,这些都是并发的性能瓶颈,毕竟实际场景中一个点每每伴随着几千条边,这种操做带来的时间、内存消耗问题不可忽视。
  • 其次,数据更新到数据读取有一个过程,在上面架构中这个过程须要主从数据库跨域通讯。原始模型使用了一个外部标识来记录过时的键值对,而且异步地把这些读取的请求从只读的从节点传递到主节点,这个须要跨域通讯,延迟相比直接从本地读大了不少。(相似从以前须要走几百米的距离而如今须要走从北京到深圳的距离)

使用图结构建模

上述关系型数据库建模失败的主要缘由在于数据间缺少内在的关联性,针对这类问题,更好的建模方式是使用图结构。 假如数据自己就是表格的结构,关系数据库就能够解决问题,但若是你要展现的是数据与数据间的关系,关系数据库反而不能解决问题了,这主要是在查询的过程当中不可避免的大量 JOIN 操做致使的,而每次 JOIN 操做却只用到部分数据,既然反复 JOIN 操做自己会致使大量的性能损失,如何建模才能更好的解决问题呢?答案在点和点之间的关系上。

点、关联关系和图数据模型

在咱们以前的讨论中,传统数据库虽然运用 JOIN 操做把不一样的表连接了起来,从而隐式地表达了数据之间的关系,可是当咱们要经过 A 管理 B,B 管理 A 的方式查询结果时,表结构并不能直接告诉咱们结果。 若是咱们想在作查询前就知道对应的查询结果,咱们必须先定义节点和关系。

节点和关系先定义是图数据库和别的数据库的核心区别。打个比方,咱们能够把经理、员工表示成不一样的节点,并用一条边来表明他们以前存在的管理关系,或者把用户和商品看做节点,用购买关系建模等等。而当咱们须要新的节点和关系时,只需进行几回更新就好,而不用去改变表的结构或者去迁移数据。

根据节点和关联关系,以前的数据能够根据下图所示建模:

image

经过图数据库 Nebula Graph 原生 nGQL 图查询语言进行建模,参考以下操做:

-- Insert People
INSERT VERTEX person(ID, name) VALUES 1:(2020031601, ‘Jeff’);
INSERT VERTEX person(ID, name) VALUES 2:(2020031602, ‘A’);
INSERT VERTEX person(ID, name) VALUES 3:(2020031603, ‘B’);
INSERT VERTEX person(ID, name) VALUES 4:(2020031604, ‘C’);

-- Insert edge
INSERT EDGE manage (level_s, level_end) VALUES 1 -> 2: ('0', '1')
INSERT EDGE manage (level_s, level_end) VALUES 1 -> 3: ('0', '1')
INSERT EDGE manage (level_s, level_end) VALUES 1 -> 4: ('0', '1')

而以前超长的 query 语句也能够经过 Cypher / nGQL 缩减成短短的 三、4 行代码。

下面为 nGQL 语句

GO FROM 1 OVER manage YIELD manage.level_s as start_level, manage._dst AS personid
| GO FROM $personid OVER manage where manage.level_s < start_level + 3
YIELD SUM($$.person.id) AS TOTAL, $$.person.name AS list

下面为 Cypher 版本

MATCH (boss)-[:MANAGES*0..3]->(sub),
(sub)-[:MANAGES*1..3]->(personid)
WHERE boss.name = “Jeff”
RETURN sub.name AS list, count(personid) AS Total

从近百行代码变成 三、4 行代码能够明显地看出图数据库在数据表达能力上的优点。

图数据库性能优化

图数据库自己对高度链接、结构性不强的数据作了专门优化。不一样的图数据库根据不一样的场景也作了针对性优化,笔者在这里简单介绍如下几种图数据库,BTW,这些图数据库都支持原生图建模。

Neo4j

Neo4j 是最知名的一种图数据库,在业界有微软、ebay 在用 Neo4j 来解决部分业务场景,Neo4j 的性能优化有两点,一个是原生图数据处理上的优化,一个是运用了 LRU-K 缓存来缓存数据。

原生图数据处理优化

咱们说一个图数据库支持原生图数据处理就表明这个数据库有能力去支持 index-free adjacency

index-free adjancency 就是每一个节点会保留链接节点的引用,从而这个节点自己就是链接节点的一个索引,这种操做的性能比使用全局索引好不少,同时假如咱们根据图来进行查询,这种查询是与整个图的大小无关的,只与查询节点关联边的数目有关,若是用 B 树索引进行查询的复杂度是 O(logN),使用这种结构查询的复杂度就是 O(1)。当咱们要查询多层数据时,查询所须要的时间也不会随着数据集的变大而呈现指数增加,反而会是一个比较稳定的常数,毕竟每次查询只会根据对应的节点找到链接的边而不会去遍历全部的节点。

主存缓存优化

在 2.2 版本的 Neo4j 中使用了 LRU-K 缓存,这种缓存简而言之就是将使用频率最低的页面从缓存中弹出,青睐使用频率更高的页面,这种设计保证在统计意义上的缓存资源使用最优化。

JanusGraph

JanusGraph 自己并无关注于去实现存储和分析,而是实现了图数据库引擎与多种索引和存储引擎的接口,利用这些接口来实现数据和存储和索引。JanusGraph 主要目的是在原来框架的基础上支持图数据的建模同时优化图数据序列化、图数据建模、图数据执行相关的细节。JanusGraph 提供了模块化的数据持久化、数据索引和客户端的接口,从而更方便地将图数据模型运用到实际开发中。

此外,JanusGraph 支持用 Cassandra、HBase、BerkelyDB 做为存储引擎,支持使用 ElasticSearch、Solr 还有 Lucene 进行数据索引。 在应用方面,能够用两种方式与 JanusGraph 进行交互:

  • 将 JanusGraph 变成应用的一部分进行查询、缓存,而且这些数据交互都是在同一台 JVM 上执行,但数据的来源可能在本地或者在别的地方。
  • 将 JanusGraph 做为一个服务,让客户端与服务端分离,同时客户端提交 Gremlin 查询语句到服务器上执行对应的数据处理操做。

image

Nebula Graph

下面简单地介绍了一下 Nebula Graph 的系统设计。

使用 KV 对来进行图数据处理

Nebula Graph 使用了 vertexID + TagID 做为键在不一样的 partition 间存储 in-key 和 out-key 相关的数据,这种操做能够确保在大规模集群上的高可用,使用分布式的 partition 和 sharding 也增长了 Nebula Graph 的吞吐量和容错的能力。

Shared-noting 分布式存储层

Storage Service 采用 shared-nothing 的分布式架构设计,每一个存储节点都有多个本地 KV 存储实例做为物理存储。Nebula 采用多数派协议 Raft 来保证这些 KV 存储之间的一致性(因为 Raft 比 Paxo 更简洁,咱们选用了 Raft)。在 KVStore 之上是图语义层,用于将图操做转换为下层 KV 操做。 图数据(点和边)经过 Hash 的方式存储在不一样 partition 中。这里用的 Hash 函数实现很直接,即 vertex_id 取余 partition 数。在 Nebula Graph 中,partition 表示一个虚拟的数据集,这些 partition 分布在全部的存储节点,分布信息存储在 Meta Service 中(所以全部的存储节点和计算节点都能获取到这个分布信息)。

无状态计算层

每一个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通讯关系。计算节点仅从 Meta Service 读取 meta 信息,以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。 计算层的负载均衡有两种形式,最多见的方式是在计算层上加一个负载均衡(balance),第二种方法是将计算层全部节点的 IP 地址配置在客户端中,这样客户端能够随机选取计算节点进行链接。 每一个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。

图数据库是当今的趋势

在当今,图数据库收到了更多分析师和咨询公司的关注

Graph analysis is possibly the single most effective  competitive differentiator for organizations  pursuing data-driven operations and  decisions after the design of data capture.        --------------Gartner

“Graph analysis is the true killer app for Big Data.”      --------------------Forrester

同时图数据库在 DB-Ranking 上的排名也呈现出上升最快的趋势,可见需求之迫切:

image

图数据库实践:不只仅是社交网络

Netflix 云数据库的工程实践

image

Netflix 采用了JanusGraph + Cassandra + ElasticSearch 做为自身的图数据库架构,他们运用这种架构来作数字资产管理。 节点表示数字产品好比电影、纪录片等,同时这些产品之间的关系就是节点间的边。 当前的 Netflix 有大概 2 亿的节点,70 多种数字产品,每分钟都有上百条的 query 和数据更新。 此外,Netflix 也把图数据库运用在了受权、分布式追踪、可视化工做流上。好比可视化 Git 的 commit,jenkins 部署这些工做。

Adobe 的技术迭代

通常而言,新技术每每在开始的时候大都不被大公司所青睐,图数据库并无例外,大公司自己有不少的遗留项目,而这些项目自己的用户体量和使用需求又让这些公司不敢冒着风险来使用新技术去改变这些处于稳定的产品。Adobe 在这里作了一个迭代新技术的例子,用 Neo4j 图数据库替换了旧的 NoSQL Cassandra 数据库。

这个被大改的系统名字叫 Behance,是 Adobe 在 15 年发布的一个内容社交平台,有大概 1 千万的用户,在这里人们能够分享本身的创做给百万人看。

image

这样一个巨大的遗留系统原本是经过 Cassandra 和 MongoDB 搭建的,基于历史遗留问题,系统有很多的性能瓶颈不得不解决。 MongoDB 和 Cassandra 的读取性能慢主要由于原先的系统设计采用了 fan-out 的设计模式——受关注多的用户发表的内容会单独分发给每一个读者,这种设计模式也致使了网络架构的大延迟,此外 Cassandra 自己的运维也须要不小的技术团队,这也是一个很大的问题。

image

在这里为了搭建一个灵活、高效、稳定的系统来提供消息 feeding 并最小化数据存储的规模,Adobe 决定迁移本来的 Cassandra 数据库到 Neo4j 图数据库。 在 Neo4j 图数据库中采用一种所谓的 Tiered relationships 来表示用户之间的关系,这个边的关系能够去定义不一样的访问状态,好比:仅部分用户可见,仅关注者可见这些基本操做。 数据模型如图所示

image

使用这种数据模型并使用 Leader-follower 架构来优化读写,这个平台得到了巨大的性能提高:

image

  1. 运维需求的时长在使用了 Neo4j 之后降低了 300%。
  2. 存储需求下降了 1000 倍, Neo4j 仅需 50G 存储数据, 而 Cassandra 须要 50TB。
  3. 仅仅须要 3 个服务实例就能够支持整个服务器的流畅运行,以前则须要 48 个。
  4. 图数据库自己就提供了更高的可扩展性。

结论

在当今的大数据时代,采用图数据库能够用小成本在原有架构上得到巨大的性能提高。图数据库不只仅能够在 5G、AI、物联网领域发挥巨大的推进做用,同时也能够用来重构本来的遗留系统。 虽然不一样的图数据库可能有着大相径庭的底层实现,但这些都彻底支持用图的方式来构建数据模型从而让不一样的组件之间相互联系,从咱们以前的讨论来看,这一种数据模型层次的改变会极大地简化不少平常数据系统中所面临的问题,增大系统的吞吐量而且下降运维的需求。

图数据库的介绍就到此为止了,若是你对图数据库 Nebula Graph 有任何想法或其余要求,欢迎去 GitHub:https://github.com/vesoft-inc/nebula issue 区向咱们提 issue 或者前往官方论坛:https://discuss.nebula-graph.io/Feedback 分类下提建议 👏;加入 Nebula Graph 交流群,请联系 Nebula Graph 官方小助手微信号:NebulaGraphbot

Reference

推荐阅读

做者有话说:Hi,我是 Johhan。目前在 Nebula Graph 实习,研究和实现大型图数据库查询引擎和存储引擎组件。做为一个图数据库及开源爱好者,我在博客分享有关数据库、分布式系统和 AI 公开可用学习资源。

相关文章
相关标签/搜索