架构
本章阐述其内部架构。Dr Strange 以清晰的分层构建,每一层与下一层之间仅有一道狭窄的 接缝,因此引擎既可以嵌入使用,也可以对外提供服务,还可以在存储层替换实现,而不 影响上层。
分层
| 层 | 职责 |
|---|---|
| 存储 | 具备 MVCC 快照的持久键/值,置于一个后端 trait 之后 |
| 缓存 | 解码对象的记忆化,以提交序号打版本戳 |
| 计算 | 计划执行、图算法、混合检索融合 |
| API | Database / PlaneHandle 接口:查询、写入、索引、时间旅行、变更流、快照 |
| 平面 | 为一切数据划定范围的贯穿性命名空间 |
在核心之上,是各个包装层:JSON-RPC Web 后端与仪表盘、各语言 SDK、命令行、MCP 服务, 以及为自然语言查询与导入提供动力的 LLM 层。
提交序号
有一个原语贯穿每一层:提交序号(commit sequence),即赋予每次已提交写入的 单调递增编号。它是存储引擎的 MVCC 版本、缓存的一致性戳、索引伴生文件上的新鲜度戳、 时间旅行读取的地址,也是每个变更事件所携带的序号。靠这一个引擎级的单一时钟,各层 就能保持一致,无需另设协调机制。
存储
图层针对一个后端 trait 编写:begin_read 产生一个稳定的 MVCC 快照,begin_write
产生一个串行化的单写者事务。在这层之上,是一组固定的逻辑表(节点、边、正向/反向
邻接、标签与属性索引、外部键、平面元数据)。键采用大端序,字节序也就是扫描序,
因此每个按平面或按节点划定的范围,都是一段连续的前缀。
默认后端是一个手写的 LSM 引擎:
- 一次写入先追加到预写日志(WAL):一条带长度前缀、CRC32 校验的批记录,在变更
发布之前会先
fsync。写入随后进入一个按提交序号打戳的版本化内存 memtable。 - 当 memtable 超过阈值,就会刷写成一个不可变、有序的 SST 文件(每个 SST 各带 一个布隆过滤器),同时轮换 WAL。
- 一次读取把 memtable 叠加在各 SST 之上(最新的 run 在前),返回其中第一个不高于 该快照的版本。
- 合并压缩(compaction) 把累积的 run 合并为一个,回收已无任何读者能观测到的 版本。
由于每个版本都携带其提交序号,且 SST 不可变,读者在其整个生命周期内不持有任何锁; 旧快照的数据在被合并回收之前,也始终可以访问。
时间旅行与保留
时间旅行几乎是从 MVCC 设计中免费得来的。begin_read_at(seq) 可以把一次读取钉在
任意历史提交序号上;读取路径本来就只返回不高于某个快照的版本,因此无需改动即可
服务这次请求。唯一的要求是历史版本能在合并压缩中存活下来,办法是用一个保留
窗口给合并 GC 设定下限(默认无界;换成有界窗口就是用历史换磁盘空间)。解析
时间戳地址时,会对每次提交记录的墙钟时间做二分查找,定位到对应的那次提交。只有
原生后端保留旧版本,因此时间旅行是原生独有的。
缓存
执行器从不直接接触存储,而是通过一个双层缓存读取。L1 缓存的作用域是单次 查询:在查询的生命周期内,它会记忆化已解码的节点、边与邻接,并都绑定在该查询的 快照上。L2 缓存跨查询共享,是一个有界的、打了版本戳的对象缓存:只有当某个 条目的戳与读者的提交序号一致时,才会返回它。因此任何一次写入,都会让面向更早 快照的旧条目静默失效。正是这条精确匹配序号的规则,让历史读取用缓存也是安全的: 绝不会拿到比请求更新的版本。
计算
执行器在一串行(row)之上运行逻辑计划,每一行携带其当前节点与产生它的路径, 因此一次读取得以返回连通的子图,而非孤立的端点。图算法运行在一个针对平面邻接 构建的稠密索引帧(frame)之上,只读,且只作用于单一快照。混合检索会独立收集 每一路信号,先在每路信号内部对分数做归一化,再按加权和融合成一个结果;其中图 信号是从最强的命中点开始播种扩展的。
索引
向量(HNSW)与关键词(BM25)索引是内存中的注册表,其声明记录在存储中。打开
数据库时,如果伴生文件(.hnsw / .bm25)上打的提交序号戳与数据一致,注册表
就直接从伴生文件加载;不一致则从键/值存储重建——键/值存储才是权威来源。一致性
在提交时维护:写事务先缓冲自己隐含的索引变更,等数据持久化之后,再在写锁保护下
把这些变更应用到注册表。做快照时会把当前活跃的注册表序列化成伴生文件字节;恢复
时如果保留了 id,就能直接重新加载它们,无需重建。
变更流
一个 Database 接受一个提交时刻的观察者。写事务会先缓冲自己对节点与边做的
变更;提交时,一旦数据持久化,这些变更就会按实体折叠、在已提交的快照上解析成
记录(剥离嵌入与内部属性),再交给观察者。Web 层会注册一个观察者,把每个变更集
发布进一个广播通道,由每个 plane.watch WebSocket 订阅者各自抽取。投递是尽力
而为的:落后的消费者会丢弃溢出的部分,而不会拖住写入方。
后端
存储后端是一个编译期选择:原生 LSM 引擎(默认)、遗留的 redb 引擎,或一个用于 测试的内存引擎。依赖旧版本的能力(时间旅行及其保留窗口)只存在于原生后端,因此 也通过特性开关做了门控;读取接缝本身则无论在哪个后端都能编译。
线协议契约
各包装层都讲 JSON-RPC 2.0:drsg serve 后端、MCP 服务与各 SDK 都用它,
接口则由一份 OpenRPC schema 描述——这份 schema 是唯一的权威来源:SDK 由它
生成,服务端也从 rpc.discover 返回它;一旦 schema、分发表与生成的客户端三者
出现分歧,漂移(drift)测试就会失败。变更流订阅是同一协议的一个 WebSocket 扩展。
设计文档
逐层的详细设计笔记位于仓库的 arch/ 目录下——那是本章所概述内容的权威
来源。