OpenMAIC 服务端持久化的资产字节怎么备份、回收与保留?
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
当你把 OpenMAIC 部署成服务端持久化(server-backed persistence)形态后,课堂里的图片、音频、视频这些资产字节不再只存在于浏览器 IndexedDB 中,而是落在服务器侧:默认写在 PostgreSQL 的字节列里,也可以整个切到 S3。随之而来的三个运维问题是:这些字节去哪里备份、被删除后由谁回收、以及被删除的字节实际上能保留多久。这篇文章基于仓库文档回答这三个问题,并给出每一步的验证方式。
适用前提:你正在使用或准备使用server-persistenceCompose profile 部署 OpenMAIC(应用容器 + PostgreSQL 两个容器,持久化 HTTP 服务内嵌在应用的/api/persistence,没有独立的持久化服务)。仅浏览器本地运行(不配置DATABASE_URL)的部署不存在服务端资产字节,本文内容不适用。
资产字节落在哪一层
OpenMAIC 的资产存储分两层(见 asset HTTP 契约):
- 注册表(registry):资产 id、归属、媒体类型、元数据、引用计数,始终存放在事务性存储(即 PostgreSQL)中;
- 字节层(byte layer):可插拔。默认是 PostgreSQL 中的一列;配置
ASSET_S3_BUCKET后切换为 S3,对象以内容哈希命名。
也就是说,无论字节层选哪个,注册表都在同一个 Postgres 库里;只有字节本体随ASSET_S3_BUCKET的取值改变位置。
前提:启用 server-persistence 部署
按 README.md 的 “Server-backed persistence (PostgreSQL)” 一节:
cp .env.example .env.local printf '\nDATABASE_URL=postgres://openmaic:openmaic-dev@postgres:5432/openmaic\nPERSISTENCE_DEV_TOKEN=openmaic-local-dev\n' >> .env.local NEXT_PUBLIC_PERSISTENCE=1 NEXT_PUBLIC_PERSISTENCE_TOKEN=openmaic-local-dev docker compose --profile server-persistence up --build两点与本文直接相关的注意事项:
NEXT_PUBLIC_PERSISTENCE是构建期开关,编译进浏览器 bundle;启用了它的构建必须配上可用的运行时DATABASE_URL和PERSISTENCE_DEV_TOKEN,且NEXT_PUBLIC_PERSISTENCE_TOKEN在构建时必须与服务端 token 一致,否则浏览器会选中 HTTP 持久化但内嵌端点报错,首页会显示 persistence-unavailable 提示并保留之前的课程列表。PERSISTENCE_DEV_TOKEN方案没有任何用户隔离能力,文档明确它只适合 localhost 或可信网络的单用户部署;上生产前需要替换 lib/persistence/server-auth.ts 中真实的会话校验。
备份资产字节:备份 Postgres 还是备份 S3 桶
文档没有给出额外的资产专属备份命令,因为备份目标就是字节所在的存储本身。默认形态和 S3 形态各有一个主备份对象:
默认(PostgreSQL 字节列):资产字节就是openmaic数据库的一部分。asset HTTP 契约 明确说明这种形态下“字节会流经预写日志(WAL)和备份,因此备份与复制的体积会随存储的资产规模增长”。因此:
- 用你现有的 Postgres 备份手段(逻辑备份、数据卷
openmaic-postgres的卷级备份)备份整个数据库,资产字节与课程文档、学习者运行时数据一起被覆盖; - 反过来,资产越多,这类备份的体积越大——这也是文档把“把字节层迁到对象存储”列为该形态的后续演进原因,配置时要有心理预期。
S3 字节层:字节在桶里,由 S3 侧的保留/复制策略承担;Postgres 里只剩注册表。文档同时提醒这种形态多出一项家务事:进程崩溃窗口会留下无人引用的对象,需要自行清理或按过期策略处理;由于对象以内容哈希命名,重复写入同字节会幂等覆盖,孤儿对象是无害的。
两种形态下注册表都不可缺:它持有 id 到内容哈希的映射和引用计数,只备份字节而不备份库(或反之)都无法还原完整的资产状态。
一个影响备份策略的运维细节(来自 README.md):PERSISTENCE_POSTGRES_PASSWORD只在数据目录为空时初始化角色,之后改它不会轮换已有卷里的密码。要轮换密码并保留数据,正确做法是连接数据库执行ALTER ROLE openmaic WITH PASSWORD 'new-password';再更新DATABASE_URL。而文档给出的docker compose --profile server-persistence down -v中的-v会删除数据卷,只可用于丢弃型本地库,执行前确认你不需要保留这些数据。
回收:离线 collector 的行为与可调参数
删除或替换某个资产只会删掉注册表条目,字节本身由一个离线 collector回收。关键性质(asset HTTP 契约 “Where the bytes live” 与 README.md):
- 请求路径从不删除字节:写路径先锁定 blob 行再写字节,读路径持共享锁读字节,collector 持排他锁删除,三者互不破坏;
- 每个候选 blob 在自己的事务里
FOR UPDATE加锁并复查后才删,所以多实例同时开 collector 也只是串行化,不会竞态; - 这个部署默认就在跑 collector,不需要任何配置资产存储才会在回收后停止增长。
collector 由应用进程调度(实现见 lib/persistence/asset-collector-schedule.ts),相关配置都在 .env.example 的 “Server-backed Persistence” 一节:
# 仅当 DATABASE_URL 已配置时 collector 才存在;默认开启 ASSET_COLLECTION_ENABLED=0 # 可选:设为 0 或 false 关闭本进程的回收 ASSET_COLLECTION_INTERVAL_MS=900000 # 可选:一轮回收的间隔,默认 900000(15 分钟),下限 1000ms ASSET_COLLECTION_GRACE_MS=3600000 # 可选:字节失去最后引用后的保留窗口,默认 3600000(1 小时)各参数的用途与边界:
ASSET_COLLECTION_INTERVAL_MS是调度节奏,不是保留时间。README 说明默认 15 分钟“短到被删资产的字节当天就消失,长到不会干扰正常请求流量”。进程内传入的不是安全整数或低于 1000ms 的值会被告警并回退默认值。进程启动后第一轮在一个间隔之后才运行,冷启动不会与 PostgreSQL 就绪竞态。ASSET_COLLECTION_GRACE_MS才是真正的保留窗口,见下一节。ASSET_COLLECTION_ENABLED=0只在当前进程关闭回收。横向扩容的部署可以所有实例都开着(有行锁保护),也可以全部关掉、自己跑一个;文档把“两处都不管”列为会无界增长的设计。
保留:grace period 到底保留了什么
- 字节在失去最后一个引用后至少存活一个 grace period(默认 1 小时),之后才可能被 collector 收走。README 的原话是“grace period 就是用户删除的字节实际得到的保留窗口,所以应该刻意(deliberately)调大它”——如果你的合规要求是“删除后 N 天内不真正消失”,就把这个值设为 N,而不是依赖默认 1 小时。
- 全局去重意味着
remove从来不保证字节被销毁:字节跨用户共享,只要有其他主体的注册表条目仍引用它,它就一直保留。文档明确指出“remove本来就无法承诺字节被销毁”,这是去重设计的固有结果,不是可调项。 - 一个值得知道的反向事实:注册表条目指向的字节如果被回收了,该 id 的读取解析为miss(未命中)而不是报错,客户端按
404 ASSET_NOT_FOUND处理。 - 元数据同样有保留含义:生成时的 provenance 文本(提示词、旁白、音色等)随资产持久化,但当前没有任何代码路径会把它读回来。文档提醒这类调用方提供的文本“应遵循其所需的保留与隐私姿态”,做数据保留策略时要把它算进去。
可选:S3 间接字节出口(ASSET_BYTE_EGRESS=redirect)
如果字节层在 S3,可以把GET字节改为返回短时效签名 URL(302,或打包客户端请求的 JSON 描述符),把下载流量从应用服务器挪走。README 给出启用前必须满足的两个对象存储前提:
- 桶的 CORS 允许本应用的 origin,并在签名响应上暴露
Content-Type; - 签名身份持有桶的
s3:ListBucket权限,这样缺失的 key 返回404 NoSuchKey而不是403——客户端只有靠这个代码才能把“字节已被回收”识别为 miss。
PostgreSQL 字节列不能签名,配置了ASSET_BYTE_EGRESS=redirect也会自动回退到直接字节。asset HTTP 契约 还规定了签名 URL 的有效期必须远低于 grace period(打包处理器要求它小于 grace 的十分之一且不超过 15 分钟上限,否则构建失败),并明确这是一个知情选择:以哈希为对象名的签名 URL 会泄露跨 id 的字节相等性,同时对象存储的ETag/Last-Modified头签名无法剥离。对这类信号敏感的部署应维持直接出口。
验证:确认备份与回收按预期工作
文档提供的判断依据:
- 回收在运行:collector 每轮若回收了 blob,会打印日志
Asset collector reclaimed <n> unreferenced blob(s)(文档示例格式,n为实际数量,仅大于 0 时打印);某轮失败则打印Asset collection pass failed; retrying on the next interval,进程不会退出,下一轮间隔自动重试。观察这两行日志即可判断调度是否活着。 - 被回收的字节读起来是 miss:按契约,注册表条目字节被收走后,该 id 的读/替换返回固定的
404 ASSET_NOT_FOUND,与其他用户的 id、从未分配过的 id 完全不可区分。 - 持久化配置错误可观察:构建启用了
NEXT_PUBLIC_PERSISTENCE但运行时 token/DATABASE_URL不匹配时,首页显示 persistence-unavailable 提示并保留旧课程列表,而不是显示空库——这是判断“浏览器连上了服务端持久化”与否的可见信号。 - S3 出口生效与否:
ASSET_BYTE_EGRESS=redirect+ S3 时字节 GET 得到签名 URL(或描述符);字节层无法签名(如 PG 列)时同一请求得到直接字节,两种形态客户端都能处理,无需额外开关。
限制
- 字节读取是整份物化的:契约不支持
Range,读字节会把整个资产读入内存,大媒体是这条链路的真实上限; remove之后字节仍在磁盘上停留至多一个 grace period 再被收集,存储会在 collector 运行前持续增长——这是设计代价,不是故障;- 开发 token 方案不提供用户隔离,任何能加载页面的人都可枚举学习者分区,备份文件本身包含所有用户数据,按生产敏感数据对待;
- 浏览器本地模式(未配
DATABASE_URL)下所有资产在 IndexedDB,服务端没有任何字节可备份,本节的备份/回收讨论不适用。
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考