【波克城市】基于图数据库Nebula Graph搭建数据血缘系统

一、业务背景
随着公司的持续发展和扩张,所涉及的项目日益增多,相应的数据积累也在持续上升。在这一过程中,数据表及其需要计算的数据指标数量也在急剧增加,数据结构变得愈加复杂和杂乱。

为了应对这一挑战,建立一个有效的数据血缘系统显得尤为必要。数据血缘系统能够追踪数据的来源、流向以及其间的变化过程,这对于维护数据质量、确保数据的可追溯性和透明性至关重要。通过构建数据血缘系统,我们能够实现对数据流转全过程的监控和管理,从而提高数据治理的效率和精确度。

首先,数据血缘系统的建设有助于清晰地识别数据之间的依赖关系和影响范围。在数据出现问题时,可以快速定位问题源头,有效减少错误诊断和修复的时间,保证业务的连续性和数据的准确性。其次,数据血缘分析为数据管理提供了透明度,使得数据的利用更加合规,尤其是在遵守日益严格的数据保护法规(如GDPR或CCPA)的背景下。

此外,数据血缘系统通过详细记录数据的每一次处理和传输,增强了数据安全性,让我们能够对潜在的数据泄露和非授权访问进行有效预防。通过这种方式,数据血缘系统不仅提升了数据的可用性和可靠性,也增强了企业对数据资产的掌控能力,从而支撑了数据驱动决策的实施,提升了企业的竞争力。

二、技术选型
在深入的技术选型过程中,我们主要对比了两款领先的图数据库:Neo4j和Nebula Graph。两者都采用属性图数据模型,允许我们存储丰富的实体属性和关系信息。这种模型特别适合用于表现和查询复杂的数据关系,这是数据血缘追踪所必需的。

Neo4j作为一款老牌图数据库,提供了强大的社区支持和成熟的技术生态,其查询语言Cypher也被广泛认可和使用。然而,Neo4j在大规模数据处理和分布式扩展性方面存在一定的局限性。尽管最新的版本有所改进,但在面对极大规模数据时,其性能可能会受到影响。

相比之下,Nebula Graph作为一款新兴的开源的分布式图数据库,展现出了在大规模数据集上的卓越性能。Nebula Graph采用shared-nothing架构,提供了在线水平扩缩容的能力,这使得它在处理千亿节点和万亿条边的超大规模图时仍能保持低延迟的查询响应。此外,Nebula Graph的兼容性设计,如支持openCypher查询语言,使得在这方面的学习成本大大降低。

在综合考虑了性能、后续的扩展性、易用性以及成本效益之后,我们得出结论:Nebula Graph更适合我们当前及未来的业务需求。Nebula Graph不仅在技术层面满足了我们对大规模图数据处理的需要,而且在成本控制和团队技能转移方面也显示出了明显的优势。因此,我们选择Nebula Graph作为构建数据血缘系统的理想图数据库解决方案。

三、踩坑总结
3.1 环境搭建阶段

问题:搭完环境尝试执行insert语句时报错:Error: Storage Error: RPC failure, probably timeout.
检查集群中各节点的状态,未发现任何异常


在每个节点上分别查看Nebula Graph的运行状态,发现有两个节点的服务没有正常启动。


进一步查看报错的执行日志,发现是接口被占用了,但netstat命令查看 占用接口的进程就是nebula。


解决方案
手动终止进程,再重启Nebula服务,数据可以正常写入,并且各个节点状态正常。
因为我这个Nebula环境是卸载后重装的,推测是在卸载之前,原来的进程没有杀干净,导致占用了接口,新安装的无法正常启动。
Nebula Graph一定要每个节点逐个关闭并卸载。

3.2 数据写入阶段

  • 问题1:VID问题

在Nebula Graph中,点的唯一标识VID,支持两种数据类型,分别是定长字符串FIXED_STRING()和INT64。其中整数VID通常比字符串类型更高效,因为整数的处理速度更快,占用空间更少。在本数据血缘项目中,由于是表级别的数据血缘,所以是 库名+表名 唯一确定一张表,很明显这是字符串类型。

在测试环境下,由于预装了Spark环境,所以考虑借助Nebula Graph提供的Spark连接器完成数据写入。对于VID问题,由于希望实现幂等写入,因此不考虑雪花ID等实现方案,而是考虑借助Java中自带的hashCode()方法生成VID。


上述方案在测试环境中能行得通,但是在正式环境下却无法实现,正式环境下并没有预装的Spark环境,因而在正式环境下,考虑使用Python连接器,拼接insert语句,批量将数据写入图库。最开始考虑与Spark环境下相同的方案生成VID,但经测试发现,Python中的生成Hash ID的方法多次执行,生成的Hash ID不一样(Python 3.3及以上版本引入了hash随机化机制)。
考虑仿照Java中hashCode的生成方式在Python中实现类似功能:

def hashCode(value):
    h = 0
    for c in value:
        h = (31 * h + ord(c)) & 0xFFFFFFFF
    return h if h < 0x80000000 else h - 0x100000000

但执行上述代码发现,在Python中并没有类似Java中的截断机制,以上面生成的ID作为VID写入图库中时,会出现超出数据范围的错误。
最终,在正式环境下,将VID类型修改为定长字符串FIXED_STRING(128),以 库名+表名 的拼接作为VID。
image

  • 问题2:SQL拼接问题
    使用拼接insert语句的方式进行数据写入时,最为关键的一环就是SQL语句的生成。在本系统中表的生成SQL将作为节点属性存入图库中,由于SQL中本身就会有一些特殊符号,如, " ’ \ / $等等,这些特殊结构在insert语句的拼接过程中需要格外注意【PS:Nebula Graph以后要是能多加一个text类型就好了,可以忽略字符串中本身存在的一些特殊格式,防止出现一些意外的错误】

并且一些空值也要注意,Python中的空值用None表示而在Nebula Graph则使用NULL表示,如果不进行转换,对于字符串类型的字段还是可以正常写入的,但是对于数字类型的则会报错。

一些特殊格式如datetime类型,不能以字符串格式直接写入,需要在写入时增加手动转换操作​。

3.3 数据查询阶段
问题:后端反应图库查询速度慢
优化方案​:增加针对常用查询字段的联合索引+优化查询语句,把多条查询压缩为一条查询

最有效的优化方案​:增加针对常用查询字段的联合索引,增加之后查询速度提高了100多倍!!!

CREATE TAG INDEX IF NOT EXISTS database_index_0 on table_node(database_name(15), table_name(20));
​
REBUILD TAG INDEX database_index_0;


速度提升非常明显!!!

优化nGQL的写法,将多次查询优化为一条,如查询当前节点、当前节点的上游以及当前节点的下游,原来的三个查询可以压缩为一条查询,有效提高查询速度。

MATCH
  // 查询指定节点及其属性
  (n:table_node{database_name:"byhls_ods",table_name:"app_login_log"}) AS target_node,
​
  // 查询该节点的所有上游节点(边的起始点)
  (upstream:table_node)-[:has_consanguinity]->(n) AS upstream_nodes,
​
  // 查询该节点的所有下游节点(边的终点)
  (n)-[:has_consanguinity]->(downstream:table_node) AS downstream_nodes
​
RETURN
  target_node.database_name AS database_name,
  target_node.table_name AS table_name,
  COLLECT(DISTINCT upstream) AS upstream_nodes,
  COLLECT(DISTINCT downstream) AS downstream_nodes

四、总结反思
目前项目处在起步阶段,很多功能还没有加,后续考虑应用Nebula Graph中自带的图计算功能,增加最优路径检索,核心度计算等功能,充分挖掘Nebula Graph的潜力​。​

6 个赞

感谢分享 :clap:
感觉游戏的数据管理是个很有意思的事情,游戏世界就是一个完整的小世界

2 个赞

wow~很精彩的文章 看来作者大大可以坐等黑神话啦:smile: