上个月给一个做供应链风控的团队做选型评审,他们原本的方案是把关系全塞进 MySQL,用七八层自连接去查"某个供应商的上游三级原材料有没有落在制裁名单里",一条 SQL 跑四十多秒,业务方点一次查询要等一杯咖啡。这不是 SQL 写得不好,是关系型数据库在处理变长路径这类查询时天生吃亏——每多一跳就多一次连接运算,跳数一上去,代价是指数级的。图数据库就是冲着这类问题来的。但它不是银弹,市面上一堆名字听着都差不多的产品,底层路线差得非常远:有的把图结构直接做进存储引擎,有的只是把图查询语言套在宽表存储上,有的干脆是拼装出来的。这篇就把 Neo4j、NebulaGraph、Apache HugeGraph、JanusGraph、ArangoDB、TigerGraph、Amazon Neptune 这七款摆在一起,按我自己踩过坑的标准做一次横向比较。适合正在做技术选型的后端和架构同学,也适合想搞清楚"图数据库到底和关系库差在哪"的新手。
1. 为什么"选图数据库"比选关系库更容易翻车
1.1 图数据库真正擅长的是变长路径,而不是"存关系"
很多人第一次接触这类产品,脑子里想的还是关系型建模那一套:先画数据库 ER 图,实体一张表,关系一张中间表,主外键连起来。这套思路搬到图数据库里会走偏。关系型里"关系"是二等公民,只能靠外键和中间表表达;图模型里顶点和边都是一等公民,边本身可以带属性,可以直接被索引,可以有自己的类型。
差别在哪里体现?假设你要查"A 认识的人认识的人里,哪些同时关注了 B",在关系型里是两次 self join,勉强能撑;如果要查"最短路径在 6 跳以内"或者"某个资金账户的上游来源",连接次数不可预期,优化器基本没法给出好的执行计划。图数据库的核心能力就在这里:给定起点,沿着边往外扩展,扩展的代价和图的整体规模关系不大,只和你实际走过的边数量相关。这也是为什么反欺诈、知识图谱、推荐召回、IT 资产拓扑、权限血缘这些场景特别喜欢它。
反过来说,如果你的查询 90% 都是"按 ID 查一行""按时间范围聚合统计""多表 join 出报表",那上图数据库大概率是给自己找麻烦。图数据库的聚合分析能力普遍弱于成熟的关系库和数据仓库,很多产品连标准的窗口函数都不支持。先把查询模式摸清楚,再谈选型。
1.2 七款候选的入选理由,以及被我淘汰的那几个
选这七款不是凑数,是覆盖了当前主流的几条技术路线:
| 产品 | 路线定位 | 查询语言 | 开源情况 | 典型使用场景 |
|---|---|---|---|---|
| Neo4j | 原生图存储,单机性能标杆 | Cypher | 社区版开源,企业版收费 | 知识图谱、中小规模高并发关联查询 |
| NebulaGraph | 分布式存算分离 | nGQL / openCypher 兼容 | 开源 | 千亿级点边、超大规模图谱 |
| Apache HugeGraph | 分布式,后端可插拔 | Gremlin | 开源 | 国内团队做关系网络、反作弊 |
| JanusGraph | 拼装式,存储与索引解耦 | Gremlin | 开源 | 已有 HBase/Cassandra 存量的大数据团队 |
| ArangoDB | 多模型(图+文档+KV) | AQL | 社区版开源 | 图与文档混合、内容关系推荐 |
| TigerGraph | 并行图计算引擎 | GSQL | 社区版有功能限制 | 深度链路分析、金融风控 |
| Amazon Neptune | 云托管,多语言支持 | Gremlin / openCypher / SPARQL | 闭源托管 | 已经在云上、不想自己运维 |
被我在这次评审里淘汰的还有几个:RedisGraph 这条线已经不再积极演进,新项目我不建议压上去;Dgraph 的部署和运维门槛对中小团队偏重;OrientDB 社区活跃度这几年掉得厉害。不是说它们不行,是选型要看三年后的维护成本,而不是今天跑 Demo 爽不爽。
1.3 我用来对比的六项硬指标
每次选型我都会固定拿这几个维度去卡,避免被厂商的宣传材料带着跑:
- 存储模型:原生图存储还是"图查询层 + 通用 KV/宽表",这决定了深链查询的下限。
- 查询语言与生态:语言的学习成本、客户端驱动是否齐全、有没有可视化工具。
- 横向扩展能力:是只能纵向升级,还是能加机器线性提升。
- 事务与一致性:有没有 ACID、跨分区事务支持到什么程度、隔离级别是什么。
- 导入与运维:批量导入工具、备份恢复、监控指标是否开箱可用。
- 成本结构:开源协议、企业版授权模式、云上按小时计费的隐性溢价。
下面逐个说。我会尽量把"为什么这样设计"讲清楚,而不是只列参数。
2. Neo4j:把图结构做进存储引擎的老牌选手
2.1 免索引邻接省下来的到底是什么
Neo4j 最核心的设计叫免索引邻接(index-free adjacency)。翻译成人话:每个顶点在磁盘上都直接记着它的第一条边在哪儿,每条边记着它的起点、终点和上下一条边的位置。你要从一个顶点往外走一跳,就是顺着指针读一次磁盘,不需要再去查任何全局索引。
这一点在关系型数据库里是做不到的。关系库查 join 必须先通过索引找到匹配行,索引本身是 B+ 树,数据量大了以后索引层级变高,查找代价随数据量对数增长。Neo4j 的邻接遍历代价基本是常数级,所以你会看到一个很有意思的现象:在千万级点、上亿级边的数据集上,Neo4j 做 3 到 5 跳的路径查询依然能压在百毫秒量级,而同样数据的 MySQL 可能已经跑不出来了。
代价也明显:这种存储结构是为"顺着边走"优化的,不适合全表扫描和聚合统计。你想算"所有顶点的平均度数",Neo4j 会比关系库慢。它天生就是点查和路径查的引擎。
实际配置上,最关键的一个参数是页缓存(page cache)。我的经验值是给它物理内存的 50% 到 70%,剩余留给操作系统和查询执行。如果热数据能全部装进页缓存,路径查询的尾延迟会明显收敛。容量估算不用太精确,把顶点数、边数乘以各自的平均属性字节数,再打个 1.5 倍的膨胀系数,就够你做机器规划了。
2.2 Cypher 的写法红利与执行计划陷阱
Cypher 是我用过最舒服的图查询语言,它用 ASCII 艺术画图:
MATCH (a:Company {name:'甲方公司'})-[:SUPPLIES*1..3]->(b:Company) WHERE b.riskLevel = 'HIGH' RETURN a.name, b.name, length(shortestPath((a)-[*]-(b))) AS hops LIMIT 50这段的意思是:从甲方公司出发,沿着 SUPPLIES 边往外走 1 到 3 跳,找出风险等级为高的下游公司。可读性比 SQL 的层层嵌套好太多。
但 Cypher 有个大坑:变长路径和笛卡尔积。MATCH (a)-[*]-(b)这种不限制方向、不限制类型的写法,会在稠密图上爆炸。我见过一个查询在测试环境跑了 200 毫秒,上线后遇上几个超级节点(度数上万的顶点)直接跑到内存溢出。规避办法有三条:
- 变长路径一定要带类型和方向,
[:SUPPLIES*1..3]->比[*1..3]安全得多。 - 用
PROFILE或EXPLAIN看执行计划,重点看有没有出现CartesianProduct和VarLengthExpand的大行数估算。 - 对超级节点做预处理,比如限制它的边采样,或者把它的邻居预先聚合出来。
提示:开发阶段养成习惯,任何带变长路径的查询上线前都跑一遍 PROFILE,看 db hits 的绝对值。db hits 上千万的查询,别指望在并发场景下表现良好。
可视化这块补一句,Neo4j Bloom 是配套的交互式探索工具,装的时候注意三点:Bloom 的版本必须和 Neo4j Server 的主版本对齐,放错版本会在浏览器控制台报错而服务端日志一片安静;独立部署 Bloom 需要企业版授权,用 Desktop 的话是内置的;插件方式是丢进plugins/目录后重启服务。Bloom 的价值在于让业务方自己拖拽着看关系,不用每次找开发写查询,做知识图谱类项目时省下的沟通成本比授权费值。
2.3 从单机到因果集群:能力边界与授权成本
Neo4j 社区版只能单机跑,企业版才有因果集群(Causal Clustering)。集群的形态是:一个写主节点(Leader)+ 多个读从节点(Follower)+ 若干只投票不存数据的仲裁节点。写请求全走主节点,读请求可以打到任意从节点,主从之间用 Raft 协议同步。
这个架构决定了它的写扩展性是有天花板的——只有单点写。如果你的场景是读多写少,比如知识图谱查询、权限校验,集群扩几个读副本就能撑住;如果场景是高并发写入,比如每秒几万条边的事件流实时入库,Neo4j 集群会让你很难受。这一点和 NebulaGraph 这种原生分片的架构有本质区别。
还有一个实际工程里经常被忽略的点:索引和约束也要建。很多人从关系库转过来,以为图数据库不需要索引,结果发现按属性查顶点慢得离谱。给高频查询的属性建索引是必须的:
CREATE INDEX company_name_idx FOR (c:Company) ON (c.name); CREATE CONSTRAINT company_id_unique FOR (c:Company) REQUIRE c.id IS UNIQUE;唯一约束除了保证数据质量,还能让MERGE语句在并发写入时不会写出重复顶点,这在实时导入场景里是刚需。
3. 分布式阵营的两种活法:NebulaGraph 与 Apache HugeGraph
3.1 NebulaGraph 的存算分离与 nGQL 的取舍
NebulaGraph 的架构是三件套:graphd负责查询解析和执行,metad管元数据和集群调度,storaged管实际数据存储,底层用 RocksDB 做单机引擎。三个角色可以独立部署和扩容,这就是所谓的存算分离。数据按分片(partition)打散到各个 storaged 上,每个分片用 Raft 做多副本,默认三副本。
这种设计的好处很直接:数据量涨了加 storaged,查询压力涨了加 graphd,互不干扰。千亿级点边的规模在它的设计目标里,实际部署时我一个客户用了十几台机器跑几百亿边,路径查询(3 跳以内)稳定在几十毫秒。它对超级节点的处理也比 Neo4j 友好,因为数据是分片的,热点不会全砸在一台机器上。
nGQL 是个折中产物。早期版本它搞了一套类 SQL 的语法,后来为了降低迁移成本,在新版本里做了 openCypher 兼容层。我的建议是:新项目直接用 nGQL 原生的GO语法,它的语义更贴近数据分布,写法也更明确:
GO 3 STEPS FROM "company_1001" OVER SUPPLIES WHERE $$.company.riskLevel == "HIGH" YIELD src(edge) AS from_id, dst(edge) AS to_id, edge.riskScoreGO N STEPS明确告诉引擎走几跳,不像 Cypher 的变长路径那样容易写出不可控的计划。
它的短板也要说清楚:跨分片事务支持有限,强一致的多点写入场景要谨慎设计,通常得靠业务层补偿或者引入外部事务协调;另外生态工具相比 Neo4j 要薄一些,Cypher 的存量代码迁移过来需要改语法。还有一个坑是顶点 ID 的类型——默认是 64 位整型,用字符串 ID 的话要在建图空间时显式声明VID_TYPE = FIXED_STRING(N),N 要一次定够,后面改不了,很多人在导入数据时才发现 ID 被截断。
3.2 HugeGraph 的后端矩阵与 Gremlin 兼容路线
Apache HugeGraph 走的是另一条路:兼容 TinkerPop Gremlin,然后让你自己挑存储后端。它的可选后端包括内存、RocksDB、Cassandra、HBase、ScyllaDB,索引后端可以选 Lucene 或者 Elasticsearch。这意味着如果你的团队已经有一整套 HBase 运维体系,可以直接把图数据落在熟悉的存储上,运维压力小很多。
Gremlin 的写法是这样的:
g.V().has('company','name','甲方公司') .repeat(out('supplies')).times(3).emit() .has('riskLevel','HIGH') .path().limit(50)Gremlin 的表达能力比 Cypher 更强,因为它是图灵完备的,可以嵌逻辑、做分支、循环。代价是可读性差,写复杂查询像在写 Lisp,团队里没有一两个熟悉函数式写法的人会比较痛苦。而且 Gremlin 的执行计划更难调优,一个repeat().until()写不好,性能可能差两个数量级。
HugeGraph 在国内的实践比较多,配套工具链还算齐:HugeGraph-Loader 做批量导入,HugeGraph-Studio 做可视化,HugeGraph-Tools 做备份和迁移。它的问题在于组件耦合较重,一个完整的生产部署要拉起 server、loader、studio、hubble 好几个东西,版本对齐是个体力活。
3.3 导入、扩容、运维三件事的实测差异
选型到最后,真正决定成败的往往不是查询性能,而是这三件事做起来顺不顺。
批量导入:NebulaGraph 有nebula-importer和 Spark 版的导入工具,大数据量下走 Spark 是正解,实测几十亿边级别的导入要以小时计,但可以横向扩 executor 加速。HugeGraph-Loader 是基于 Spark 的,配置项多但文档清楚。两者都要注意一件事:导入前先建好索引和 Schema,边类型、属性类型都要预先定义,图数据库普遍是 Schema 相对严格的,没有关系库那种"先插数据再改表"的自由。
扩容:NebulaGraph 加 storaged 后需要做数据均衡(balance data / balance leader),这一步会消耗 IO,建议在业务低峰做,而且要监控均衡进度。HugeGraph 换后端基本等于重新导入数据,扩容依赖底层存储(比如 HBase 自身的 Region 分裂),所以它的扩容体验本质上取决于你选的存储后端,选对了后端就白送,选错了就得自建。
运维监控:NebulaGraph 暴露的指标比较全,和 Prometheus 集成顺滑;HugeGraph 靠 HTTP 接口取状态,得自己补一些采集脚本。这块的实际工作量经常被低估,等出问题了才发现连个 QPS 曲线都没有。
4. 拼装派与多模型派:JanusGraph 与 ArangoDB
4.1 JanusGraph 的存储后端加索引后端组合矩阵
JanusGraph 是我见过最"乐高"的图数据库。它自己不管存储,只做图语义层,把数据存到 Cassandra / HBase / ScyllaDB / BerkeleyDB,把索引存到 Elasticsearch / Solr / Lucene。你可以自由组合:
| 存储后端 | 索引后端 | 适合场景 | 要注意的坑 |
|---|---|---|---|
| Cassandra | Elasticsearch | 写多读多、已有 Cassandra 集群 | ES 索引延迟导致"写入后立刻查不到" |
| HBase | Solr | 已有 Hadoop 生态 | 组件多,JVM 调优复杂 |
| ScyllaDB | Elasticsearch | 追求低延迟写入 | ScyllaDB 运维经验要求高 |
| BerkeleyDB | Lucene | 单机开发、测试 | 生产不可用,无法横向扩展 |
这个设计的最大优势是复用存量基础设施。团队已经有成熟的 Cassandra 集群和 ES 集群,JanusGraph 几乎是零新增运维负担。劣势也很清楚:跨两个系统的最终一致性。JanusGraph 的索引更新是异步的,你写完一个顶点,立刻按属性去查,可能查不到,得等 ES 的刷新间隔(通常配置成 1 秒左右)。这在高并发写入 + 立即读取的场景里会出问题,常见的规避办法是:关键路径用顶点 ID 直查(走存储后端的精确读,不经过 ES),只有属性查询才依赖索引。
4.2 Gremlin 在 JanusGraph 上最容易踩的性能坑
JanusGraph 的性能调优,八成问题出在三个地方。
第一,没用索引。Gremlin 的g.V().has('name','x')如果 name 没建索引,会退化成全图扫描,几百万顶点就是灾难。必须显式建:
mgmt = graph.openManagement() name = mgmt.getPropertyKey('name') mgmt.buildIndex('byNameComposite', Vertex.class).addKey(name).buildCompositeIndex() mgmt.commit()复合索引(composite)只支持等值查询,混合索引(mixed)支持范围查询和全文检索,但必须挂索引后端。这个区分很多人搞混,建了复合索引然后写has('age', gt(30)),结果索引不生效。
第二,事务没提交。JanusGraph 的写入在一个事务里,不 commit 就不落盘。有人在循环里写了几万条边上一次 commit,内存直接打满。
第三,OLAP 和 OLTP 混用。JanusGraph 支持通过 Spark 做全图分析(OLAP),但那个路径的资源消耗和在线查询完全不是一个量级,千万别在同一个集群上跑。
4.3 ArangoDB 用 AQL 把图、文档、键值装进一个引擎
ArangoDB 的定位是多模型数据库,同一个引擎里能存文档、键值和图。它的核心创新是"边"就是特殊的文档,存在边集合(edge collection)里,带_from和_to两个字段。这样一来,图遍历和文档查询可以写在同一条语句里,用 AQL:
FOR v, e, p IN 1..3 OUTBOUND 'company/1001' supplies FILTER v.riskLevel == 'HIGH' RETURN { company: v.name, hops: LENGTH(p.edges), path: p.vertices[*].name }AQL 的可读性介于 SQL 和 Cypher 之间,学过 SQL 的人上手很快。它的优势场景是**"图 + 文档"混合**:比如一个商品推荐系统,商品详情是文档、用户行为是边、标签是键值,用 ArangoDB 可以一个库全搞定,不用维护两套系统做数据同步。
代价是它在单一维度上都不是最强的。纯深链遍历性能打不过 Neo4j,超大规模分布式能力不如 NebulaGraph。它赢在"少一个组件就少一类故障"。集群模式下 ArangoDB 用分片 + 副本(RocksDB 引擎),跨分片遍历的性能会明显下降,做选型时一定要用自己的数据实测跨分片场景。
4.4 两条路线各自的适用面
我把这两个的判断标准简化成两句话:
- 如果团队已经有 Cassandra 或 HBase 且运维成熟,图数据量在十亿级以内,JanusGraph 是性价比很高的选择,因为新增的运维成本接近于零。反过来,如果团队没有大数据组件经验,JanusGraph 会让你陷入"排查问题不知道是图的问题还是存储的问题"的泥潭。
- 如果业务里图查询只占一部分,另外一部分是文档检索和键值读写,ArangoDB 值得认真评估。它的短板是超大规模,但中小规模下开发效率的提升非常明显。我见过一个内容社区用它同时扛文章存储、标签关系和相关推荐,省掉了一整套数据同步链路。
5. 闭源与托管:TigerGraph、Amazon Neptune 的取舍点
5.1 TigerGraph 的 GSQL 与并行遍历引擎
TigerGraph 的技术亮点是并行图计算。它的存储和计算都做了分片,遍历的时候多个分片同时展开,用一套叫累加器(accumulator)的机制做中间结果聚合。这让它在超深链分析(比如 5 跳以上的路径枚举)上有明显优势,风控和反洗钱领域用得比较多。
GSQL 的写法是"查询 + 存储过程"混合的:
CREATE QUERY findRiskPath(STRING startId, INT maxHops) { SumAccum<INT> @@pathCount; Start = {Company.*}; Result = SELECT t FROM Start:s -(:e)- Company:t WHERE s.id == startId ACCUM @@pathCount += 1; PRINT Result, @@pathCount; }这种写法的好处是可以把复杂逻辑预编译成过程,调用时只传参,省去了解析开销;坏处是学习曲线陡,跟 Cypher、Gremlin 都不兼容,迁移等于重写。另外要注意社区版在集群规模和数据量上是有限制的,生产用基本要走商业授权,报价不算便宜,做预算时要提前跟采购对齐。
5.2 Amazon Neptune 的三套查询语言与管理边界
Neptune 最大的特点不是技术,是三种查询语言同时支持:Gremlin(属性图)、openCypher(属性图)、SPARQL(RDF 三元组)。这意味着你做技术选型的时候不用先被迫决定用属性图还是 RDF,可以先跑起来再收敛模型。RDF 这条路在需要遵循 W3C 标准、做数据互操作的场景里很有价值,比如医药、科研、出版领域的数据集。
底层存储是三可用区六副本,写入自动同步,故障切换对应用基本透明。你不用管分片、不用管副本、不用管备份脚本,这些都是托管的。代价是:
- 看不见的边界:慢查询的诊断手段比自运维少,遇到性能问题很多时候只能提工单。
- 成本随数据量线性上升:按实例小时 + 存储 + IO 计费,冷数据放在那也在烧钱,长期看比自建贵。
- 迁移成本极高:一旦数据进了托管服务,想搬走要经历导出、转换、重新导入,中间还有停机窗口。
我的建议是:短期项目、团队没有专职 DBA、数据量可控的情况下,托管是理性的选择;如果这个图谱会成为公司核心资产,跑五年以上,自建或者至少保留一套可迁移的方案更稳妥。
5.3 云托管的真实账单与锁定风险
算账的时候别只看实例单价。托管图数据库的成本至少包含四块:实例费、存储费、IO/请求费、跨可用区流量费。其中 IO 费是最容易失控的,图查询的随机读特别多,一次深链遍历可能产生几千次 IO,如果你的查询模式本身不优化,账单会给你上课。
我会在选型阶段做一个"单位查询成本"的测算:挑十条典型查询,各跑一万次,记录总 IO 和耗时,换算成钱。这个数字比任何性能报告都实在。同时把"迁移出去需要多少人天"也写进选型文档,这不是要立刻迁移,而是给未来的自己留条路。
6. 七款横向总表与场景化选型路径
6.1 关键能力对照表
| 维度 | Neo4j | NebulaGraph | HugeGraph | JanusGraph | ArangoDB | TigerGraph | Neptune |
|---|---|---|---|---|---|---|---|
| 存储模型 | 原生图 | 原生分布式图 | 依赖后端 | 依赖后端 | 多模型 | 原生分布式图 | 托管原生 |
| 查询语言 | Cypher | nGQL / openCypher | Gremlin | Gremlin | AQL | GSQL | Gremlin / openCypher / SPARQL |
| 横向写扩展 | 弱(单写主) | 强 | 取决于后端 | 取决于后端 | 中 | 强 | 强(托管) |
| 跨分区事务 | 单机 ACID,集群弱 | 有限支持 | 依赖后端 | 弱 | 支持 | 支持 | 支持 |
| 深链性能 | 强(内存内) | 强(分片并行) | 中 | 中 | 中 | 强 | 强 |
| 可视化 | Bloom 成熟 | Studio | Studio / Hubble | 第三方 | 自带 Web UI | GraphStudio | 需第三方 |
| 运维成本 | 低 | 中高 | 中高 | 高 | 低 | 低(商业版) | 极低 |
| 成本结构 | 企业版按核授权 | 开源免费 | 开源免费 | 开源免费 | 社区版免费 | 商业授权为主 | 按用量付费 |
6.2 按数据规模、查询深度、一致性要求走决策路径
我在实际选型里会把决策简化成三步走。
第一步看规模。点边总量在一亿以内,单机 Neo4j 或 ArangoDB 完全够用,别过度设计。上了十亿级,就必须考虑分布式,NebulaGraph、HugeGraph、Neptune、TigerGraph 进入候选,JanusGraph 配合 Cassandra 也能撑。到了百亿千亿,基本只剩原生分布式的几款,而且要做分片键设计。
第二步看查询深度和模式。大量 1 到 3 跳的点查,任何一款都能应付,选生态好的。需要 5 跳以上深度遍历或者全图算法,优先考虑 TigerGraph、NebulaGraph 这类并行遍历引擎。
第三步看一致性和写入模式。如果业务要求"写进去立刻能按属性查到",JanusGraph 配合 ES 的异步索引会让你很难受,Neo4j 或者带强一致索引的方案更合适。如果是海量事件流持续写入、对延迟不敏感,选支持高吞吐写入的分布式方案。
6.3 三个典型场景的推荐组合
场景一:企业知识图谱,几千万到几亿点边,读多写少,业务人员要自己探索关系。推荐 Neo4j 企业版配 Bloom。理由:Cypher 生态成熟,招人和培训成本最低,Bloom 让业务方自助查询,减少开发排期压力。预算紧张就用社区版单机加分片,或者把冷数据归档。
场景二:超大规模关系网络,百亿级点边,需要在线 3 跳查询加离线全图分析。推荐 NebulaGraph 做在线查询,配合 Spark 做离线图计算。理由是存算分离让查询和存储独立扩容,成本可控,而且 nGQL 的GO N STEPS语义明确,不容易写出失控计划。
场景三:已有 Hadoop 生态,想快速把图能力接进现有数据链路。推荐 JanusGraph 配 HBase 加 Solr。理由是几乎不新增运维对象,数据可以和离线任务共享存储。前提是团队能接受异步索引带来的延迟。
7. 别信评测报告,用七天跑一遍自己的数据
7.1 前 48 小时:建模与数据导入
任何第三方测评对你都没有参考价值,因为图数据库的性能和数据分布关系极大。有没有超级节点、平均度数是多少、属性是宽是窄,这些才是决定性能的关键。所以第一件事是把自己的数据抽一份样本出来,规模控制在生产数据的 5% 到 10%,但拓扑结构必须是真实的,尤其是要包含那些度数异常高的顶点。
导入阶段要记录三个数字:导入总耗时、失败记录数、导入过程中的峰值内存。失败记录往往能暴露出建模问题,比如顶点 ID 格式不统一、时间字段格式不一致、边的两端有悬空引用。这些问题在 Demo 数据里永远不会出现,在真实数据里一抓一大把。
# 以 Neo4j 为例,用 UNWIND 批量导入比逐条 CREATE 快一个数量级 from neo4j import GraphDatabase driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password")) def batch_import(tx, rows): tx.run(""" UNWIND $rows AS row MERGE (a:Company {id: row.from_id}) MERGE (b:Company {id: row.to_id}) MERGE (a)-[r:SUPPLIES {year: row.year}]->(b) SET r.amount = row.amount """, rows=rows) with driver.session() as session: for chunk in chunks(all_rows, 5000): session.execute_write(batch_import, chunk)一个实操心得:批量导入时批次大小不是越大越好。我的经验区间是 2000 到 10000 条之间,具体取决于单条记录的大小。批次太大事务日志压力大,反而变慢;太小则网络往返开销占比过高。这个数字要在自己的环境上试出来,别照抄别人的。
7.2 中间三天:查询模式覆盖与压力测试
第二天开始跑查询。做法是把业务方提的需求整理成 10 到 20 条典型查询,覆盖点查、1 跳、3 跳、5 跳、聚合统计、写操作,每条都记录平均延迟、P99 延迟、返回行数。P99 比平均值重要得多,图数据库的尾延迟经常因为碰上超级节点而抖动得厉害。
压测要用真实并发模型。我见过太多团队用 100 并发去压一个实际只有 5 并发的场景,然后在生产上发现瓶颈完全在别的地方。压测的目标不是刷出一个漂亮数字,是找到性能开始劣化的拐点:从多少并发开始,P99 突然翘起来。这个拐点就是你未来的容量红线。
同时要做破坏性测试:故意在压测中注入写请求,看读写混合场景下性能怎么变化;故意让查询命中超级节点,看会不会拖垮整个实例;故意跑一条没有索引的查询,观察它对其他查询的影响。这些信息在你上线后救命的时刻会体现价值。
7.3 最后两天:备份恢复、扩容与故障演练
这一步最容易被跳过,但恰恰是决定长期成本的地方。
备份恢复:做一次完整备份,记录备份耗时和备份文件大小,然后真的恢复一次。很多方案备份没问题,恢复的时候发现版本不兼容、索引要重建、权限要重配,恢复时间比预期长好几倍。恢复时间(RTO)和可接受的数据丢失量(RPO)必须在这两天里明确,不能写"待定"。
扩容演练:加一台机器进去,观察数据重分布耗时、对在线查询的影响、扩容后性能是否有提升。如果扩容后性能没变化甚至下降,说明瓶颈在别的地方(通常是查询本身或者客户端),那就别急着买机器。
故障演练:手动杀掉一个节点,记录服务中断时长、有没有数据丢失、客户端能不能自动重连。用托管的服务就模拟可用区切换,看监控指标和实际业务表现是否一致。
7.4 必须记下来的那几个数字
测评结束,我会把这些数字填进一页纸的表格里,作为最终决策依据:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 导入吞吐(边/秒) | 数据装载能力 | 决定上线窗口和后续增量导入方案 |
| 3 跳查询 P99(毫秒) | 核心场景体验 | 直接影响业务可用性判断 |
| 性能劣化拐点(并发数) | 容量红线 | 决定机器数量和扩容时机 |
| 备份恢复耗时 | 灾难恢复能力 | 决定你能承诺的 RTO |
| 单万次查询成本 | 经济性 | 托管的隐性成本最容易在这里暴露 |
| 迁移工作量(人天) | 退出成本 | 保护自己不被锁定 |
这六个数字填完,选哪款基本就没悬念了。真正让我吃亏的从来不是性能不够,而是当初没把"备份恢复要多久""换掉它要多少人力"算进去。选型这件事,性能只是入场券,可维护性和可退出性才决定你三年后是不是还在骂自己。