第 7 章 · 01 storage 双层与虚拟 FS


第 7 章 · 01 storage 双层与虚拟 FS

本节摘要:翻到文件系统的「磁盘层」。openviking/storage/ 是双层设计:上层 viking_fs/ 是虚拟文件系统——封装 AGFS 绑定客户端,提供 viking:// URI 寻址、目录树操作与 sidecar 语义层;下层 vectordb/ 是向量层——承载嵌入与相似度检索。两层各司其职:树管「在哪」,向量管「像什么」。本节精读 VikingFS 的操作面(read/write/mkdir/rm/mv 及其向量级联)、ls 的 1000 条保护线、temp 目录转正协议,再看 queuefs 队列族与已被 Rust 层接管的路径锁事务。

内容来源:原项目源码 openviking/storage/(viking_fs/、content_write.py、semantic_sidecar.py、queuefs/、transaction/、vectordb/、vikingdb_manager.py)

⚠️ 注意:VikingFS 自己不存字节。文件与元数据的真正落地在 AGFS(下一节的 Rust RAGFS),向量在 vectordb 后端——VikingFS 是语义门面:URI 规范化、权限上下文(RquestContext)、sidecar 读写、向量同步编排都在这一层完成。把它理解为「带语义的驱动器接口」比「文件系统实现」更准确。

学习目标

  1. 画出 storage/ 的模块地图:viking_fs、vectordb、vectordb_adapters、queuefs、transaction、ovpack 各管什么。
  2. 说出双层的分工契约:树管「在哪/是什么」,向量管「像什么」,写入时两层如何同步。
  3. 精读 VikingFS 操作面:rm/mv 的向量级联、LS_ALL_NODES 哨兵、temp→persist 协议、snapshot diff。
  4. 理解 ContentWriteCoordinator:直写+回滚与向量化的编排;storage/transaction 为何只剩空壳。
  5. 数清 queuefs 的队列族与并发参数,说清语义 DAG 在队列里的位置。

一、模块地图:先看骨架

ls openviking/storage/ 会看到十几个条目,按职责分四堆:

模块 职责
viking_fs/ 虚拟 FS 门面:URI 寻址、目录树操作、sidecar 读写、向量同步(8 个 mixin 拼装)
content_write.py ContentWriteCoordinator:内容写入编排,直写/批量/标签/语义刷新
semantic_sidecar.py OKF 格式 sidecar 的解析/渲染/写入(第 3 章的落点)
vectordb/ 内置向量引擎的 Python 侧(collection/engine/index/store/meta)
vectordb_adapters/ 4+1 种向量后端适配器(03 节主角)
vikingdb_manager.py 管理器与 VikingDBManagerProxy:绑定请求上下文的租户代理
queuefs/ 嵌入/语义/资源/提交四类队列与 DAG 执行器(上一节已见)
transaction/ 路径锁——已移交 Rust ragfs 层
ovpack/ 快照打包(03 节)

viking_fs/__init__.py 的 docstring 就是这一层的自我介绍:

""" VikingFS: OpenViking file system abstraction layer Responsibilities: - URI conversion (viking:// <-> /local/) - L0/L1 reading (.abstract.md, .overview.md) - Relation management (.relations.json) - Semantic search (vector retrieval + rerank) - Vector sync (sync vector store on rm/mv) """

注意最后一条:rm/mv 时同步向量库。文件系统里删一个文件容易,但向量库里它的记录若不跟着删,检索就会命中幽灵。双层的最终一致性由 VikingFS 亲自编排——这正是「门面」的价值。

二、viking_fs 的操作面:像文件系统一样用

viking_fs/ 由 8 个 mixin 拼成(_ops/_access/_grep/_semantic/_snapshot/_sync/_vector),_ops.py 是最像传统 FS 的部分。看它的方法签名,几乎就是一套带 ctx 的 POSIX:

class _OpsMixin: async def read(self, ...) async def write(self, ...) async def mkdir(self, ...) async def rm(self, ...) async def mv(self, ...) async def stat(self, ...) async def exists(self, uri, ctx=None) -> bool async def glob(self, ...) async def tree(self, ...) async def ls(self, ...) async def create_temp_uri(self, ctx=None) -> str async def persist_temp_tree(self, ...) async def delete_temp(self, ...)

三个设计点值得展开。其一,rm 的级联账本。删除目录前先 _estimate_deleted_count 估算规模,删完文件层再批量清向量层;mv 更讲究——_copy_for_mv/_copy_dir_through_vikingfs/_copy_file_through_vikingfs 三条复制路径,迁完再改写向量记录的 URI(不然第 4 章 search_children 的前缀匹配就断了)。其二,ls 的保护线_base.py 顶部的注释讲了一个真实教训:

# Sentinel node_limit for internal callers that MUST enumerate an entire # directory. ``ls()`` defaults to ``node_limit=1000`` to protect agent-facing # context from being flooded, but internal system operations (parse merge, # temp->final sync, summary DAG, vectorization) must see every child or they # silently drop entries beyond the cap — e.g. a >1000-doc directory ingest only # materializes its first 1000 subdirectories. Pass this explicitly at those # call sites. LS_ALL_NODES = 2**31 - 1

面向 Agent 的 ls 默认最多 1000 条,防止上下文被灌爆;但内部管线(临时目录转正、语义 DAG、向量化)必须看全量,否则超 1000 文档的目录只入库前 1000 个——静默截断是最阴的 bug,于是有了 LS_ALL_NODES 哨兵。同一个 ls,内外两面:对外节流,对内全量。其三,temp→persist 协议。上一节代码仓库入库先落临时目录,persist_temp_tree 负责转正:把 temp 目录原子地合并进目标树(带精确锁迁移),_sync.pysync_tree 做目录级 diff+merge——增量入库复用这条路径,只搬运变化过的文件。

_snapshot.py 则是给「看文件变化」用的:_prepare_snapshot_diff 对前后两版做 UTF-8 解码与行数上限(10 万行)防护,输出 added/deleted/modified/unchanged 四态——第 5 章 memory_diff 的底层原语同款。目录树的全量遍历另有一组常量兜底:单文件 diff 上限 10MB、输出上限 20MB、超时 500ms——所有「看一眼」的操作都有预算,这是面向 Agent 的系统区别于面向人的系统的分寸感。

三、content_write:写入编排与回滚

文件层之上、业务之下的写入编排者是 content_write.pyContentWriteCoordinator。它管四类写入:write(单文件)、batch_write(批量操作,带根校验)、set_tags_write_memory_with_refresh(记忆写入)。批量的形态先规范化(_normalize_batch_operations)、校验批次根(_validate_batch_root)再执行。最有意思的是直写路径的容错:

async def _write_direct_with_refresh(self, ...) async def _rollback_direct_write(self, ...) async def _vectorize_written_file(self, ...) async def _vectorize_semantic_sidecar(self, *, uri, ctx, ...)

写文件成功但向量化失败怎么办?_rollback_direct_write 提供回退;语义刷新走队列(_enqueue_semantic_refresh_changes),_wait_for_queues 支持等待落地并 _raise_refresh_errors 上抛队列错误。写入不是一步,是「文件层落盘 → 语义刷新 → 向量化」的小事务,Coordinator 就是这个小事务的指挥——第 5 章记忆 patch-merge 最终也经由这里落库。

四、transaction 与 queuefs:锁与队列

storage/transaction/__init__.py 如今只剩一个说明:

""" Path-lock management has been moved to the Rust ragfs layer. Python-side lock operations now go through RAGFSBindingClient.pathlock_* methods. """

Python 侧的路径锁事务已整体下沉到 Rust。多步操作(如 mv 目录+改向量)的原子性靠 AGFS 的 pathlock 族方法保证——LockAcquisitionError 异常类型在 Python 与 Rust 之间共享(ragfs-python 绑定在初始化时缓存 Python 异常类,让原生层抛出同一类型)。这是整个项目「重活下沉」策略的缩影:语义在 Python,机制在 Rust/C++,下一节展开。

队列侧的 queuefs/ 在上一节已经上场,这里补齐族谱。QueueManager 单例封装 AGFS QueueFS 插件,四类 NamedQueue 各带并发上限:

def init_queue_manager( agfs, timeout=10, mount_point="/queue", max_concurrent_embedding=10, max_concurrent_semantic=32, max_concurrent_external_parse=4, max_concurrent_add_resource=4, max_concurrent_session_commit=8, ) -> "QueueManager":

语义队列并发 32、嵌入 10、外部解析 4、资源入库 4、会话提交 8——数字背后是资源画像:语义任务轻(LLM 调用为主,IO 等待多)、解析重(CPU/内存),并发配额各按其性。semantic_dag.py 的 DirNode/DagWork 就是挂在语义队列上的执行单元,session_commit_processor 对接第 5 章的 auto-commit。队列本体仍是 AGFS 文件——队列也是文件系统里的目录,这是「一切皆文件」的又一处贯彻:消息持久在 /queue 挂载点下,进程重启队列不丢。

五、两层协作:一个写入的完整旅程

把双层串起来走一遍完整旅程。ov add doc.pdf:ResourceService 编排(上一节)→ 解析器建树 → ContentWriteCoordinator/batch_write 把文件树写进 viking_fs(AGFS 落盘)→ 嵌入队列算向量、语义 DAG 自底向上生成 sidecar → _vectorize_written_file/_vectorize_semantic_sidecar 把 L2 正文与 L0/L1 摘要的向量写进 vectordb 后端(经 VikingDBManagerProxy 绑定租户)。检索时(第 4 章)反着走:向量层出候选,viking_fs 补路径语境与原文。删除时 rm 两层联动清。树是唯一的真相源(在哪、层级、标签、sidecar),向量是派生索引(像什么)——所以 ovpack 能只靠文件层重建全部向量(03 节),所以向量库可换后端而业务无感。

💡 漫游要点:storage 的双层不是「两个数据库」,是一个文件系统范式的一体两面:VikingFS 是带语义的门面(URI 寻址、sidecar、rm/mv 的向量级联、temp 转正、snapshot diff),vectordb 是可插拔的派生索引。ls 的 1000 条默认上限与 LS_ALL_NODES 哨兵是内外有别的节流哲学;路径锁已下沉 Rust(机制下移),语义留在 Python;队列也是文件(/queue 挂载点)。记住一句话:删了向量库可以重建,删了文件层才是真丢数据——下一节看这层「可重建的索引」在 C++ 里怎么长。

本节要点回顾

  • 模块地图:viking_fs(语义门面)/content_write(写入编排)/semantic_sidecar(OKF)/vectordb(内置引擎 Python 侧)/vectordb_adapters(后端适配)/queuefs(队列)/transaction(已空壳,锁在 Rust)/ovpack(快照)。
  • viking_fs 目录八文件各一事:_ops 文件操作、_access 访问控制、_grep 内容检索、_semantic 语义接口、_snapshot 差异、_sync 树同步、_vector 向量同步——mixin 拼装,职责切面干净。
  • temp→persist:persist_temp_tree/sync_tree 做目录级 diff+merge,增量入库同路径;create_temp_uri/delete_temp 配对管理临时区;snapshot diff 输出 added/deleted/modified/unchanged。
  • VikingFS 八 mixin 拼装,docstring 五职责:URI 转换、L0/L1 读取、relations 管理、语义检索、rm/mv 向量同步;向量不同步就会有幽灵命中。
  • rm 先估量再级联清向量;mv 三条复制路径+向量 URI 改写,否则 search_children 前缀匹配断裂;ls 默认 node_limit=1000 防 Agent 上下文灌爆,内部管线用 LS_ALL_NODES=2^31-1 哨兵防静默截断(超千文档目录只入库前 1000 个的真实教训)。
  • snapshot diff 预算:单文件 10MB、输出 20MB、10 万行、超时 500ms——面向 Agent 的「看一眼」都有预算。
  • temp→persist:persist_temp_tree/sync_tree 做目录级 diff+merge,增量入库同路径;snapshot diff 输出 added/deleted/modified/unchanged,10 万行上限防护。
  • ContentWriteCoordinator:write/batch_write/set_tags/记忆写入四类;直写带回滚(_rollback_direct_write),语义刷新走队列,wait_for_queues 可等待可上抛;batch 先 _validate_batch_root 校验根再规范化操作。
  • transaction 空 shell:路径锁整体移交 RAGFSBindingClient.pathlock_*,异常类型跨语言共享(LockAcquisitionError 单例);queuefs 四队列并发配额(语义 32/嵌入 10/外部解析 4/资源 4/提交 8),队列持久在 /queue 挂载点,进程重启不丢。

下一节:02 C++ 原生引擎与 Rust RAGFS——向下一层:src/ 约 1.39 万行 C++ 原生向量引擎(abi3 稳定 ABI、SIMD 运行时检测、LevelDB 持久化)与 crates/ragfs 约 4.6 万行 Rust 聚合文件系统(shell/缓存/git/PyO3)。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U