今天在 GitHub Trending 上看到一个很有意思的项目:HydraDB,一个把图数据库直接建在对象存储(S3)上的 Rust 实现。它想解决的问题很清晰——传统图数据库的存储与计算耦合在一起,扩缩容时往往要搬数据;而 HydraDB 让对象存储成为唯一的持久化真相源,计算和存储彻底解耦。
一、项目概述
HydraDB 是一个 对象存储原生的分布式图数据库(object-store-native distributed graph database),用 Rust 编写,采用 AGPL-3.0 许可证,要求 Rust 1.91+。
它的核心主张可以概括为一句话:S3 兼容的对象存储就是图本身,计算节点只是可随时替换的无状态外壳。
主要特性包括:
- 对象存储持久化:图记录、WAL(预写日志)、manifest、不可变遍历索引全部落在 S3 兼容存储上。
- 存算分离:数据节点(
graph-node)负责查询和规范化变更,索引器(graph-indexer)在后台异步构建不可变遍历索引;两者都只保留可丢弃的本地缓存(内存 / 本地 SSD / NVMe),可以随时替换或扩缩容而不迁移图数据。 - 安全的写入切换:用对象存储 CAS(Compare-And-Swap)租约选出每个 cell 的活跃写入者,同时用 SlateDB 的 writer epoch 隔离掉陈旧写入者(fencing)。
- 一致读:每个查询都绑定到一个固定的 SlateDB 快照;索引遍历会把编译好的 CSC 生成(compiled CSC generation)与其可见的 WAL 覆盖层(WAL overlay)结合。
- 图原生执行:查询规划器会利用属性索引、反向邻接、稀疏遍历,并在合适处使用 SuiteSparse GraphBLAS。
- 熟悉的客户端:应用可以用 Neo4j 驱动走 Bolt 5.x,或者使用类型化的 JSON / 流式 NDJSON 的 HTTP API。
- 有界运行:鉴权、授权、截止时间、结果限制、背压、取消、缓存预算、指标和链路追踪都是服务运行时的一部分。
二、技术原理
架构:三层、存算彻底分离
HydraDB 的拓扑可以分为三块:
- 数据层(Data tier)— graph-node:运行「查询 + 变更引擎」,每个节点拥有私有的本地 SSD/NVMe 缓存。多个 graph-node 无状态地横向扩展,共享同一份对象存储。
- 索引层(Indexing tier)— graph-indexer:异步构建不可变的 CSC 生成(CSC generation),并通过原子对象存储指针发布。当某个索引缺失或落后时,读者仍然正确——因为可见的 WAL 尾部会被应用到索引基线上。
- 对象存储(S3-compatible):整个集群共享的底层,也是图唯一持久化副本,保存 WAL、SST、租约、CSC 生成等。
这种设计带来一个很好的性质:一个监听中的端口不代表节点可用,一次往返成功的写入才代表可用(README 反复强调这一点,并在验证步骤中专门区分)。
持久化与写入协调
HydraDB 在底层用 SlateDB 作为对象存储之上的 KV/表引擎。写入协调靠两层机制保证安全:
- CAS 租约:对象存储的 Compare-And-Swap 语义为每个 cell 选出唯一活跃写入者;
- Writer epoch fencing:SlateDB 的写入者 epoch 会隔离(fence)掉迟到的旧写入者,防止脑裂导致的脏写。
查询执行与 GraphBLAS
查询语言是 OpenCypher 的一个实用子集,支持类型化关系、有界变长路径、属性与 label 谓词、排序、分页、聚合、OPTIONAL MATCH、UNION、批量 UNWIND 写入。
在遍历内核上,HydraDB 同时提供自研的 Rust 稀疏实现和 SuiteSparse GraphBLAS 两种执行路径。从 build.rs 可以看到它对 GraphBLAS 链接路径做了细致处理,在 macOS 上自动从 Homebrew 的 suite-sparse 解析 libgraphblas:
// build.rs:只负责告诉链接器 GraphBLAS 的位置
fn graphblas_link_search_paths() -> Vec<String> {
if let Some(dir) = std::env::var_os("GRAPHBLAS_LIB_DIR") {
// 1. 显式覆盖优先
...
}
for package in ["GraphBLAS", "graphblas"] {
let dirs = pkg_config_link_dirs(package);
if !dirs.is_empty() { return dirs; }
}
if cfg!(target_os = "macos") {
if let Some(prefix) = command_stdout("brew", &["--prefix", "suite-sparse"]) {
// 3. macOS 上回退到 brew 前缀
...
}
}
Vec::new()
}
读一致性的两种模式
| 模式 | 行为 |
|---|---|
causal(默认热路径) | 使用节点当前的持久化读者视图,在提供的 bookmark 要求更新序列时刷新 |
strong | 在固定查询快照前先从对象存储刷新 SlateDB 读者,付出对象存储新鲜度成本 |
HTTPS 请求在请求体中设置 "consistency": "causal" 或 "strong";Bolt 客户端在 RUN 元数据或事务元数据里设置 consistency。
三、安装与快速开始
HydraDB 提供两种起一个单机开发节点的方式:官方 Docker 镜像,或从源码构建。默认部署环境要求 TLS,本地验证时显式开启明文。
方式一:Docker(最快,无需本地工具链)
镜像发布在 ghcr.io/hydra-db/hydradb,支持 linux/amd64 和 linux/arm64(注意:0.1.0 及之前只有 amd64,Apple Silicon 需要 0.1.0 之后版本或加 --platform linux/amd64 模拟)。
mkdir -p hydradb-data/store hydradb-data/cache
printf '%s\n' 'local-development-token-32-bytes' > hydradb-data/auth-token
docker run --rm \
--user "$(id -u):$(id -g)" \
-p 7687:7687 -p 8443:8443 -p 9090:9090 \
-v "$PWD/hydradb-data:/data" \
-e CLOUD_PROVIDER=local \
-e LOCAL_PATH=/data/store \
-e GRAPH_NAMESPACE=default \
-e GRAPH_ID=default \
-e GRAPH_CELL_ID=cell-0 \
-e GRAPH_CELLS=cell-0 \
-e GRAPH_NODE_ID=node-0 \
-e GRAPH_BOLT_NODE_ADDRESSES=node-0=127.0.0.1:7687 \
-e GRAPH_ADVERTISED_BOLT_ADDR=127.0.0.1:7687 \
-e GRAPH_DATA_CACHE_DIR=/data/cache \
-e GRAPH_AUTH_TOKEN_FILE=/data/auth-token \
-e GRAPH_ALLOW_PLAINTEXT=true \
-e RUST_MIN_STACK=33554432 \
ghcr.io/hydra-db/hydradb:latest
注意 --user "$(id -u):$(id -g)" 是必需的:镜像以 UID/GID 10001 运行,但挂载目录归宿主机用户所有,不加这个参数容器会在首次存储操作时写入失败。
方式二:从源码构建
前置要求:Rust 1.91+、C/C++ 工具链、libcypher-parser、SuiteSparse GraphBLAS。
# Ubuntu / WSL
sudo apt-get install -y build-essential clang libclang-dev cmake pkg-config \
libcypher-parser-dev libgraphblas-dev curl git python3 python3-venv
# macOS (Homebrew)
brew install just cmake pkg-config llvm suite-sparse
brew install cleishm/neo4j/libcypher-parser
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 克隆并冒烟测试
git clone https://github.com/hydra-db/hydradb.git
cd hydradb
just native-check
just smoke
验证节点真的可用
监听端口不等于可用,README 用一个最小 HTTP 往返验证:
TOKEN='local-development-token-32-bytes'
curl -sS http://127.0.0.1:8443/v1/graphs/default/query \
-H "Authorization: Bearer $TOKEN" \
-H 'X-Graph-Namespace: default' \
-H 'Content-Type: application/json' \
--data '{"cell_id":"cell-0","query":"CREATE (a {id: 1})-[:FOLLOWS]->(b {id: 2})"}'
curl -sS http://127.0.0.1:8443/v1/graphs/default/query \
-H "Authorization: Bearer $TOKEN" \
-H 'X-Graph-Namespace: default' \
-H 'Content-Type: application/json' \
--data '{"cell_id":"cell-0","query":"MATCH (a {id: 1})-[:FOLLOWS]->(b) RETURN b.id AS id"}'
第二个调用返回一行 {"type":"vertex_id","value":2}。一次往返成功的写入,才是节点工作的真正证据。
四、使用方法与实战
用 Neo4j 驱动连接
应用侧可以直接用现成的 Neo4j 驱动,路由 URI 形如:
neo4j://127.0.0.1:7687
生产环境用 neo4j+s://(公开可信证书)或 neo4j+ssc://(自签名开发证书)。直接 bolt:// 节点地址仅用于诊断和定向故障测试;可写集群客户端应使用路由协议。
原生路径过程(path procedures)
HydraDB 内置了快照作用域的原生路径过程,避免客户端侧查询扇出(query fan-out):
algo.SPpaths:单个源到单个目标的有界路径;algo.SSpaths:从单个源出发的有界路径;algo.MSpaths:解析多个索引的源/目标值并一起求值。
CALL algo.MSpaths({
sourceLabel: 'Entity',
sourceProperty: 'name',
sourceValues: ['alpha', 'beta', 'gamma'],
targetValues: ['alpha', 'beta', 'gamma'],
pairwise: true,
relTypes: ['RELATES'],
relDirection: 'both',
maxLen: 3,
pathCount: 5,
fairRelationshipVariants: true,
resultLimit: 100
})
YIELD path
RETURN path
这些过程都基于一个固定的存储快照、可用的编译 GraphBLAS 拓扑、可见的 WAL 覆盖层,以及有界元数据水合(bounded metadata hydration)。
在 Kubernetes 上部署
Helm chart 部署了查询节点、索引器工作负载、服务、缓存卷、网络策略、中断预算、TLS 资源、鉴权和可选的 Prometheus 集成:
helm upgrade --install hydradb charts/hydradb \
--namespace hydradb \
--create-namespace \
--values charts/hydradb/examples/values-eks.yaml \
--atomic \
--timeout 15m
可观测性
- 公共 HTTP 服务暴露
GET /healthz; - graph-node 和 indexer 的管理服务暴露
GET /readyz与GET /metrics; - 运行时为查询指纹、访问路径、缓存命中、一致性模式、作用域、cell、存储序列、规划器决策等发出结构化追踪字段。
五、常见问题与解决方案
| 现象 | 原因与修复 |
|---|---|
No available formula with the name "libcypher-parser" | Homebrew 没有这个 formula,用 tap:brew install cleishm/neo4j/libcypher-parser |
command not found: rustup-init | Homebrew 的 rustup 是 keg-only 且不再提供 rustup-init,改用官方安装脚本 |
invalid environment variable CLOUD_PROVIDER value 'null' | CLOUD_PROVIDER 未设置,null 表示缺失而非字符串;local 还需要已存在的 LOCAL_PATH 目录 |
wrapper.h:4:10: fatal error: 'cypher-parser.h' file not found | macOS 上直接调 cargo 时未设置 BINDGEN_EXTRA_CLANG_ARGS;优先用 just,它会自动导出 |
节点响应 /readyz 后,首个查询 has overflowed its stack 崩溃 | 未设置 RUST_MIN_STACK,需 export RUST_MIN_STACK=33554432(32MB) |
curl: (7) Failed to connect ... port 9090 | 节点没在跑,graph-node 占用前台,要在独立 shell 中启动 |
补充一个 macOS 上从源码运行的关键坑:直接调 cargo run 时不会继承 justfile 导出的环境变量,需要手动导出:
export BINDGEN_EXTRA_CLANG_ARGS="-I$(brew --prefix)/include"
export LIBRARY_PATH="$(brew --prefix)/lib"
六、总结
HydraDB 的设计哲学很值得关注:把对象存储当成数据库本身,而不是缓存或冷备。 通过 SlateDB 在 S3 之上提供 KV 抽象、用 CAS 租约 + writer epoch 做安全的写入协调、把计算拆成无状态的数据节点和后台索引器,它在「存算分离」这件事上比很多同类走得更彻底。
对想尝鲜的 Rust / 分布式系统爱好者来说,它有几个加分项:用 Neo4j 兼容的 Bolt 协议降低迁移成本、内置 GraphBLAS 做图原生遍历、文档里还附带了正确性案例集、Quint 形式化验证和 Jepsen 一致性报告——这在早期开源图数据库里相当少见。
如果你正在评估「对象存储原生」架构,或者想找一个能直接在 S3 上跑、又能用现成图查询语言的存储引擎,HydraDB 值得放进候选清单。
- 仓库地址:https://github.com/hydra-db/hydradb
- 官方基准:https://hydra-db.github.io/benchmark/
- 架构文档:
architecture.md - Kubernetes 部署:
charts/hydradb/README.md