OpenViking 多写存储(Multi-Write)实战指南:Primary + Backup 复制架构、S3 兼容后端与读加速配置
OpenViking 的多写存储(Multi-Write Storage)允许一个 primary 后端在统一文件系统抽象下同时向多个 backup 后端复制写入,用于高可用、跨区域副本、读加速和存储迁移。本文以 多写存储指南 为主线,结合 RAGFS 多后端源码 深入讲解从最小配置到生产级 S3 备份、同步/异步一致性选择、Redirect/Exclude 策略、加密与存量迁移的完整链路;读完本文,你将掌握在 ov.conf 中配置任意数量 backup、正确接入 S3 兼容服务、让备份参与读路由并排查常见故障的实战能力。
多写逻辑全部位于 RAGFS 内部:OpenViking 的 Python SDK、HTTP API 和 CLI 使用方式保持不变,read()、write()、ls()、stat() 等接口语义不变,调用方无需关心文件最终落在哪个底层后端(参见 多写存储概念)。
核心模型:Primary 与 Backup
多写存储由一个 primary 和多个 backup 组成,角色与配置位置如下:
| 角色 | 配置位置 | 说明 |
|---|---|---|
| primary | storage.agfs.backend |
权威写入目标,也是读取兜底 |
| backup | storage.agfs.backups.items[] |
接收复制写入,可选参与读取 |
没有配置 backups 时,OpenViking 继续使用原有单后端模式——从源码看,PluginConfig.backups 字段的默认值是 None(crates/ragfs/src/core/types.rs),这正是"未配置即单后端"的实现依据。
默认写入路径为:
Client
-> OpenViking API
-> RAGFS MultiWrite
-> primary
-> backup1 / backup2 / ...
从源码结构看,多写核心是 MultiWriteWrappedFS(crates/ragfs/src/core/multibackend_wrapper.rs):BackendEntry 中 primary 恒为索引 0,其余均为 backup,写入通过 execute_write 完成"primary 写入 → 写 sync 日志 → backup 扇出"的整条管线。backup 未配置 operations 时默认参与写入(participates_in_write() 在 operations 为空时返回 true),这样用最少配置即可得到冷备能力。
前置条件
开始配置前确认以下事项:
- 已有可用的
ov.conf。 - 已确认 primary backend 可以正常读写。
- 如果要接入 S3 兼容存储,已准备好 bucket、endpoint 和访问凭据。
- 如果要迁移已有数据,先完成存量数据迁移,再启用多写(多写只复制启用之后的新写入)。
最小配置:本地目录双写
下面示例使用本地目录作为 primary,并把写入复制到另一个本地目录:
{
"storage": {
"workspace": "./data",
"agfs": {
"backend": "local",
"backups": {
"sync_type": "async",
"items": [
{
"name": "local-backup",
"backend": "local",
"local": {
"workspace": "./data/backup"
}
}
]
}
}
}
}
说明:
- 顶层
backend是 primary。 backups.items[]是 backup 列表。name是 backup 的稳定身份,后续同步元数据会引用它;源码中validate_redirect_targets会逐一校验 redirect 的target是否能在 backup 列表中按name找到(crates/ragfs/src/multibackend/config.rs),找不到即报配置错误。backend = "local"的 backup 使用local.workspace指定本地目录。sync_type不配置时默认按异步模式理解——BackendsConfig.sync_type的默认值在源码中定义为"async"(crates/ragfs/src/core/types.rs)。
多 Backup 配置
可以配置多个 backup。下面示例同时写入本地副本和 S3 兼容对象存储:
{
"storage": {
"workspace": "./data",
"agfs": {
"backend": "local",
"backups": {
"sync_type": "async",
"items": [
{
"name": "local-az2",
"backend": "local",
"local": {
"workspace": "./data/local-az2"
}
},
{
"name": "object-store",
"backend": "s3",
"s3": {
"bucket": "openviking-backup",
"region": "us-east-1",
"endpoint": "https://s3.example.com",
"access_key": "your-access-key",
"secret_key": "your-secret-key",
"prefix": "openviking",
"directory_marker_mode": "none"
}
}
]
}
}
}
}
建议:
name不要使用会频繁变化的机器名或临时编号。- backup 的底层路径或 bucket 应避免与 primary 指向同一物理位置。
- 修改 backup
name会影响历史同步元数据的识别,生产环境应谨慎变更。
从源码看,backup 的 name 还有两项硬性约束:不能使用保留名 "primary",且整个列表中不允许重名(crates/ragfs/src/multibackend/factory.rs)。每个 backup item 支持 timeout、encryption、operations、excludes 等可选字段(crates/ragfs/src/core/types.rs)。
S3 兼容存储注意事项
使用 S3 兼容服务(MinIO、RustFS、Ceph 等)时,s3 段需要额外配置以下字段:
| 字段 | 是否必填 | 说明 |
|---|---|---|
use_path_style |
大多数 S3 兼容服务必填 | 设置为 true 使用路径风格 URL(http://host/bucket/key)。大多数 S3 兼容服务需要此配置。 |
directory_marker_mode |
S3 兼容服务必填 | 必须显式设置为 "none"。如果不配置,RAGFS Rust binding 启动时会报 AGFSConfigError: invalid directory_marker_mode: null 并静默崩溃。 |
use_ssl |
可选 | HTTP 端点(如 http://localhost:9000)需要设置为 false。 |
S3 兼容存储最小示例(RustFS/MinIO):
{
"name": "s3-backup",
"backend": "s3",
"s3": {
"bucket": "my-bucket",
"endpoint": "http://localhost:9000",
"access_key": "your-access-key",
"secret_key": "your-secret-key",
"prefix": "openviking",
"use_ssl": false,
"use_path_style": true,
"directory_marker_mode": "none"
}
}
为什么需要
directory_marker_mode?S3 兼容存储服务对"目录"的处理方式与 AWS S3 不同。RAGFS Rust binding 必须知道创建目录时是否需要写入目录标记对象。合法取值为
"none"、"empty"和"nonempty"。对于不使用目录标记的 S3 兼容服务(RustFS、MinIO、Ceph 等),设置为"none"。如果省略,Rust binding 默认值为null(不合法),导致服务端在启动时静默崩溃,报错AGFSConfigError: invalid directory_marker_mode: null。
这一行为在源码中有明确证据:S3FS 插件在 validate 阶段只接受 none、empty、nonempty 三种取值,其它值直接返回 invalid directory_marker_mode 配置错误(crates/ragfs/src/plugins/s3fs/mod.rs);而 use_path_style 的默认值在客户端构造时是 true(crates/ragfs/src/plugins/s3fs/client.rs),directory_marker_mode 的插件级默认值是 "empty"(crates/ragfs/src/plugins/s3fs/mod.rs)——因此在对接不使用目录标记的 S3 兼容服务时,显式写 "none" 是避免启动崩溃的关键。prefix 可用于同一 bucket 内的命名空间隔离(如 agfs/)。如果未显式提供 access/secret key,SDK 会走默认凭证链。
Docker 网络配置
在 Docker 中运行 OpenViking 并配置同主机的 S3 备份时,需要注意:
- Linux Docker:使用
--network host或宿主机局域网 IP。Docker bridge 网络可通过网关 IP(如172.17.0.1:9000)访问宿主机局域网。 - macOS/Windows Docker Desktop:
--network host不支持。S3 端点使用host.docker.internal(映射为宿主机的 localhost),或使用宿主机局域网 IP。
如果启用 S3 备份后服务静默崩溃,请优先排查 Docker 网络。RAGFS Rust binding 在容器内无法访问 S3 端点时会报 dispatch failure 错误。
同步模式选择
多写支持两种一致性模式,由 backups.sync_type 控制:
| 模式 | 配置值 | 行为 | 适用场景 |
|---|---|---|---|
| 异步多写 | async |
primary 写成功后立即返回,backup 后台同步 | 低延迟写入、最终一致 |
| 同步多写 | sync |
primary 写成功后等待 backup 确认 | 更强写入确认、可接受额外延迟 |
异步模式
异步模式适合大多数场景。
{
"backups": {
"sync_type": "async",
"items": []
}
}
特点:
- primary 写入成功后立即返回。
- backup 写入在后台执行。
- 写入延迟低。
- backup 可能短暂落后。
适合:
- 写入吞吐优先。
- backup 主要用于灾备。
- 可以接受最终一致性。
从源码看,异步模式通过 fanout_async 以后台任务方式扇出写入,并使用 PathSerializer 按"路径 + backup 名"串行化同一路径的多次写入,防止 backup 端乱序应用(crates/ragfs/src/multibackend/meta.rs)。build 时若存在 write-enabled backup,会自动拉起后台 retry_loop 兜底修复(crates/ragfs/src/core/multibackend_wrapper.rs),重试间隔默认 30 秒、退避基数默认 1000ms、每轮每目标最多 3 次、连续失败 9 次后进入隔离(quarantine)(crates/ragfs/src/multibackend/factory.rs)。
同步模式
同步模式会等待 backup 确认。
{
"backups": {
"sync_type": "sync",
"write_ack_count": 1,
"write_ack_timeout_ms": 5000,
"items": []
}
}
参数说明:
| 参数 | 说明 |
|---|---|
write_ack_count |
写入返回前至少需要多少个 backup 确认 |
write_ack_timeout_ms |
等待 backup 确认的超时时间,单位毫秒 |
特点:
- 写入确认更强。
- 写入延迟受 backup 影响。
- 未确认的 backup 会继续由后台重试修复。
- primary 已写成功但 backup 未达确认数时,客户端可能收到错误;此时 primary 中可能已经存在数据。
适合:
- 希望尽量减少 primary 与 backup 的确认窗口。
- backup 延迟可控。
- 调用方能接受同步写入带来的额外延迟。
源码中,sync_type 为 sync 时 fanout_sync 会并行向所有目标写入并等待达到 write_ack_count 个确认(不足时返回 SyncWriteQuorum 错误),write_ack_timeout_ms 对应确认等待超时(crates/ragfs/src/multibackend/config.rs、crates/ragfs/src/core/multibackend_wrapper.rs)。值得注意的是:同步模式并不代表"全有或全无",未达到确认数的 backup 会留在 .sync_log.json 中,由后台重试继续修复;因此调用方收到失败时 primary 数据可能已存在。
BackendsConfig 还提供以下可选的调优字段(crates/ragfs/src/core/types.rs):
| 参数 | 说明 |
|---|---|
write_concurrency |
异步写入的并发上限(通过信号量控制) |
retry_interval_ms |
后台重试循环间隔(默认 30000) |
retry_backoff_base_ms |
重试退避基数(默认 1000) |
retry_max_retries_per_round |
每轮每文件/目标最大重试次数(默认 3) |
retry_quarantine_after_failures |
连续失败多少次后隔离该文件/目标对(默认 9) |
配置读加速
backup 默认不参与读取。要让 backup 服务读取,需要显式配置 operations。
{
"name": "cache-backend",
"backend": "memfs",
"operations": [
{
"operation": "read",
"priority": 10
}
]
}
读取优先级规则:
priority越小越优先。- 只有声明
read的 backup 才参与读取。 - primary 始终作为最终兜底。
- 冷备 backup 不建议配置读能力。
如果一个 backup 只配置了 read,没有配置 write,它不会接收普通多写复制。只有在你明确知道该 backend 的数据来源时,才应使用这种配置。
源码中的读取路径与上述规则完全一致(crates/ragfs/src/core/multibackend_wrapper/routing.rs):
1. 按 priority 升序访问 read-enabled backup
2. 回退到 primary
3. 如果文件被 redirect,则访问 redirect target
4. 仍未命中则返回 NotFound
read_backups_sorted() 按 priority 升序(缺失则视为最大值)排序(crates/ragfs/src/core/multibackend_wrapper.rs)。读路由还会记录 backup/primary/redirect/miss 命中等指标(read_route_metrics),可用于运维观测各后端读取命中情况。这种设计避免冷备节点默认参与读取,降低读到旧数据的风险。
Redirect 配置
Redirect 用于把匹配的文件写入指定 backup,而不是写入 primary。
按扩展名重定向:
{
"storage": {
"agfs": {
"backend": "local",
"redirects": [
{
"type": "FileExtensionPolicy",
"extensions": ["(pdf|ppt|zip)"],
"target": ["object-store"]
}
],
"backups": {
"items": [
{
"name": "object-store",
"backend": "s3",
"s3": {
"bucket": "openviking-large-files",
"endpoint": "https://s3.example.com"
}
}
]
}
}
}
}
按大小重定向:
{
"type": "FileOverSizePolicy",
"max_size_mb": 100,
"target": ["object-store"]
}
注意:
target必须引用已有 backup 的name。- redirect 文件仍会通过普通 API 呈现为可读、可列举、可查询状态。
- redirect 映射保存在 primary 的内部元数据中。
从源码看,Redirect 策略配置在 primary 上(PluginConfig.primary_redirects),策略匹配发生在写入管线的最前端:execute_write_with_redirect 先评估 check_redirect(path, size),命中后直接把文件写入第一个可用 target,再把映射记录到 .redirect.json 内部元数据中(crates/ragfs/src/core/multibackend_wrapper.rs)。FileOverSizePolicy 按 max_size_mb * 1024 * 1024 字节比较,FileExtensionPolicy 的 extensions 是正则模式,会先按正则匹配文件名、失败时退化为后缀匹配(crates/ragfs/src/core/multibackend_wrapper.rs)。用户执行 ls()、stat()、read() 时仍能看到正常的文件系统视图,因为目录列表会把 redirect 条目合并回来。
常见用途:大文件进入对象存储、特定后缀文件进入专门 backend、主存储只保存常规内容。
Exclude 配置
Exclude 用于让某个 backup 跳过匹配文件。
{
"name": "cache-backend",
"backend": "memfs",
"excludes": [
{
"type": "FileOverSizePolicy",
"max_size_mb": 50
},
{
"type": "FileExtensionPolicy",
"extensions": ["(mp4|zip)"]
}
]
}
常见用法:
- 缓存 backend 排除大文件。
- 低成本备份排除无需保存的文件类型。
- 某个 backup 只保存文本或配置类资源。
如果 redirect 的目标 backup 同时 exclude 了该文件,说明配置互相冲突。请优先修正配置,不要依赖系统自动猜测其他目标。
源码层面的规则是:Exclude 策略配置在 backup 上,只影响该 backup 是否接收写入;write_targets() 在扇出前会过滤掉命中 exclude 的 backup(crates/ragfs/src/core/multibackend_wrapper.rs)。同时,配置校验器会拒绝在 exclude 策略中携带 target 字段,防止无意义的目标指向(crates/ragfs/src/multibackend/config.rs)。
加密配置
多写存储复用 OpenViking 的透明静态加密能力。
全局加密开启示例:
{
"encryption": {
"enabled": true,
"provider": "local",
"local": {
"key_file": "~/.openviking/master.key"
}
},
"storage": {
"workspace": "./data",
"agfs": {
"backend": "local",
"backups": {
"items": [
{
"name": "plain-cache",
"backend": "memfs",
"encryption": {
"enabled": false
}
},
{
"name": "encrypted-backup",
"backend": "local",
"local": {
"workspace": "./data/encrypted-backup"
},
"encryption": {
"enabled": true
}
}
]
}
}
}
}
规则:
- 全局
encryption.enabled=true时,primary 必须加密。 - backup 可以通过
encryption.enabled单独控制是否加密。 - Python SDK、HTTP API 和 CLI 不需要处理加解密。
.redirect.json和.sync_log.json等内部元数据会跟随 primary 加密策略。
源码中,validate_primary_encryption_flags 会在全局加密开启时强制 primary 的 server_encryption_enabled 与 primary_encryption_enabled 均为 true,否则直接报配置错误(crates/ragfs/src/multibackend/config.rs);factory.rs 组装时,backup 的加密状态默认继承全局设置(除非 item 显式设置 encryption.enabled=false),并只对支持 replace() 语义的 localfs、s3fs、memfs 三类后端做加密包装(crates/ragfs/src/multibackend/factory.rs、crates/ragfs/src/multibackend/factory.rs)。内部元数据通过 MetaStateStore 全部经由 primary backend(即加密入口)读写,天然继承 primary 的加密策略(crates/ragfs/src/multibackend/meta.rs)。
存量数据迁移
多写只复制启用之后的新写入,不会自动复制历史文件。
推荐迁移流程:
- 停止或冻结写入窗口。
- 使用 OVPack 或其他受控工具把存量数据迁移到目标 backup。
- 校验目标 backend 的数据完整性。
- 配置并启用
storage.agfs.backups。 - 恢复写入。
- 观察同步状态和错误日志。
如果无法冻结写入,可以先做一次全量迁移,再短暂停写做增量校验,最后启用多写。
多写与 OVPack 导入导出 的分工是:OVPack 负责历史数据迁移,多写负责启用后的持续复制。若需在启用多写前先了解存量数据规模与存储整体架构,可参考 存储架构。
验证配置
启动前建议运行:
openviking-server doctor
doctor 是 OpenViking CLI 的原生子命令,用于校验子系统并输出可操作诊断,会检查配置中的未知或非法字段(openviking_cli/doctor.py)。
启动后可以用普通文件 API 验证:
openviking write viking://resources/multiwrite-check.txt \
--content "multi-write check" \
--wait
openviking read viking://resources/multiwrite-check.txt
viking:// URI 是 OpenViking 的统一资源定位格式(viking://<scope>/<path>,resources 为独立资源作用域),write/read 等命令直接复用普通文件 API(openviking_cli/utils/uri.py)。
如果使用本地 backup,可以直接检查 backup 目录中是否出现对应文件。生产环境更推荐使用系统健康检查和同步状态命令。除此之外,还可通过读路由指标(read_route_metrics 中的 backup/primary/redirect 命中计数)从运维侧确认备份确实在参与读写(crates/ragfs/src/core/multibackend_wrapper/routing.rs)。
常见问题
为什么 backup 没有参与读取?
backup 默认只参与写入,不参与读取。需要在 backup 上显式配置:
{
"operations": [
{
"operation": "read",
"priority": 10
}
]
}
为什么启用多写后历史文件没有出现在 backup?
多写只处理启用后的新写入。历史文件需要先通过 OVPack、对象存储复制或后续 backfill 能力迁移。
异步模式下能否保证立即读到 backup 的最新数据?
不能。异步模式只保证最终一致。需要强读一致时,应让读取回退到 primary,或避免让可能滞后的 backup 参与读路由。
内部元数据文件会出现在用户列表里吗?
不会。.redirect.json 和 .sync_log.json 是内部文件,会被普通目录列表隐藏。源码中 is_hidden_internal_name 将这两个文件名(连同 .path.ovlock、.exact.ovlock.* 锁文件)统一判定为隐藏内部名(crates/ragfs/src/core/internal_names.rs),它们也不应通过公开 API 直接读写。
sync 模式返回失败是否表示 primary 一定没写入?
不是。primary 写成功但 backup 未达到确认数时,客户端可能收到失败。此时 primary 数据可能已经存在,落后的 backup 会由后台重试修复。
已知限制
- 异步模式下 backup 可能短暂落后。
- 启用多写前的历史文件需要单独迁移或回填。
- redirect 文件依赖内部元数据恢复目录视图。
- 多进程同时写同一 primary 时,需要未来的分布式元数据锁能力。
- 热点目录会频繁更新内部元数据,可能带来额外写放大。
相关文档
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00