接触图数据库的团队,第一反应往往是问“这台机器够不够”。问题在于 NebulaGraph 不是单个进程,它是三种角色拼起来的:graphd 是查询层,负责解析 nGQL、生成执行计划、把下层返回的数据拉上来做聚合与过滤;metad 是元数据层,掌管图空间、schema、用户权限、分区到机器的映射表,靠 Raft 保持一致;storaged 是存储层,真正的数据以键值形式落在它本地的 RocksDB 里,副本之间同样靠 Raft 同步。三个进程各自的资源画像差别极大,按一个平均值配机器,一定有一头被饿死。
graphd 是无状态的。它不落任何持久化数据,重启之后内存里的东西没了就是没了,对业务没有影响。因此它可以随意横向扩,前面挂一层负载均衡就行。它吃的是 CPU 和内存:复杂 nGQL 的执行计划生成、多跳遍历时的算子调度、结果集的临时缓存都要占资源;并发会话一多,线程池排队的代价会直接反映到客户端等待时间上。判断 graphd 够不够的标准不看 CPU 平均使用率,看线程池队列有没有堆积。
metad 的数据量极小,一台 4 到 8 核、16G 内存的机器跑它绰绰有余。但它对磁盘的要求反而是全集群最高的那一类——不是容量,而是 fsync 延迟。metad 的每一次 Raft 提交都要落盘,leader 心跳、选举超时、租约续期全挂在 fsync 上。一旦 fsync 抖动到几十毫秒甚至上百毫秒,最典型的症状是选举频繁触发、schema 变更卡住、SHOW HOSTS 之类的元数据操作几秒钟才返回。所以 metad 必须配 SSD,这是硬性要求,不是优化建议。
storaged 是资源消耗的大头,也是最容易被配错的一层。它吃四样东西:磁盘的随机读能力、磁盘在 compaction 期间的写能力、RocksDB 各类缓存占的内存、以及 Raft 复制与给 graphd 回数据所用的内网带宽。一台机器上跑了 storaged 之后,这块盘的 IO 基本就被它包了,不该再让别的服务来抢。
怎么合怎么拆,有一条清晰的分界线。三台机器以内的规模,混布是合理的:每台都跑 graphd + metad + storaged,三进程共享一台机器,metad 独占一小块 SSD 或者至少独立分区,数据和 WAL 落在同一块 NVMe 上可以接受。规模再往上,storaged 必须独立成单角色的机器——它是唯一一个对 IO 抖动零容忍的进程,跟别人混居意味着它的延迟被别人的峰值决定。再往后 graphd 按并发需求单独成组横向扩,metad 保持奇数台小规格独立部署。这条演进路径不需要推倒重来,因为三个进程之间只靠配置里的地址互相发现。
图数据库在机械盘上跑不动,不是一个笼统的“图数据库吃 IO”能解释的,要把账算出来。假设图里每个点的平均出度是 40。从某个起点出发做跳数为 3 的遍历,展开的节点数量级是 1 加 40 加 1600 加 64000,也就是六万多次邻居扫描。这六万多次扫描的每一次,都是一次键值前缀查询:拿起点 VID 加边类型拼成前缀,去 RocksDB 里 seek。它们之间没有顺序性可言。
为什么没有顺序性?关系型库里扫描一张表,数据是按主键物理相邻存放的,一次读可以带出后面一大串,操作系统和盘固件的预读还能帮上忙。图不一样:逻辑关系上的邻居,在物理存储上大概率彼此远离。VID 经过哈希落到不同分区,不同分区的数据分布在不同机器、不同 SST 文件的不同位置。你刚读完 A 的邻居清单,下一个要读的 B 可能在另一个数据节点上,也可能在同一块盘的几十万个块之外。于是每一次读都退化成一次独立的定位。
这就解释了图数据库对 4K 随机读延迟为什么敏感得近乎苛刻。粗略地讲,一次多跳查询的耗时约等于「扫描次数 × 单次延迟」再除以能并行跑的线程数。用公开资料里常见的量级来比:企业级 NVMe 的 4K 随机读延迟在几十到一百微秒区间,随机读 IOPS 在数十万量级;SATA 固态盘的随机读延迟要翻一到几倍,IOPS 掉一到两个数量级;机械硬盘的随机读延迟是毫秒到十几毫秒级,IOPS 只有几百。同一条三跳查询,在 NVMe 上可能是几百毫秒,换到机械盘上就是几十秒甚至几分钟,这中间的差距还不包括并发叠加之后的排队效应。具体数值以厂商规格书为准,不同型号差异很大。
机械盘在这里不是“慢一点”,是“不可用”。多跳遍历天然会把延迟一层层乘上去,单次 4K 随机读多出来的那几毫秒,都会被跳数和邻居数放大成肉眼可见的秒级等待。更糟的是机械盘的随机读延迟本身方差极大,磁头寻道时间不可预测,P99 会是均值的数倍,线上表现就是偶尔一条查询卡死十几秒。图 workload 里几乎不存在能让机械盘发挥顺序带宽优势的场景——全图扫描另说,但那应该交给离线图计算引擎,不该在在线图库上跑。
还有一个被忽略的放大源:block cache 命中率。上面算的账是全部落到磁盘的悲观情形,实际线上会有相当比例的读被 RocksDB 的 block cache 和操作系统页缓存接住,命中和不命中差两个数量级。所以内存给多少,直接换算成查询延迟好不好看。这也是为什么图数据库宁可贵一点上大内存加 NVMe,也不愿意用“配置看着差不多”但内存小一档的机器。而一旦业务模式变了、图的热点分布漂移了,命中率会掉,这时候 CPU 和磁盘再闲也没用。
把这条账推到建模上,会得到几条硬约束:跳数要在语句里显式限制,不要写不设深度的遍历;入边上尽量带过滤条件让下层能提前剪枝;超级节点要单独处理,因为一个百万度的点会把一次遍历直接爆炸成百万次随机读;以及在压测阶段先跑一遍「跳数为 3、带典型过滤条件」的查询拿到 P99 基线,再谈别的优化。
NebulaGraph 分片的基本单位是 partition。创建图空间的时候必须指定 partition_num,之后每个 VID 会被映射到一个确定的 partitionId,同一个 VID 的所有数据——它的点属性、它的所有出边、它的所有入边——都落在同一个 partition 里。每个 partition 是一个独立的 Raft group,有三副本时就是三个副本分散在不同机器上,其中一个副本是 leader,读写默认都走 leader。
这个映射决定了几件事。其一,VID 的设计就是分片键的设计。如果 VID 本身带有规律且前缀重复度高,哈希之后仍可能出现分布不均;用定长整型做 VID,无论是密度还是比较成本都比变长字符串好。其二,超级节点必然造成单分区热点。因为同一个 VID 的所有边在同一个 partition 里,一个拥有几十万条边的中心节点,它的全部边就只能由一组 Raft 副本承担,其他机器帮不上忙。社交网络里的大 V、支付网络里的商户聚合节点、设备拓扑里的核心交换机,都是典型。
partition 的数量该怎么定?行业里常见的做法是按 storaged 节点数的整数倍来取,并留足未来扩容的余量。原因很直接:加机器的时候,metad 要把已有 partition 尽量平均地重新分布,partition 总数是节点数的整数倍才分得干净;更重要的是,一个 partition 是一个完整的 Raft group,搬运它等于整份拷贝,粒度太粗不好均衡,粒度太细则 Raft 心跳、日志、状态的元数据开销会显著上升,metad 的压力也会跟着涨。所以它不是“越细越好”,也不是“default 就行”。
为什么定了不能改?因为它是图空间级的元数据,映射函数写进了 schema 里。修改它意味着每一个 VID 都要重新计算归属、每一条边都可能要搬家,代价等同于把整个库重写一遍。正确的做法是在建 space 之前就把节点规模、未来两年的扩容预期想清楚,宁可一开始多留几倍余量,也不要事后改。这也是我们建议用户在压测环境用真实数据量先跑一遍的根本原因——压测环境怎么犯错都不怕,生产环境改不了。
热分区在线上的表现通常是这样:某几台 storaged 的 CPU、磁盘利用率、网络出流量长期高于其他节点,而集群整体利用率看着并不高;进一步的证据是某个 partition 的读写量明显高于均值,或者某几个 leader 集中在同一台机器上。这种情况下要先做的是 balance leader,把 leader 打散,代价很小几乎是瞬时生效;如果数据本身分布就不均,那只能做 balance data,它会把整个 partition 的副本组通过网络整份拷到新节点,期间持续占用磁盘写和网络带宽。
扩容搬运的账要提前算。假设单个 partition 平均承载 20 GB 数据,要搬 20 个 partition 就是 400 GB,在万兆内网上理论十几分钟到几十分钟,但在已经跑满业务 IO 的盘上,很可能拖到小时级。因此 balance data 必须放在业务低峰,并且要避开 compaction 窗口——两件重 IO 的事叠在一起,业务侧看到的就是查询大面积超时。
storaged 底下真正干活的是 RocksDB,采用的是 LSM 树结构。写入进来先进内存里的 memtable,同时写一份 WAL 保证掉电不丢;memtable 攒够了就 flush 成一个不可变的 SST 文件落到第 0 层。这套机制让写入变得极为友好——内存里排序好之后是一次顺序写,比关系库原地改页的方式快得多,这就是 LSM 写入吞吐高的原因。
代价被推到了读侧和后台。读一个键的顺序是:先查 memtable,查不到再查各级 SST,每往下一层就要多一次索引查找和数据块加载,层级越深、文件越多,一次逻辑读背后真正的物理 IO 次数越多,这就是读放大。为了把层数压下来,后台会持续做 compaction,把下层文件往上合并,顺带把被覆盖和被删除的旧版本丢掉。合并这件事自己既要读又要写,于是又产生了写放大。
写放大在图场景里尤其明显。业务侧写入常常是 upsert 密集型的——边的权重更新、属性刷新、关系新增——每一次更新都会留下一个旧版本等 compaction 来回收。公开资料里 leveled compaction 的写放大倍数在十到几十倍区间,具体取决于层级比例和数据形态;这意味着业务写了 1 GB,磁盘实际承受的写入量可能是十几 GB 到几十 GB。这直接决定了两件事:固态盘的寿命消耗要按这个倍数算,磁盘余量也要按这个倍数留。
内存这块是最容易扯皮的地方。storaged 进程的内存账面构成大致有三块:RocksDB 的 block cache 占大头,用来缓存解压后的数据块,它直接决定上一节说的读命中率;memtable 与相关写入缓冲,按写入并发和列族数量分配;还有索引与布隆过滤器、迭代器执行期间的临时内存,以及 Raft 层的缓冲。所以“内存给少了查询慢、给多了怕 OOM”,中间这个平衡点要算清楚。
为什么看起来像内存泄漏?因为 block cache 是惰性分配的,它的占用会一路往上走到配置的上限才停住。这个“一直涨”不是泄漏,是缓存填满了。真正的问题是很多人压根没显式配置上限,或者配了一个接近物理内存的值,于是进程涨到被系统 OOM 杀掉。合理的做法是用 --rocksdb_block_cache 明确给出上限,通常取物理内存的三分之一到二分之一,剩下的留给操作系统页缓存、memtable、查询执行和系统本身。操作系统页缓存不是可以砍掉的部分——文件系统元数据、SST 的压缩块读取都还要用它,把它挤光会让整体反而更慢。
想要判断自己的内存是不是合理,盯三个指标:block cache 的命中率、进程常驻内存的收敛曲线(应该在上限值附近走平而不是持续单边上扬)、以及系统剩余内存是否长期低于安全线。如果常驻内存确实在配置上限之上还继续涨,那就要看是不是 jemalloc 或 tcmalloc 的碎片回收不及时,而不是急着加内存。
三副本是生产环境的基本形态。每个 partition 的三个副本构成一个 Raft group,写入需要多数派确认,也就是说三个里至少两个写成功才返回。落到时间轴上,一次写请求的延迟大约等于:leader 本地 raft 日志的 fsync 时间,加上到 follower 的网络往返,再加上 follower 自己的 fsync 时间,取多数派里的较慢者。这里的 fsync 是同步刷盘,不是写进页缓存就算。
于是磁盘的 fsync 能力直接成为写延迟的地板。NVMe 的 fsync 在百微秒量级,SATA 固态盘要高一档,机械盘的 fsync 是毫秒级且在并发下会迅速劣化。用机械盘跑 storaged,写延迟会被抬到十几毫秒甚至更高,多线程并发写入时队列堆积,表现就是批量导入卡成一条直线。这也是为什么 storaged 的数据盘和 WAL 目录都要求 SSD,没有讨论余地。
leader 分布是另一个容易被忽视的点。默认情况下读写都走 leader,如果几十个 partition 的 leader 恰好集中在某两三个节点上,那几台机器就会承担远超份额的读写压力,其他机器却在闲着。新节点加入、节点重启、故障恢复之后都可能出现这种不均衡,所以需要执行 balance leader 把 leader 重新摊平。这一步的代价极小——leader 只是角色,不涉及数据搬运——因此应该作为常规巡检动作,而不是等到出问题才想起来。
metad 的 Raft 更是纯粹的 fsync 敏感型。它的数据量可以忽略不计,但心跳间隔和选举超时都在亚秒到秒级,任何一次 fsync 抖动接近这个量级,就可能触发选举;选举期间元数据写入会被阻塞,schema 变更、分区调度、机器上下线的感知全部受影响。极端情况下会出现“选举—超时—再选举”的震荡,集群看起来是活的,但什么都做不了。实践中 metad 应该配独立的低延迟 SSD,严格避免和 storaged 的数据盘共用同一块物理盘,更不要放在网络文件系统上。
副本带来的容量账也要在采购阶段算进去。三副本意味着有效容量只有裸容量的三分之一,再叠加 RocksDB compaction 需要的工作空间,一块 3.84 TB 的 NVMe 上真正能装的业务数据远低于 1.28 TB。行业里通行的做法是按 25% 到 30% 的水位余量来规划,把 compaction 峰值、重建期间的临时占用都算进去。 磁盘写满在 LSM 上不是变慢的问题,是会直接写失败。
图数据库上线绕不开一次初始导入。常见的路径有三条:nebula-importer 单机多线程读 CSV 并提交写入;Spark 或 Flink Connector 分布式批量提交;以及用客户端自己拼 INSERT 语句。三者的资源画像高度相似——短时的 CPU 高占用、磁盘高写入、内网高带宽,同时在 graphd 和 storaged 两端都形成压力。
拆解这个窗口期的资源占用。CPU 方面,客户端侧的序列化与反序列化、graphd 侧的语句解析与执行计划生成、storaged 侧的键值写入,三处同时吃满;磁盘方面,memtable 的 flush 是顺序写,但随之而来的 compaction 是随机读加随机写的混合形态,业务写入越猛 compaction 越重;网络方面,写入数据要从客户端流向 graphd、从 graphd 流向 leader、再从 leader 复制到两个 follower,等于业务数据量乘以副本数再加一层。
为什么导入期间不要同时跑在线查询?三个原因。其一是 IO 争抢:compaction 正在吃掉盘的读写带宽,在线查询的随机读只能排队。其二是缓存污染:批量写入会把大量冷数据块冲进 block cache,把原本为在线热数据准备的缓存位挤掉,命中率断崖式下跌,导入刚结束的那段时间查询反而比导入前更慢。其三是连接池:graphd 的线程池和连接数被打满,在线请求根本排不上。
导入之后必须手动触发一次 compaction。因为高速写入会在第 0 层堆出大量 SST 文件,层数深、文件多,读放大处在最差状态,此时上线的查询延迟会明显偏离稳态。手动压实之后,层级回落、读路径变短,延迟才会回到正常水平。这一步应该在导入窗口结束、业务流量切换之前完成,并且要在监控里确认磁盘写 IO 已经平息。如果嫌手动压实太慢或者怕影响太大,另一个思路是把导入拆成多轮小批量,每轮之间留时间让后台自动 compaction 追上来。
还有一个顺序问题:索引什么时候建。如果在导入前就把索引建好,每写一条边都要同步更新索引结构,导入速度会大幅下降;更常见的做法是先导入数据,再创建索引并重建。VID 类型的选择也属于这一类前置决策——定长整型在密度、比较成本和内存占用上都优于变长字符串,一旦选了字符串 VID 又没指定长度,后面的存储和传输开销会一直跟着你。这些都属于“改不了的那类决定”,要在第一次导数据之前定下来。
下面这张表按 CPU、内存、硬盘、网络、副本与容量、运维复杂度六个维度,把三种最常见的机器形态摆在一起。三档不是官方分级,而是服务器租用咨询里最常遇到的三种预算与规模组合。所有带宽与延迟数字均为理论量级换算,实际能力需在自己的链路上验证。
| 对比维度 | 入门验证(单台 8 核 32G + NVMe) | 标准生产(3 台 16 核 64–128G + NVMe) | 规模集群(3 storaged + 2 graphd + 3 metad 分离) | 选型判据 |
|---|---|---|---|---|
| CPU | 8 核三层混布,nGQL 并发到几十就开始排队,compaction 与查询抢核 | 16 核起步,留出 compaction 线程与批量导入窗口的余量,CPU 峰值多在 compaction 期间 | storaged 32 核起(Raft + compaction + 并发读),graphd 16 核按并发横向堆叠,metad 4–8 核足够 | 看 storaged 的 compaction 线程占用与 graphd 线程池队列长度,不看整机平均使用率 |
| 内存 | 32G 总量,扣除系统与进程开销,block cache 只能给到 8–12G,缓存命中率偏低 | 64–128G,block cache 按三分之一到二分之一给,页缓存与 memtable 各留份额 | storaged 128G 起并把上限写死,graphd 32–64G,metad 16–32G,互不挤占 | 用 block cache 命中率反推,命中率上不去优先加内存,而不是加 CPU |
| 硬盘 | 单块企业级 NVMe,数据与 WAL 同盘可接受;SATA SSD 勉强能跑,机械盘基本跑不动多跳查询 | 每节点 2 块 NVMe,数据盘与 WAL 分盘,写按十倍到几十倍放大量准备寿命与余量 | storaged 每节点多块 NVMe 或 U.2 拉满随机读能力与寿命,SATA SSD 只用于 metad 与日志,全集群不应出现机械数据盘 | 盯 4K 随机读 IOPS 与 P99 延迟,顺序带宽指标对图 workload 没有参考价值 |
| 网络 | 单机无内网副本流量,导入时只有本机回环,带宽不构成瓶颈 | 万兆内网起步,承载 Raft 复制与 graphd 拉取;千兆在批量导入阶段会先成为瓶颈 | 万兆内网打底,storaged 与 graphd 之间独立网段;超大图建议规划更高规格或专用链路 | 按「峰值写入带宽 × 副本数 + 查询回流量」估算,并给 balance data 留三分之一冗余 |
| 副本与容量 | 通常单副本运行,机器故障即数据丢失,只适合可重建的验证环境 | 3 副本分布在 3 个节点,容忍单节点故障;有效容量约等于裸容量的三分之一再打折 | 3 副本 × 3 台 storaged,metad 独立 3 台保 Raft 奇数;容量横向扩展靠加 storaged 组 | 容量要按副本数、compaction 余量、25%–30% 安全水位三层折减后倒推采购 |
| 运维复杂度 | 一条命令起集群,出问题直接看日志,人员门槛最低,但没有冗余也没有演练价值 | 需要掌握 leader 均衡、compaction 窗口、导入隔离三项常规动作,一人可维护 | 需要分区热点巡检、灰度变更、跨节点搬运编排与容量趋势预测,建议至少两人轮值 | 复杂度主要来自“分区不可改”带来的前期决策权重,以及 IO 争抢窗口的编排 |
把表格翻译成采购语言,是这样的。CPU 这一维,钱不要花在堆核上。storaged 的 CPU 消耗集中在 compaction 线程和 Raft 处理上,graphd 的消耗集中在计划生成与结果聚合上,两者的峰值通常不同时出现;一台 16 核跑满,往往不是核数不够,而是某一类线程的配置不合理或者 compaction 正在抢占。真要考虑加核的场景是:批量导入窗口被压缩得很短、或者需要同时支撑高并发在线查询。
内存这一维恰恰相反,值得花到边际收益递减为止。因为内存在这里唯一的换算出口是 block cache 命中率,而命中率又是查询 P99 延迟的主导因素之一。标准生产档配 128G 而不是 64G 的差价,通常远低于业务因为延迟不达标而返工的成本。这里一条经验:宁可 CPU 降一档,也不要内存降一档。
硬盘这一维是唯一没有妥协余地的一维。NVMe 与 SATA 固态盘的差别不在标称顺序带宽,而在 4K 随机读延迟和 P99 一致性;机械盘与固态盘的差别则是量级差异,直接决定多跳查询能不能在秒级返回。三种介质的定位很清晰:NVMe 做 storaged 数据盘,企业级 SATA 固态盘做 metad 与系统盘、日志盘,机械盘只配给冷备份与归档,不进图集群。
网络这一维容易被低估,因为它在平稳期几乎不出镜,只有在两个窗口才露面。一个是批量导入,数据要经过客户端到 graphd、leader 到 follower 两条路径,写 1 GB 数据网络上要跑 2 GB 以上;另一个是 balance data,整份 partition 跨节点搬运,可能持续几十分钟到数小时。千兆内网在标准生产档就已经不够看,万兆是我们给的最小建议值。
副本与容量这一维,本质是同一笔钱花两遍:买磁盘容量是为了装数据,买副本是为了容错,两者相乘才是真实账单。规划时按“目标有效数据量乘以副本数,再除以 0.6 到 0.7 的安全系数”倒推裸容量,才不会在半年后被 compaction 峰值卡死。运维复杂度这一维则要提前问清楚团队有没有人愿意长期盯着分区热点和 IO 窗口——规模集群档带来的不仅是性能,还有一套排班和巡检制度。
现象:集群跑半年后加机器,发现 balance data 怎么均都均不平,某些节点始终高出一大截;或者分区数太少,单个 Raft group 过大,搬运一次要几小时。原因:partition_num 在建图空间时就固化了,VID 到分区的映射是元数据的一部分,运行期无法修改,只能重建图空间并把数据重导一遍。处置:上线前按节点数的整数倍并预留未来两三年的扩容余量来定;已经踩了的,只能在低峰期新建图空间、双写切换、重导数据,这个代价远高于一开始就多留几倍。
现象:storaged 进程内存一路涨到几十 GB 后被系统杀掉、重启、再涨再被杀,日志里是标准的内存分配失败。原因:block cache 是惰性分配,会持续增长到配置上限;如果上限定得太高,再加上 memtable、索引与布隆过滤器、迭代器临时内存、Raft 缓冲和操作系统页缓存,总需求必然超过物理内存。处置:用 --rocksdb_block_cache 显式写上界,取物理内存的三分之一到二分之一;留足操作系统页缓存;同时给容器或进程配置合理的内存上限与告警线,让它在被杀之前先告警。
现象:查询延迟忽高忽低没有任何规律,故障时表现为大面积 IO 超时甚至 data 目录不可用;元数据类操作偶尔卡住数秒。原因:图 workload 是极致的随机小 IO,网络文件系统在每一次随机读上都要叠加一次网络往返,服务端排队与协议开销也会让延迟变得不可预测。处置:storaged 与 metad 的数据盘必须是节点本地的企业级 SSD,最好独占物理盘;网络存储可以用于备份目标,不作为在线数据目录。云主机场景也同理,要确认所挂载的是本地实例存储而非远程网络盘,具体以服务商的规格说明为准。
现象:可用性根本没有提升,任意一台机器宕机之后部分分区直接不可用,或者副本被迫堆叠在同一台机器上导致“副本冗余”名存实亡。原因:三副本要求每个 partition 的三个副本分布在不同节点,两台机器最多只能让三个副本中的两个处于不同位置,多数派无法在单机故障后成立。处置:三副本的最低正确形态是三台机器;如果预算只够两台,要么降到单副本并做好备份与快速重建预案,要么接受「三台里有一台是低配」的组合——因为第三个副本对 CPU 和内存的要求并不高。metad 同理,Raft 需要奇数台。
现象:导入跑到一半,线上接口的 P99 从几十毫秒涨到几秒,部分请求直接超时;导入结束后查询仍然很慢,要过很久才恢复。原因有三层:compaction 与在线随机读抢同一块盘的 IO;批量顺序写把 block cache 冲掉造成缓存污染;graphd 的连接与线程池被导入任务占满。处置:把导入放到独立的低峰窗口,从负载均衡上摘掉导入用的 graphd 节点或者在业务侧切流;限制导入并发与批次大小;导入结束后先手动触发 compaction,确认 IO 平息、延迟回到基线再把流量切回来。
现象:Raft 频繁触发无谓选举,心跳判超时;日志时间戳前后不一致,排障时根本没法对齐;证书校验或定时任务偶发失败。原因:分布式一致性协议依赖节点间的时钟偏差在可接受范围内,时钟漂移会让超时判断、租约判定、日志比较全部失真。处置:所有节点统一配置 NTP 并纳入监控,把时钟偏移作为一个独立告警项;虚拟机环境尤其不能依赖宿主机的时钟自动同步,要在客户机里起独立的同步服务。
现象:两个进程在同一台机器上互相倾轧,查询稍微一复杂就把 storaged 顶到 OOM;或者反过来 storaged 缓存把内存吃满,graphd 的大结果集直接申请不到内存。原因:三层混布时两个进程都按“可用内存”来申请,谁也不让谁,最终由内核的 OOM 杀手决定谁死,而死的往往是持有数据的那一方。处置:分离部署是最干净的解法;必须混布时给两边各自写死内存上限,block cache 上限按剩余可用内存重新计算,并保留固定的系统余量。
现象:新节点磁盘几乎空的,其余节点依然在告警水位;集群对外声称容量变大了,实际热点一点没分散。原因:新节点加入只是把它注册进 metad,已有的 partition 不会自动迁移,leader 也不会自动挪过去。处置:分两步做,先执行 balance leader 把读写压力摊平,代价极小可以随时做;确有必要时再执行 balance data 搬运数据,务必放在低峰并避开 compaction 窗口,同时盯紧网络带宽与磁盘 IO。
现象:每到固定时段查询集体变慢,磁盘利用率打到百分之百,但业务请求量并没有明显变化;持续十几分钟到一小时之后自动恢复。原因:后台 compaction 在吸收之前写入积累的层级压力,它同时消耗读带宽、写带宽和 CPU,恰好覆盖业务高峰期。处置:观察写入量与 compaction 压力的对应关系,把批量写入与 compaction 编排到低峰;给 compaction 设置合理的触发阈值与并发限制(具体参数以所用版本文档为准);并保留足够的磁盘空闲余量,避免 compaction 因为空间不足而反复重试。
现象:个别查询一跑就是几十秒甚至把 graphd 内存打爆,而其他查询都正常;把 VID 换成普通节点立刻秒回。原因:同一个 VID 的全部边必然落在同一个 partition,一个几十万度的点会把一次遍历展开成几十万次随机读,同时结果集在内存里堆积。处置:建模阶段就做超级节点处理——把稠密边拆成中间节点、按属性分桶、或者限制遍历出边数量;查询侧强制带深度上限和 LIMIT;把这类节点单独列进巡检清单,定期统计入度分布变化。
判断标准不在技术热度,在数据关系的形态。适合上图数据库的场景通常有三个特征:查询的跳数不固定,业务会问“A 和 B 之间有没有路径、最短几跳”,而不是固定的两表关联;需要在毫秒级实时遍历,数据一直在增量变化,边随时可能新增;核心问题本身就是关系,比如资金链路追溯、团伙识别、设备可达性分析、知识图谱上的语义检索。
反过来的清单同样明确。简单两表或三表 join,关系库配好索引就够了,上图数据库只会增加一套要运维的组件。OLAP 式的大范围聚合,比如按天统计全量交易金额,列式存储加 MPP 引擎是按这个场景设计的,图库在这种 workload 上没有任何优势。全文检索应该给搜索引擎,图库自带的索引能力不为此设计。全图算法类任务(全量 PageRank、全图社区发现)属于离线图计算的范畴,应该在独立的计算引擎上跑,不要压在在线图数据库上。
还有一个容易被忽略的判断维度:写入模式。图数据库擅长的是关系的结构化遍历,不擅长把图当宽表用。如果大部分请求本质上是“取某个点的全部属性并按某个字段排序”,那它更接近一个键值或文档查询,用关系库或文档库成本更低。真正你应该上图的信号是:现有方案里出现了大量递归 SQL、临时表,以及查询耗时按跳数呈指数增长的自关联。
上生产之前,建议按顺序跑完三件事。第一,用真实数据量做一次完整导入压测。注意是真实量级,不是抽样百分之一;同时记录导入耗时、磁盘写峰值、CPU 峰值与内存收敛值。这一步会暴露分区数、VID 类型、索引顺序等所有前期决策是否合理。第二,建立三跳查询的 P99 延迟基线。挑几条带典型过滤条件的代表性的多跳查询,在导入并压实之后连续压测,记下 P50、P99、P999 三条曲线,作为日后一切调优的参照系。没有基线的优化等于闭眼开车。第三,做故障演练。直接杀掉一个 storaged 进程,记录 leader 切换耗时、受影响的分区数量、业务侧的超时比例与恢复时间;再演练一次整机断电。这两组数字决定了你能给业务方承诺什么样的恢复预期。
不能达到期望的效果。三副本的意义是在单机故障后仍能凑够多数派继续提供服务,两台机器做不到这一点。可行的两个选项是:单副本运行加定期备份,接受故障时需要从备份重建;或者把第三副本放在第三台哪怕配置很低的机器上。第三种方案通常是性价比最高的,因为 metad 与第三个 storaged 副本对 CPU 的要求并不高。
没有放之四海皆准的数字,但它有两个约束条件:一是应当能被 storaged 节点数整除,方便后续均衡;二是要给未来扩容留倍数空间,因为它在图空间创建后不可更改。颗粒太粗会导致单个 Raft group 搬运成本过高、热点难打散;颗粒太细会让 Raft 心跳与元数据开销显著上升、metad 压力大。建议用真实数据量在压测环境里试两三个取值,看 balance 之后的数据分布是否均匀。
多数情况下不是。block cache 是惰性增长的缓存,它会一直涨到配置上限然后走平,这是设计行为,不是泄漏。判断方法很简单:看常驻内存是否在上限值附近收敛。如果持续单边上扬超过上限,再去看内存分配器的碎片回收行为,或者检查是不是并发查询的临时内存堆积。
技术上可以横向扩,但有几个决定在这个阶段会被固化:分区数、VID 类型、索引策略、副本数。其中分区数后期改动的代价是全量重导。所以正确做法是把这台机器当作“生产规格的验证机”,用它跑满真实数据量的压测,把该暴露的问题暴露完;正式生产建议至少三台起步。
数据量不是判断依据,跳数才是。哪怕只有几百万条边,只要业务需要在线追问“这条路径通不通、几跳可达、有没有闭环”,图库就是合适的工具。反过来,如果所有查询都能写成固定深度的 join,几亿条边也不必上图。
要非常谨慎。图 workload 的瓶颈是极致的随机小 IO 延迟和它的稳定性,网络存储会在每一次随机读上叠加一次网络往返,并且延迟受服务端负载影响不可预测。可以选择本地实例存储,或者自建物理机加本地 NVMe。网络存储更适合做备份目标。云盘的具体形态与是否具备本地盘能力,需向服务商确认。
把全文压缩成三句话。第一,钱应该优先花在企业级 NVMe、大内存和万兆内网上,而不是 CPU 核数——图查询的成本几乎全部由 4K 随机读延迟和 block cache 命中率决定。第二,分区数、VID 类型、索引创建顺序这三项在创建图空间时就固化了,它们的决策权重远高于后期任何一项调优,必须用真实数据量的压测来验证,而不是凭文档默认值直接上生产。第三,容量要按副本数、写放大、安全水位三层折减后倒推,并按盘而不是按集群来监控。
对大多数从关系型迁过来的团队,一万网络的标准建议是:标准生产档起步,三台机器、每节点企业级 NVMe、万兆内网、三副本,内存宁高一档,导入窗口与在线查询严格隔离。把这套跑稳三个月,拿到真实的 P99 基线和一次完整的故障恢复记录,再决定要不要扩到存储与查询分离的规模形态。
一万网络深耕 19 年(成立于 2007 年),长期做服务器租用与托管,接触过大量数据库与中间件集群的选型和扩容需求。围绕 NebulaGraph 这类图数据库场景,我们可以协助做几件事:按你的点数、边数、平均度数与平均查询跳数,反推 CPU、内存、NVMe 容量与内网带宽的搭配;判断该从入门验证档、标准生产档还是分离部署的规模形态起步;在机器到位后协助确认磁盘与网络基线表现,让 fio 的结果和你业务要求对齐。
具体到机型、可选的单盘容量、是否支持扩展到本地 NVMe、以及内网带宽规格,不同机房不同时期的可选项都不一样,我们不在这里给出报价,具体以官网实时报价为准,需向服务商确认。你可以把现有的图数据规模、几类典型查询语句和延迟要求发过来,我们从硬件账的角度帮你算一遍。
数据来源:本文内容依据 NebulaGraph 官方文档中关于进程架构、分区机制、BALANCE 操作、配置项的说明,以及 RocksDB 公开的 LSM 树结构与 compaction 机制资料整理;文中的 IOPS、延迟与写放大倍数均为公开资料的量级描述,不同硬件型号与软件版本差异较大,实际数值以厂商规格书和自有环境验证为准。涉及的服务器硬件规格、机房与带宽选项可参考一万网络官网 https://www.idc10000.net/ 的服务器租用与服务器托管页面。文中出现的所有配置建议均为典型部署思路,并非特指某一真实客户;所有价格相关内容均仅作区间参考,具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品