图数据结构天然适配风控场景的关联关系建模------------将用户、商户、设备、账户等实体抽象为 "点",注册登录、交易支付、设备绑定等行为抽象为 "边",可直观呈现复杂风险网络,而随着互联网与金融业务发展,风控图谱已突破千亿顶点、万亿边规模,且需支持毫秒级多跳查询、灵活关联遍历等高频需求,传统关系型数据库的多表 join 操作在多跳查询时性能呈指数级衰减,NoSQL 数据库缺乏原生图语义支持,传统图数据库难以兼顾超大规模存储与高并发查询,这一痛点催生了高性能分布式图数据库的需求,Nebula Graph 凭借存算分离、Shared-nothing 分布式存储、Raft 一致性协议等核心设计,可实现千亿级数据高效存储与毫秒级查询响应,本文将从架构原理、存储设计、查询流程等维度,对其核心技术设计进行初步介绍与梳理。
一. 整体架构设计

一个完整的 Nebula Graph 部署集群包含三个服务,即 Query Service,Storage Service 和 Meta Service。每个服务都有其各自的可执行二进制文件,这些二进制文件既可以部署在同一组节点上,也可以部署在不同的节点上。
1.1 Meta Service:集群的 "大脑与配置中心"
Meta Service 作为 Nebula Graph 集群的核心管控组件,承担着全集群元数据管理与运维调度的双重职责,是保障集群稳定运行的 "大脑与配置中心"。其管理的元数据覆盖四大核心维度:图空间(Space)的全局配置(如 Partition 数量、副本数)、Schema 定义(包括点 / 边类型的结构、属性数据类型及约束)、用户及角色的权限矩阵(如读写权限、Space 访问权限),以及集群拓扑与数据分布信息(如 Partition 与 StorageD 节点的映射关系、Raft Group 的 Leader/Follower 状态)。
在高可用设计上,Meta Service 基于 Raft 共识协议实现数据同步与故障转移,因此通常要求部署奇数个节点(推荐 3 或 5 个),确保在部分节点宕机时仍能维持集群元数据的一致性与服务可用性。从集群协作流程来看,所有元数据变更操作(如创建 Space、新增 Storage 节点、修改 Schema)均需通过 Meta Service 发起并同步至全集群;而 GraphD(计算节点)与 StorageD(存储节点)在启动时,会主动向 Meta Service 注册节点信息,并实时拉取最新的集群状态(如 Partition 分布、Leader 位置),确保计算层与存储层的协同一致性。
1.2 存算分离架构
架构图中 Meta Service 左侧为 Nebula Graph 的核心服务集群,其采用存储与计算分离的经典架构设计(以虚线为界,上层为计算层,下层为存储层)。这种架构通过解耦数据存储与计算任务,带来三大核心价值,且各优势层层递进:
- 独立弹性伸缩:计算层(Query Service)的负载特征为高并发、短查询,存储层(Storage Service)则侧重海量数据持久化与 I/O 密集型操作,两者资源需求差异显著。存算分离允许单独对计算层或存储层进行扩容 / 缩容 ------ 例如业务高峰期仅增加 GraphD 节点应对查询压力,数据量激增时单独扩展 StorageD 节点提升存储能力,无需整体扩容,大幅降低资源浪费。
- 无限水平扩展:得益于计算层无状态设计与存储层 Shared-nothing 分布式架构,存算分离彻底突破单机硬件资源限制。集群可通过持续增加计算节点或存储节点,实现查询吞吐量与数据存储容量的线性增长,轻松支撑从亿级到千亿级数据规模的平滑扩展。
- 多计算引擎适配:存储层(Storage Service)抽象为统一的数据服务层,脱离了对单一计算层的依赖。除了优先支撑 Query Service 处理实时查询请求外,还可灵活对接各类计算引擎 ------ 例如适配 Spark/Flink 等迭代计算框架处理离线批处理任务,或对接机器学习引擎实现图算法训练,让存储资源复用率最大化,构建更灵活的计算生态。
1.3 无状态计算层(GraphD)
现在我们来看下计算层,每个计算节点都运行着一个无状态的查询计算引擎,而节点彼此间无任何通信关系 。计算节点仅从 Meta Service 读取元数据信息,以及和 Storage Service 进行交互。这样设计使得计算层集群更容易使用 K8s 管理或部署在云上。
计算层的负载均衡有两种形式,最常见的方式是在计算层上加一个负载均衡,第二种方法是将计算层所有节点的 IP 地址配置在客户端中,这样客户端可以随机选取计算节点进行连接。
每个查询计算引擎都能接收客户端的请求,解析查询语句,生成抽象语法树(AST)并将 AST 传递给执行计划器和优化器,最后再交由执行器执行。
1.4 Shared-nothing 分布式存储层(StorageD)
Storage Service 采用 shared-nothing 的分布式架构设计,每个存储节点都有一个或多个本地 KV 存储实例(默认是RocksDB)作为物理存储。Nebula 采用多数派协议 Raft 来保证这些 KV 存储之间的一致性(由于 Raft 比 Paxo 更简洁,Nebula Graph 选用了 Raft)。在 KVStore 之上是图语义层,用于将图操作转换为下层 KV 操作。
图数据(点和边)是通过 Hash 的方式存储在不同 Partition 中。这里用的 Hash 函数实现很直接,即 vertex_id 取余 Partition 数。在 Nebula Graph 中,Partition 表示一个虚拟的数据集,这些 Partition 分布在所有的存储节点,分布信息存储在 Meta Service 中(因此所有的存储节点和计算节点都能获取到这个分布信息)。
1.5 核心特性
Nebula Graph 基于 C++ 实现,架构设计支持存储千亿顶点、万亿边,并提供毫秒级别的查询延时。我们在 3 台 48U192G 物理机搭建的集群上灌入 10 亿美食图谱数据对 Nebula Graph 的功能进行了验证。
- 一跳查询 TP99 延时在 5ms 内,两跳查询 TP99 延时在 20ms 内,一般的多跳查询 TP99 延时在百毫秒内。
- 集群在线写入速率约为20万 Records/s。
- 支持通过 Spark 任务离线生成 RocksDB 底层 SST File,直接将数据文件载入到集群中,即类似 HBase BulkLoad 能力。
- 提供了类 SQL 查询语言,对于新增的业务需求,只需构造 Nebula Graph SQL 语句,易于理解且能满足各类复杂查询要求。
- 提供联合索引、GEO 索引,可通过实体属性或者关系属性查询实体、关系,或者查询在某个经纬度附近 N 米内的实体。
- 一个 Nebula 集群中可以创建多个 Space (概念类似 MySQL 的DataBase),并且不同 Space 中的数据在物理上是隔离的。
二. 存储设计
2.1 Partition:逻辑分片的基本单位
在 Nebula Graph 中,Partition 是图数据水平分片的最小逻辑单元 。每个图空间(Space)在创建时需指定 Partition 的数量(例如 100、1000 等),该数值一旦设定便不可更改。所有点(Vertex)和边(Edge)根据其 Vertex ID 通过哈希函数映射到具体的 Partition:
PartitionID = hash(VertexID) % PartitionCount
这一机制确保了:
- 同一个顶点的所有属性(Tag)和出边(Outgoing Edges)必然落在同一个 Partition 中;
- 查询邻接关系时,无需跨 Partition 拉取数据,极大提升了遍历效率;
- 数据分布尽可能均匀(前提是 Vertex ID 具备良好散列性)。
📌 注意:Nebula 默认仅存储出边。若需高效查询入边(In-Edges),可显式创建反向边(Reverse Edge),此时反向边将根据目标顶点的 ID 被分配到其所属的 Partition。
2.2 Partition 与 Storage Service 的物理映射
虽然 Partition 是逻辑概念,但它必须部署在物理的 Storage Service(StorageD)节点 上。Meta Service 负责维护全局的 Partition 分布表,记录每个 Partition 的副本位于哪些 StorageD 节点。
- 每个 Partition 可配置多个副本(通常为 3 副本),组成一个 Raft Group;
- Raft 协议在副本间选举 Leader,所有读写请求均由 Leader 处理,并通过日志复制同步到 Follower;
- 当某 StorageD 节点宕机,Meta Service 会触发自动故障转移(Failover),从存活副本中重新选举 Leader,保障服务连续性。
值得注意的是:一个 StorageD 节点通常托管多个 Partition(包括主副本和从副本),而非"一个 Partition 对应一个进程或文件"。
2.3 底层存储引擎:共享 RocksDB 实例
Nebula Graph 的 Storage Service 底层使用 RocksDB 作为本地 KV 存储引擎 。每个 StorageD 节点默认配置下仅运行一个 RocksDB 实例。那Storage Service 如何实现多 Partition 共存?
答案在于 Key 编码设计。Nebula 将 Partition ID 作为所有 Key 的前缀,使得不同 Partition 的数据在同一个 RocksDB 中逻辑隔离。典型 Key 结构如下:
-
点(Vertex)属性 Key:

Type:1 个字节,用来表示 key 类型,当前的类型有 data, index, system 等Part ID:3 个字节,用来表示数据分片 Partition,此字段主要用于 Partition 重新分布(balance) 时方便根据前缀扫描整个 Partition 数据Vertex ID:8 个字节, 用来表示点的 IDTag ID:4 个字节, 用来表示关联的某个 tagTimestamp:8 个字节,对用户不可见,未来实现分布式事务(MVCC)时使用
-
边(Edge)Key(出边):

Type:1 个字节,用来表示 key 的类型,当前的类型有 data, index, system 等。Part ID:3 个字节,用来表示数据分片 Partition,此字段主要用于 Partition 重新分布(通过 BALANCE 命令)时方便根据前缀扫描整个 Partition 数据Vertex ID:8 个字节, 出边里面用来表示源点的 ID, 入边里面表示目标点的 ID。Edge Type:4 个字节, 用来表示这条边的类型,如果大于 0 表示出边,小于 0 表示入边。Rank:8 个字节,用来处理同一种类型的边存在多条的情况。用户可以根据自己的需求进行设置,这个字段可存放交易时间、交易流水号、或某个排序权重(比如,定义一个 edge type "转账",用户 A 可能多次转账给 B, 所以 Nebula 又增加了一个 Rank 字段来做区分,表示 A 到 B 之间多次转账记录。)Vertex ID:8 个字节,出边里面用来表示目标点的 ID, 入边里面表示源点的 ID。Timestamp:8 个字节,对用户不可见,未来实现分布式做事务的时候使用。
由于 RocksDB 底层使用 LSM-Tree(Log-Structured Merge-Tree) 作为其核心存储结构,它将所有写入先缓存在内存中的有序结构(MemTable),再批量顺序刷入磁盘形成不可变的 SSTable 文件,并通过后台 Compaction 合并多层文件以维持读取效率。这种设计天然支持高效的范围查询和前缀扫描(Prefix Seek) ------因为 SSTable 中的键是全局有序的。因此,StorageD 可以快速定位某个 Partition 内的所有数据(只需指定 Type + Part ID 作为前缀),或高效遍历某顶点的所有出边,从而在单个 RocksDB 实例中实现多个 Partition 的高性能共存与隔离。
✅ 优势:
- 避免为成百上千个 Partition 启动独立 RocksDB,节省内存与文件句柄;
- 全局 Compaction、WAL、缓存策略更易优化;
- 利用 RocksDB 的前缀 Bloom Filter 加速 Partition 内查询。
虽然在默认配置下,一个 StorageD 实例仅使用一个 RocksDB 实例,但这并不意味着 Nebula Graph 限制为只能使用单一 RocksDB。实际上,Nebula Graph 支持 多 RocksDB 实例 的设计:每个配置的 data_path 路径对应一个独立的 RocksDB 实例。
这一机制充分考虑了现代服务器通常配备多块物理磁盘的硬件特性。通过在 data_path 中指定多个存储路径(以逗号分隔),Nebula Graph 会为每个路径启动一个独立的 RocksDB 实例。这种设计不仅能够提升 I/O 并行度 ,还能更高效地利用本地存储资源,从而显著增强系统的整体吞吐能力和写入性能。
例如,若配置如下:
data_path=/disk1/nebula/data,/disk2/nebula/data,/disk3/nebula/data
则 StorageD 将分别在三块磁盘上管理三个 RocksDB 实例,Partition 数据会按策略分布到这些实例中,实现物理层面的 I/O 负载分散。
Storage 服务配置 - NebulaGraph Database 手册
2.4 图数据的 KV 映射逻辑
Nebula 将图模型"拍平"为一系列 KV 对,以支持高效的随机读写和范围扫描。
2.4.1 点的存储
每个顶点可拥有多个 Tag(标签),每个 Tag 定义一组属性。例如,顶点 "user101" 可同时具有 Person 和 Employee 两个 Tag。
- 每个 Tag 对应一个独立的 KV 条目;
- Value 为序列化后的属性值(如 JSON 或自定义二进制格式);
- 查询时,只需根据 VertexID + TagID 定位 Key,一次 Get 即可返回全部属性。
2.4.2 边的存储
边按 源顶点(Src)组织,Key 中包含源点 ID、边类型、排序字段(Ranking)和目标点 ID。
- Ranking 字段用于支持多边(如多次转账记录)的排序与去重;
- 边属性存储在 Value 中;
- 遍历邻居时,StorageD 可对
[SrcVertexID][EdgeType]前缀执行 Range Scan,高效获取所有邻接点。
这种设计天然支持 局部性优化:点及其出边物理相邻,减少 I/O 跳数。
三. 查询执行流程示例
GO FROM "user101" OVER friend YIELD dst(edge)
执行过程如下:
-
GraphD(计算层) 接收请求,解析 nGQL 语句;
-
根据
"user101"计算其所属 Partition:P = hash("user101") % N; -
向 Meta Service 查询 Partition
P的 Leader 所在 StorageD 地址; -
向该 StorageD 发起 RPC,请求
"user101"的friend类型出边; -
StorageD
Key Range: [P]["user101"][friend][min_rank] ~ [P]["user101"][friend][max_rank] -
返回所有目标顶点 ID 列表;
-
GraphD 汇总结果并返回客户端。
整个过程最多一次网络跳转(若无多跳),且 StorageD 本地完成大部分计算(如过滤、投影),极大降低延迟。
四. Partition Raft同步原理
Nebula Graph 采用 Raft 协议保证 Partition 副本间的数据一致性。下面我们以一个典型的写入操作为例,详细解析 Raft 同步的完整流程。
4.1 写入请求的 Raft 同步触发时机
当客户端发送写入请求(如插入顶点或边)时,整个处理流程涉及多个组件的协同工作。Raft 同步并非在接收到请求后立即开始,而是经过一系列前置处理后,在特定时机触发。
4.2 Raft 日志复制与提交完整流程
以下是 Raft 同步的核心流程,从客户端请求到数据持久化的完整路径:
客户端 GraphD (计算层) Meta Service StorageD Leader (Partition Leader) StorageD Follower1 StorageD Follower2 本地RocksDB 发送写入请求 解析nGQL,计算Partition ID 查询Partition Leader地址 返回Leader地址 发送写入请求 合法性校验 编码为Raft日志 写入本地WAL 复制日志(AppendEntries) 复制日志(AppendEntries) 写入本地WAL 写入本地WAL 确认复制成功 确认复制成功 多数派确认(2/3) 更新committedLogId 批量应用日志到RocksDB 写入完成 返回成功响应 返回写入结果 异步过程 携带新committedLogId 携带新committedLogId 应用日志到RocksDB 应用日志到RocksDB 客户端 GraphD (计算层) Meta Service StorageD Leader (Partition Leader) StorageD Follower1 StorageD Follower2 本地RocksDB
4.2.1 请求路由阶段
-
客户端请求 :客户端发送 nGQL 写入语句到任意 GraphD 节点
-
GraphD 解析 :解析语句,确定操作类型和涉及的顶点/边
-
Partition 计算 :根据顶点 ID 通过哈希算法计算所属 Partition ID
-
Leader 查询 :向 Meta Service 请求该 Partition 的 Leader 所在 StorageD 地址
-
请求转发 :将写入请求发送到对应的 StorageD Leader 节点
4.2.2 Leader 处理阶段
- 请求校验 :检查 Schema 合法性、权限等
- 日志编码 :将写入操作转换为结构化的 Raft 日志条目
- WAL 写入 :将日志追加到本地 WAL(Write-Ahead Log),确保数据持久化
- 触发复制 :调用 Raft 协议的日志复制机制
4.2.3 日志复制阶段
- 复制请求 :Leader 向所有 Follower 发送 AppendEntries RPC
- Follower 处理 :
- 验证日志连续性和完整性
- 将日志写入本地 WAL
- 返回确认响应
- 多数派确认 :Leader 统计确认响应,当达到多数派阈值(如 3 副本集群中收到 2 个确认)时,日志被标记为可提交
4.2.4 日志提交阶段
- 更新提交点 :Leader 更新已提交日志的最高 ID(committedLogId)
- 批量应用 :将所有已提交但未应用的日志批量写入 RocksDB
- 创建 WriteBatch,批量执行 PUT/REMOVE 等操作
- 调用 RocksDB 的批量写入 API,提高写入效率
- 响应客户端 :写入完成后,返回成功响应给 GraphD,最终传递给客户端
4.2.5 Follower 同步阶段
- 提交信息同步 :Leader 在后续 AppendEntries 请求中携带新的 committedLogId
- Follower 日志应用 :Follower 发现本地有已复制但未提交的日志时,自动将其应用到 RocksDB
- 状态一致 :最终所有副本的 RocksDB 中的数据达到一致状态
4.3 Raft 同步的设计优势
Raft一致性协议流程可参考:深度解析 Raft 分布式一致性协议 | 听到微笑的博客
-
强一致性保障
- 基于多数派确认机制,确保数据在副本间的最终一致性
- 自动故障转移,当 Leader 宕机时,从存活副本中重新选举 Leader
-
高性能写入
- 批量日志复制,减少网络传输开销
- 顺序写入 WAL,避免随机 I/O,提高写入性能
- 批量应用到 RocksDB,减少磁盘写入次数
-
资源高效利用
- 多个 Partition 共享同一 RocksDB 实例,减少内存占用和文件句柄消耗
- 统一的存储管理,便于优化和维护
-
灵活的配置选项
- 支持调整副本数量,平衡一致性与性能
- 可配置 WAL 同步策略,根据业务需求调整可靠性级别
- 支持快照频率调整,平衡存储开销与恢复速度
4.4 Raft 同步与 RocksDB 的关系
Raft 同步与 RocksDB 是两个独立的层次:
- Raft 层 :负责分布式一致性,确保日志在副本间同步
- 存储层 :负责数据持久化,将已提交的日志写入 RocksDB
- 解耦设计 :Raft 层不直接操作 RocksDB,而是通过抽象接口与存储层交互
- 批量写入 :Raft 层将多个日志批量传递给存储层,存储层批量写入 RocksDB,提高效率
五. 总结
Nebula Graph 通过 Meta Service + 无状态计算层 + Shared-nothing 存储层 的三层架构,实现了高可用、高扩展的分布式图数据库系统。其核心设计亮点包括:
- 静态哈希分片(即 Partition 数量创建后不可修改):以 Vertex ID 为分片键(PartitionID = hash (VertexID) % PartitionCount),保证点与出边归属同一 Partition,且 Partition 数量创建后不可修改(即'静态'),兼顾查询效率与集群稳定性;
- 共享 RocksDB 实例:通过 Key 前缀实现多 Partition 逻辑隔离,兼顾性能与资源效率;
- Raft 多副本一致性:保障数据可靠性与自动容灾;
- 存算分离:计算层无状态,易于云原生部署;存储层专注数据持久化与局部计算。
这种架构使 Nebula Graph 能够支撑 千亿级顶点、万亿级边 的超大规模图场景,同时保持毫秒级查询响应,适用于社交网络、金融风控、知识图谱等对关联分析要求极高的业务领域。
参考文章:
NebulaGraph 数据库架构------概览 --- NebulaGraph Database Architecture --- A Bird's Eye View
NebulaGraph 存储引擎简介 --- An Introduction to NebulaGraph's Storage Engine
