Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

架构

本章阐述其内部架构。Dr Strange 以清晰的分层构建,每一层与下一层之间仅有一道狭窄的 接缝,因此引擎既可以嵌入使用,也可以对外提供服务,还可以在存储层替换实现,而不 影响上层。

分层

层职责
存储具备 MVCC 快照的持久键/值,置于一个后端 trait 之后
缓存解码对象的记忆化,以提交序号打版本戳
计算计划执行、图算法、混合检索融合
APIDatabase / 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/ 目录下——那是本章所概述内容的权威 来源。