管过图数据库的人心里都有一张运维清单:副本怎么放、共识组怎么扩、snapshot 定不定期备份、扩缩容要不要搬数据。HydraDB 给出的答案是直接把这张清单划掉:S3 兼容对象存储是唯一持久层,图记录、WAL、manifest、遍历索引、连写入者租约和心跳都躺在桶里,查询节点和索引器手里只留一次性状态,进程挂了重新起一个,缓存再从桶里暖一遍。这跟 RustFS 的关系很好讲:RustFS 是 100% S3 兼容的分布式对象存储,HydraDB 认 S3 端点就认它,图库的灾备跟着降级成桶的灾备。
这是 2026 年 7 月才创建的项目,10 月 12 日我在 GitHub API 上实测的基本盘是 13012 stars、5440 forks、Rust 编写、AGPL-3.0 协议。目前唯一的 release v0.2.0 没有挂任何附件,想跑只能源码构建。本文的架构描述全部来自它的 README 和 architecture.md,性能数字来自官方 benchmark 站,出处都在文末。
桶里到底躺着什么
先把"对象存储当持久层"拆开讲,因为它容易被理解成一条边存成一个对象。HydraDB 的图数据底层是 SlateDB,一个把 LSM 树整个建在对象存储上的 Rust 存储引擎。LSM 树是多数宽表和 KV 数据库的存储骨架:写入先记进 WAL(Write-Ahead Log,预写日志,先记流水再动数据,崩了可以重放)和内存表,攒够再刷成不可变的 SST 文件落盘。在 HydraDB 这里,WAL、manifest、SST 全是桶里的普通对象,顶点、边、属性、拓扑记录都以 SlateDB key 的形式存在桶里,一条边并没有对应一个单独的对象。桶里还躺着几类跟数据平级的东西:每个 cell 的写入者租约_writer_leases/v2/<cell-id>(cell 是一张图内部可独立存储的分片单位,每个 cell 各自是一套 SlateDB 数据库)、共享时钟探针_coordination/v1/server-clock、索引器发布的不可变 CSC 索引代和切换用的 current 指针。协调记录和图数据共用同一个桶、同一套对象语义,这是后面"选主不需要独立组件"的物质基础。
对 RustFS 用户,这一节落在一个很实在的问题上:桶里这些对象,拿 RustFS 的 S3 客户端直接列目录就能看。没有私有卷格式,没有隐藏的元数据服务,图库整个摘掉之后,桶里留下的就是 WAL、SST 和索引文件的普通对象结构,读得出来也备份得走。RustFS 侧开版本化和对象锁,等于再给图数据加一层防误删的底。
本地盘降级成缓存,换来什么、付出什么
HydraDB 的架构文档把边界写得很硬:内存、本地写入句柄、编译好的矩阵、SSD 缓存,全部是一次性状态,丢了只涨延迟,不会丢已提交的数据。缓存是五层结构:查询结果缓存、邻接矩阵与 GraphBLAS 编译缓存、SlateDB 块缓存、NVMe 对象缓存,最后才回源到对象存储。官方 benchmark 给了一组冷热对比:缓存清空后的第一次查询,代价是热查询的 25 倍到 56 倍,扇出 10000 的超级节点冷启动 51ms、热查 912µs。这组数字反过来说明本地盘的价值:缓存命中的单跳查询 p50 只要 408µs(官方数据,15 核 Apple Silicon 单机、Docker 里的 MinIO 后端、2026-08-12 跑的,4 次取样取中位),一旦 miss 就得回对象存储拉块。
写的账单是另一副面孔。每次 mutation 提交,就是对象存储上的一次 WAL 写入加可见性确认,官方实测单次写 p50 在 4.9ms 这个量级,比热读慢两个数量级。更关键的是写吞吐的天花板来自单写者模型:单写入者 194 writes/s,加到 32 个写入者也只有 229 writes/s,p50 却从 4.9ms 涨到 138ms,加机器不加快写。这不算实现缺陷,是设计取舍:每个 cell 同时只允许一个写入者,换来的才是"谁写谁拥有、过期写入者被 fence(围栏,旧写入者被标记失效、无法再提交)"的简单正确性。
RustFS 在这里的落点很直接:图写的延迟就是对象存储写的延迟,RustFS 的写 p99 会原样进入图写的 p99。已有 RustFS 集群的团队,拿自己的写延迟数字就能估出这套图库的写体验下限,不用等谁给。
查询层没让你换方言
接入面是 HydraDB 对存量用户最友好的一块。它实现了 Bolt 5.1 到 5.4 的服务端(Bolt 是 Neo4j 的二进制查询协议),Neo4j 官方驱动拿neo4j://路由 URI 直接连,写落到租约持有者节点,读散到所有活节点;另有 JSON 和 NDJSON 的 HTTPS 查询 API。查询语言是 OpenCypher 的一个实用子集(Cypher 查询语言的开放规范),类型化关系、有界变长路径、属性与标签过滤、分页聚合都覆盖;README 也写明了没有跨多个 RUN 请求的显式事务,每次 mutation 是一次有界的服务端提交。遍历加速用了 GraphBLAS(SuiteSparse 里的稀疏矩阵代数库,把图遍历翻译成矩阵运算),另带algo.SPpaths、algo.SSpaths、algo.MSpaths三个原生路径过程,多源多目标的批量寻路一次请求做完,省掉客户端扇出。
读一致性分两档,这个设计跟对象存储绑得很紧。默认 causal 档用节点当前的 durable reader view,调用方带 bookmark 就刷新到书签位置再钉快照;strong 档在钉快照之前先回对象存储刷新 reader,主动付一次远端新鲜度成本。每次查询都钉在一个 SlateDB 快照上,元数据和拓扑读自同一个存储序列号,不存在混读两代数据的问题。
索引是理解这套架构的钥匙。graph-indexer 异步地把邻接数据编译成不可变的 CSC 索引代(CSC,Compressed Sparse Column,稀疏矩阵的列式压缩格式),先建对象、再用 CAS 指针切换 current。查询时拿索引代做底,叠上索引基线之后可见的 WAL 尾巴;尾巴缺失或超出配置范围就退回裸快照读。所以索引挂了、落后了,查询只会变慢不会变错,压力则全转嫁到对象存储的读路径上。对 RustFS 用户换个说法:索引器抖动时的读体验,就是 RustFS 读路径加 WAL 重放的体验,桶的读性能在这里直接变成图库的降级质量。
选主这件事被压进了对象存储
传统图集群的写入权要靠 Raft 组或外置协调服务选出来,HydraDB 的架构文档明确写了一句:数据路径里没有可写控制器,也没有独立的共识数据库。写入准入拆成三层:placement 用 Rendezvous 哈希在心跳活集里挑出每个 (scope, cell) 的候选写入者;候选节点拿对象存储的 CAS 租约获得写入准入(CAS,Compare-And-Set,条件更新,旧值对不上就失败,多个竞争者只有一个成功);SlateDB 的 writer epoch 做最后的 fence,就算两个进程都以为自己是写入者,旧 epoch 也提交不进 WAL。租约到期判定用的是租约对象在桶里的 LastModified,配合_coordination下的共享时钟探针,不信任节点本地钟。
有三个细节容易看漏。其一,所谓"CAS 租约选主"其实是按 cell 粒度的单写者准入,不是集群选一个全局 leader,读侧始终是任意多节点。其二,心跳列表失败时的处理是逐级收缩:新鲜视图、有界的宽限视图、然后主动 shed readiness 拒绝新写入者晋升,宁可不可用也不拿过期成员表猜。其三,这套机制的正确性押在对象存储的条件更新和 LastModified 语义上,对象存储在这里扮演的角色远超"存文件的盘",它同时是共识机制的地基。接 RustFS 时,S3 语义的覆盖质量就是这套机制的正确性参数,这也成了图库挑后端时最该测的一项:拿官方just minio-fence同款的 fencing 测试对着 RustFS 跑一遍,比任何兼容性声明都硬。
AGPL-3.0 和 Apache 2.0 之间隔着什么
HydraDB 用 AGPL-3.0,RustFS 用 Apache 2.0,两个协议在同一套架构里各管一层,正好把边界讲清楚。AGPL-3.0 最出名的是网络条款:把修改过的版本当网络服务对外提供,需要向服务使用者提供你修改后的源码;原样部署不改代码、仅内部使用,义务基本不触发;想把一个 AGPL 组件嵌进闭源产品,传染性条款就会约束整个组合作品。Apache 2.0 没有这条网络条款,闭源嵌入、商用分发都留有余地,附带的专利授权也让集成方踏实。
组合视角下两层互不牵连:查询节点和索引器是"你的业务代码加一个 AGPL 图库",要不要开自己的修改源码,取决于你怎么拿它对外服务,这是应用层的事;持久层是独立的 RustFS 进程,Apache 2.0,不受图库协议传染。也正因为持久层独立,把 RustFS 换成任何 S3 兼容存储的迁移成本,比换一个存储格式耦合死的数据层低得多。具体义务的认定要法务说了算,这里只把两条协议的行为差异摆出来。
什么图值得放进桶里
值得看的场景有三个共同点:图够大、写够慢、团队已经养着对象存储。超大图的低频多跳查询是它最舒服的区间,官方数字里单跳热查 408µs,扇出 10000 的 20 跳也只要 11ms,读吞吐峰值 16805 q/s(32 workers、单跳、扇出 100);计算层随起随停,图数据零搬迁;灾备从一套图库备份流程变成桶复制,RustFS 的桶复制和站点复制正好接住。反过来,三种情况别上:低延迟高频写(每次提交都是一次对象存储往返,微秒级写不存在);写吞吐敏感的负载(单 cell 单写者,194 到 229 writes/s 的天花板不会因为加机器抬高);超热点边的反复更新(官方 scaling 数据里,扇出 10000 的多跳吞吐在 4 到 32 worker 之间几乎不动,热点在哪里塌就塌在哪里)。
风险前置说清楚,且都在项目侧、不在存储侧。这是 2026 年 7 月创建的项目,v0.2.0 是唯一 release 且无任何附件,没有预编译二进制,源码构建要求 Rust 1.91 以上、libcypher-parser、SuiteSparse GraphBLAS 三个依赖;API 和部署面大概率还会动。benchmark 是官方自测:MinIO 后端、单机、2026-08-12 的数据,没有给 RustFS 的数字,公开基准没有的数字就得自己测。
接 RustFS 先看这组变量
仓库没有给"接任意 S3 后端"的傻瓜式文档,但配置面真实存在:scripts/minio_smoke.sh起本地 MinIO 容器时写进环境文件的就是这组变量,RustFS 同为 S3 兼容端点,换地址换密钥走同一条路:
CLOUD_PROVIDER=awsAWS_ACCESS_KEY_ID=<rustfs-access-key>AWS_SECRET_ACCESS_KEY=<rustfs-secret-key>AWS_DEFAULT_REGION=us-east-1AWS_ENDPOINT=http://<rustfs-host>:9000AWS_BUCKET=<graph-bucket>AWS_ALLOW_HTTP=true# 仅限内网明文测试,生产走 TLS 就删掉这行AWS_VIRTUAL_HOSTED_STYLE_REQUEST=false# path-style 寻址,与 RustFS 默认一致跑通的顺序照 README 来:先just minio-smoke确认 S3 路径本身是通的,再把这组变量指向 RustFS 桶重跑一遍,最后用just minio-fence的同款 fencing 测试验证租约语义在 RustFS 上成立。三个测试都绿了,这张图的持久层就算在 RustFS 上落了地。注意这些变量取自 v0.2.0 时期的脚本,项目早期配置项可能随版本变动,动手前以仓库当前文档为准。
相关入口:HydraDB 仓库、官方 benchmark 站、architecture.md。RustFS 的 S3 接入与桶复制细节见官方文档。