备份与恢复
Notarium 的备份是镜像内置的命令,而不是从外部复制文件。notarium backup 会在服务持续对外可用的同时组装一个逻辑 ZIP:读取全程可用,写入只在两个短暂的检查点被拦一下。你只需要 Docker 和一个运行中的容器——不必停机。
meta.dbcp /data/meta.db 不是备份。元数据库运行在 WAL 模式下:已提交的行可能还躺在 meta.db-wal 里,而逐个复制出来的文件也不是同一时刻的快照。从这样的副本恢复出来的实例,会悄无声息地丢数据。
做一次备份
docker compose exec -T notarium backup > notarium-20260731.zip
这是一次货真价实的备份,而不是“能不能跑”的探测:命令会先构建归档,再用自带的校验器过一遍,然后才把字节流送出去。-T 标志必不可少——不加它,Compose 会分配一个伪终端,而二进制流无法完好无损地穿过伪终端,最后你拿到的是一个损坏的归档。裸 docker exec 默认不分配伪终端,所以那里不需要这个标志。标准输出严格保留给 ZIP 字节;进度和最后的汇总走 stderr,绝不会污染归档。
用于定时任务
> 重定向有一个陷阱:以最终名字命名的文件在命令开始干活之前就已经被创建出来。如果 Docker 或备份中途挂掉,你手上就会留下一个名字正确、内容却不完整的文件。下面这套发布流程在出错时也是安全的:先写入临时文件,刷盘,再用原子硬链接发布。
backup="notarium-$(date -u +%Y%m%dT%H%M%SZ).zip"
partial="${backup}.partial.$$"
set -eu
umask 077
committed=0
cleanup() { test "$committed" -eq 1 || rm -f "$partial"; }
trap cleanup EXIT
docker compose exec -T notarium backup > "$partial"
sync -f "$partial"
sync -f "$(dirname "$backup")"
committed=1
ln "$partial" "$backup"
if sync -f "$(dirname "$backup")"; then
if rm "$partial"; then
sync -f "$(dirname "$backup")" ||
echo "backup warning: final is durable; partial cleanup fsync failed" >&2
else
echo "backup warning: final is durable; retaining recovery partial $partial" >&2
fi
else
echo "backup warning: final is visible; retaining durable recovery partial $partial" >&2
fi
trap - EXIT
set -e 保证 Docker 或备份一旦返回错误,归档就不会被发布。临时文件名里带着 PID,因此两个同时跑的任务不会争抢同一个文件。临时文件和它所在的目录都会在发布点之前刷到磁盘,而发布本身是一次绝不覆盖的原子硬链接:两个盯着同一个目标名的任务不会互相踩掉对方。
发布点之后的失败只是警告,而不是真的出了事:最终文件已经就位,遇到判断不清的情况,临时文件还会作为备用副本保留下来。哪怕 ln 返回了含义不明的非零退出码,也照样保留——链接有可能就是在进程被打断前刚刚建好的。umask 077 让归档只有属主可读。请把临时文件和最终文件放在同一个文件系统上。
如果容器是用裸 docker run 以 notarium 为名启动的,上面的一切照样成立——只需改一行:
docker exec notarium backup > "$partial"
上面这层包装之所以存在,是因为 stdout 这条通道本身没法发布文件。当归档目录已经挂载进来时——备份共享目录、NFS,或者只读根文件系统旁边挂的一个临时卷——backup --output /path/archive.zip 做的是同一件事:写入目标文件旁边的临时文件、刷盘、校验,再用绝不覆盖的原子硬链接发布;出错时不会在目标名下留下任何东西。这时就不需要 shell 包装了,stdout 里只会输出一份 JSON 汇总。
这个命令要用 docker exec 在已经对外提供服务的那个容器里执行,而不是另起一个容器:要取得一致的快照,就得和活着的应用打配合。
备份什么时候会失败
备份的设计目标是绝不让你悄无声息地拿到一份不一致的归档:快照取不到时,命令直接报错退出,什么都不发布。有两种情况会这样:
- 编辑不断涌入。 组装归档期间数据必须保持不动;与之重叠的写入会触发重试,而在无休止的编辑流下,命令会放弃并报错。实际中你会在繁忙的实例上碰到它——稍后重试即可。
- 正在跑一个耗时的导入或导出。 备份需要两次极短的写入暂停,而长时间运行的任务塞不进这么短的间隙。别把备份安排在批量导入的同一个时间窗口里。
这两种情况都不会伤到服务:写入队列会立刻释放,运维命令绝不会把应用卡住。无论哪种情况,普通的读取在备份的全过程中都保持可用。
归档里有什么
| 归档中的路径 | 这是什么 |
|---|---|
data/meta.db | 账户、会话、成员资格、稳定标识符、版本历史,以及任务状态。 |
data/spaces/ | 作为唯一可信来源的 Markdown,包括智能体记忆和项目标记文件。 |
data/jobs/ | 已完成的产物,以及导入的持久化上传件。 |
manifest.json | 格式版本、时间戳、精确的目录集合,外加每个文件的大小、mtime 和 SHA-256。 |
派生目录 data/engine/ 不在归档之列:索引会在恢复之后从文件重建。未完成的文件里,只有内部文件会被跳过——笔记原子写入的临时文件、只传了一半的导入上传件,以及导出产物的分片。名字以 .part 结尾的普通用户文件仍会留在归档里:它们是正当的文件。
这个 ZIP 里装着来自元数据库的账户和会话状态。请把它当机密对待:上面片段里的 umask 077 会让新归档只有属主可读。
校验
校验不改动任何东西,它应当是每个备份任务的固定一环:
docker compose exec -T notarium backup verify < notarium-20260722.zip
# 裸 docker run 时:
docker exec -i notarium backup verify < notarium-20260722.zip
成功时命令会打印一份 JSON 汇总,并以退出码 0 结束。它会拒绝:不安全的路径和重复路径、清单里没有记录的文件、对不上号的目录集合、大小与哈希不符、时间元数据无效、超出限额,以及未通过的 SQLite 完整性检查。整个过程既不读取也不改动正在使用的数据目录。
哈希能抓住意外损坏,却挡不住蓄意篡改:谁要是能把内容和清单一起换掉,就能通过校验。请把备份存储当作访问受限的可信状态,或者在搬运归档的那一层加上签名或加密。
恢复
恢复是一次离线的灾难操作。它只接受干净、空的数据根目录,绝不会与现有实例合并,也不会覆盖现有实例。
在启动之前准备好一个全新的数据卷,并把服务切到它上面:
set -eu
docker compose stop notarium
# 把旧数据卷挪到一边;在 compose 里挂载一个空的 /data
docker compose run --rm --no-deps -T notarium restore \
< notarium-20260722.zip
docker compose up -d --force-recreate --no-deps notarium
之后必须重新创建容器,而不是简单地启动它:docker compose start 会把旧容器连同它那套旧的挂载配置一起拉起来。在验证恢复出的实例之前,旧数据卷先留着别删。
恢复会先把整个归档完整校验一遍,然后才安装任何内容。如果过程在安装中途被打断,它会留下一个明确的标记:请把那个目标视为一次性的,改为恢复到一个新的空目标,而不是硬着头皮往下推、或者做合并。
恢复之后要检查什么:
- 用备份里的账户登录。
- 打开几个空间,确认地址和标识符都还在。
- 打开一篇你改过的笔记,看看它的历史。
- 检查那些上传件或产物对你来说重要的导入和导出任务。
你要恢复的元数据库必须带着目标构建能够接受的迁移记录表。非空却没有迁移记录表的库会直接失败(fail closed)——恢复既不会去猜它的版本,也不会自作主张地补上标记。参见数据库。
边界
内置命令只支持规范的布局:单一数据根目录,以及存放在 SQLite 文件里的元数据库。如果 META_DB_URL 指向 Postgres,或者笔记放在 DATA_DIR 之外,命令会直接失败,而不是塞给你一份残缺的归档——这时请改用你所用数据库的原生工具,再配合对已挂载目录的快照。
备份和校验的中间文件默认落在 /tmp。流式备份在发布前会自我校验,因此可能临时需要“一份归档加两份展开阶段”的空间;单独的校验则是“一份归档加一份展开”。恢复会把进来的数据流缓冲在临时空间里,但归档本身是直接展开到那个全新的数据根目录中的。如果容器的根文件系统是只读挂载的,或者你的数据量很大,就把 NOTARIUM_BACKUP_TMPDIR 指向一个可写的挂载目录。
压缩后与展开后的输入上限是 64 GiB 和一百万个条目;文件名、ZIP 的内部结构和 manifest.json 另有 32 MiB 的内存上限。受信任的大型部署可以调高 NOTARIUM_BACKUP_MAX_BYTES、NOTARIUM_BACKUP_MAX_ENTRIES 和 NOTARIUM_BACKUP_MAX_METADATA_BYTES——参见环境变量。