
Onyx Craft 沙箱快照保留机制深度解析prune-on-write 策略与幂等清理实现【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer导读Craft 是 Onyxdanswer中的云端开发沙箱能力每个用户的沙箱 pod 空闲时会进入休眠/唤醒sleep/wake循环而支撑这一循环的核心数据基础设施就是会话快照snapshot。本文围绕仓库中的设计文档 docs/craft/infra/snapshot-retention.md完整拆解快照的语义定义、每会话只保留最新一份的 prune-on-write 保留策略、blob-then-row 的幂等删除流程以及这套机制如何做到无需周期任务、无需配置开关、无需数据库迁移即可自愈。读完你将掌握快照在 Onyx 中的内部定位、cleanup_idle_sandboxes_task空闲清理任务与create_session_snapshot_keep_latest的核心调用链、SnapshotManager的存储/删除语义以及如何通过测试用例验证 prune-on-write 的正确性。什么是快照内部 sleep/wake 管道而非用户可见的版本历史在 Onyx 的 Craft 架构中每个用户的沙箱sandbox是一个运行在 Kubernetes/Docker 上的独立 pod承载outputs/、attachments/、.opencode-data/等工作区目录以及聊天历史。由于沙箱是按需启动的资源空闲的 pod 必须被回收否则成本与集群容量都不可持续。快照正是为这一回收行为服务的内部管道。文档 docs/craft/infra/snapshot-retention.md 明确触发时机当 Craft 沙箱进入空闲idle状态时清理任务把每个会话session的工作区打包为tar.gz归档存储位置归档通过 Onyx 的 FileStore 抽象持久化抽象接口见 backend/onyx/file_store/file_store.py文件来源标记为SANDBOX_SNAPSHOT见 backend/onyx/configs/constants.py沙箱处置归档完成后终止 pod唤醒恢复用户再次使用时从最近一份快照恢复工作区。从源码看快照的字节流处理非常讲究。SnapshotManagerbackend/onyx/server/features/build/sandbox/snapshot_manager.py以 8 MiB_SNAPSHOT_COPY_CHUNK_BYTES 8 * 1024 * 1024为分块单位流式拷贝归档字节流先落地到临时文件再交给 FileStore 持久化并返回(snapshot_id, storage_path, size_bytes)三元组。存储路径的命名空间是sandbox-snapshots/{tenant_id}/{sandbox_id}/{snapshot_id}.tar.gz快照的文件类型为application/gzip同时在snapshot数据库表中记录元数据session_id、storage_path、size_bytes、created_at表定义见 backend/onyx/db/models.py。关键认知必须澄清快照不是面向用户的版本历史。用户看不到上一份快照、回滚到某版本这类功能它纯粹是 sleep/wake 的持久化管道用户可感知的只是沙箱休眠后再次打开工作区还在。因此保留策略的目标不是给用户留多个可回溯的版本而是在保证可恢复性的前提下把存储占用压到最低。保留策略每会话恰好一份prune-on-write为什么只保留最新一份每个会话的每一份新快照都会完全取代supersede之前的快照——休眠时抓一次、唤醒后恢复、再次休眠时从当前工作区再抓一次旧快照代表的是已经被后续状态覆盖的历史中间态。保留旧快照既没有恢复价值还会持续消耗 FileStore 存储。因此文档给出的策略直截了当每个会话只保留一份快照——最近的一份。prune-on-write 而非周期清扫实现上有一个明确的设计取舍不用周期任务去累积后再批量清扫而是在写入时立即修剪prune on write。这在空闲清理任务 backend/onyx/background/celery/tasks/build/tasks.py 中体现为两个阶段对于空闲沙箱idle调用sleep_sandbox走完整快照→终止→标记 SLEEPING的休眠流程对于非空闲沙箱执行后台增量快照background snapshot把数据丢失窗口压缩到SANDBOX_IDLE_TIMEOUT_SECONDS / SNAPSHOT_INTERVAL_DIVISOR默认 1 小时 / 4 15 分钟见 tasks.py 中的SNAPSHOT_INTERVAL_DIVISOR 4与 backend/onyx/server/features/build/configs.py 中的SANDBOX_IDLE_TIMEOUT_SECONDS默认 3600 秒。两个阶段最终都汇聚到同一个函数create_session_snapshot_keep_latestbackend/onyx/server/features/build/session/sandbox_lifecycle.py。它的执行顺序是先查旧get_snapshots_for_session取出该会话当前已存在的全部快照按创建时间倒序见 backend/onyx/server/features/build/db/sandbox.py再写新sandbox_manager.create_snapshot生成新归档记新行create_snapshot__no_commit在snapshot表写入新记录只 flush 不 commit修剪旧遍历旧快照逐个执行删 blob → 删行最后提交db_session.commit()一次性提交整个快照创建 修剪事务。由此可以推导出两条关键保证新快照先完全持久化durable旧快照才被删除。即便在删除旧快照的瞬间进程崩溃新快照也已落库落盘会话永远有一份可恢复的快照存储不会累积。每次修剪都是针对所有旧快照for old in prior_snapshots而不是只删最近一份。自愈能力self-healing文档特别指出这套机制是自愈的每次会话被 reap即进入 sleep 流程时修剪逻辑会清除该会话全部历史快照。因此即便过去某次部分失败partial failure留下了多余的行例如某个旧 blob 删了但行没删、或反之在该会话的下一次 reap 时也会被一并清理干净无需任何人工干预或修复脚本。删除语义blob-then-row、幂等、尽力而为先删 blob再删行对于每一份被取代的快照删除顺序是严格固定的先删 FileStore 中的 blob再删snapshot数据库行。对应实现为blob 删除SnapshotManager.delete_snapshot(storage_path)backend/onyx/server/features/build/sandbox/snapshot_manager.py底层调用 FileStore 的delete_file(file_id, error_on_missingFalse)行删除delete_snapshot__no_commit(db_session, old)backend/onyx/server/features/build/db/sandbox.py。这个顺序不是随意的。如果先删行再删 blob一旦 blob 删除失败就会留下孤儿 blob没有元数据、无人引用的存储垃圾而先删 blob 再删行即使行删除失败最坏情况也只是残留一行指向已删除 blob 的元数据——它会被下一次 reap 幂等清除见下。幂等性blob 缺失视为已删除delete_file(error_on_missingFalse)是幂等性的关键FileStore 抽象在 backend/onyx/file_store/file_store.py 中定义了该语义——当文件记录不存在时静默返回而非抛错。因此一个此前部分运行中已经删掉 blob、但还没来得及删行的快照在下次修剪时blob 删除成功实际是无操作随后行被正常删除blob 与行最终保持一致。这正是文档中a row whose blob was already removed in a prior partial run still gets its row dropped的源码级依据。尽力而为真实失败留行待重试删除失败的场景也有明确定义。在create_session_snapshot_keep_latest中每次 blob 删除都被try/except包裹for old in prior_snapshots: try: snapshot_manager.delete_snapshot(old.storage_path) except Exception as e: logger.warning(Skipping prune of snapshot %s; blob delete failed: %s, old.id, e) continue delete_snapshot__no_commit(db_session, old)当 blob 删除因真实故障如 S3 不可达抛错时该行被保留continue跳过行删除等待该会话下一次 reap 时重试不阻断后续快照的修剪——其余旧快照照常删除。由此保证 blob 与行永远不会失步要么两者都删除要么两者都保留等待重试不会出现行没了但 blob 还在或blob 没了但行还在以外的第三种泄漏形态——而后者也会被幂等语义自愈。这一行为由单元测试直接锁定backend/tests/unit/onyx/server/features/craft/sandbox/test_prune_prior_snapshots.py 中的test_blob_delete_failure_keeps_that_row_but_continues验证了第二个 blob 删除失败时其行保留其余行照删test_prunes_blob_then_row_for_each_prior验证了删除顺序与逐项调用test_empty_priors_is_a_noop验证了无旧快照时不产生任何删除调用test_create_snapshot_does_not_prune_snapshots_created_during_archive则验证了归档创建期间并发产生的新快照不会被误删修剪只针对写入前已存在的快照列表。与 FileStore 及数据库模型的对应关系FileStore 抽象与删除语义FileStore是所有快照 blob 存储的抽象层backend/onyx/file_store/file_store.pydelete_file(file_id, error_on_missing: bool True)是核心删除接口error_on_missingTrue时文件不存在会抛错error_on_missingFalse时静默返回。快照修剪统一使用后者从而获得幂等性。具体后端实现如 S3、数据库等通过get_default_file_store()注入SnapshotManager只依赖抽象不感知底层存储。snapshot 表模型数据库侧Snapshot模型backend/onyx/db/models.py结构如下字段类型说明idUUID主键快照唯一标识session_idUUID外键 →build_session.idON DELETE CASCADE所属会话storage_pathStringFileStore 中的 blob 路径size_bytesBigInteger归档字节数created_atDateTime(timezoneTrue)服务端默认now()创建时间表上建有ix_snapshot_session_created复合索引session_id 倒序created_at支撑get_latest_snapshot_for_session与get_snapshots_for_session的按会话查询。后台快照与数据丢失窗口值得一提的细节是空闲清理任务除了对空闲沙箱做完整 sleep 快照外还会对非空闲沙箱做后台增量快照backend/onyx/background/celery/tasks/build/tasks.py。其依据是user_has_stale_active_sessionbackend/onyx/server/features/build/db/sandbox.py这个 DB-only 预过滤当用户的所有 ACTIVE 会话都已拥有比snapshot_cutoffSANDBOX_IDLE_TIMEOUT_SECONDS / 4默认 15 分钟更新的快照时直接跳过避免无谓的 pod 往返。这样做的意义是即便 pod 因 kubelet 驱逐、节点丢失、spot 回收等非优雅原因死亡数据丢失也被约束在约 15 分钟窗口内。后台快照同样走create_session_snapshot_keep_latest因此同样执行 prune-on-write不会额外累积旧快照。上线与运维零配置、零迁移、重启即生效文档在 Rollout 一节给出的结论非常明确——没有周期任务、没有 beat 条目、没有配置开关、没有数据库迁移无周期任务/beat 条目修剪逻辑内嵌在已有的cleanup_idle_sandboxes_taskOnyxCeleryTask.CLEANUP_IDLE_SANDBOXES中不新增任何定时调度无配置开关保留策略是硬编码语义每会话一份最新不提供可调参数无数据库迁移snapshot表结构id、session_id、storage_path、size_bytes、created_at完全复用现有模型无需 schema 变更生效方式重启 Celery workers以加载新的 reap 行为即可。从运维角度看这意味着升级路径极简灰度重启 worker 后下一次空闲清扫就会自动按新策略修剪存量累积的旧快照也会随着各会话的下一次 reap 被自愈式清空。失败场景推演与保证汇总场景行为保证新快照写入成功随后崩溃旧快照尚未删除新快照已持久化会话可恢复旧 blob 已删行未删部分失败下次修剪时 blob 删除幂等成功行被删blob 与行最终一致blob 删除真实失败如 S3 不可达该行保留其余照删下次 reap 重试blob 与行不失步会话从未被 reap 且留有历史积压下一次 reap 修剪全部旧快照存储自愈pod 非优雅死亡最近后台快照≤15 分钟兜底数据丢失窗口受约束整体上这套设计以写入时修剪替代周期清扫以先 blob 后行 幂等删除消除孤儿对象以尽力而为 下次重试容忍瞬时故障最终在可恢复性、存储占用与实现复杂度之间取得了平衡——这也正是它能做到零新增调度、零配置、零迁移的根本原因。相关代码与测试索引设计文档docs/craft/infra/snapshot-retention.md空闲清理任务backend/onyx/background/celery/tasks/build/tasks.py快照生命周期prune-on-write 核心backend/onyx/server/features/build/session/sandbox_lifecycle.py快照存储管理器backend/onyx/server/features/build/sandbox/snapshot_manager.py快照数据库操作backend/onyx/server/features/build/db/sandbox.pyFileStore 抽象backend/onyx/file_store/file_store.pySnapshot模型backend/onyx/db/models.py空闲超时配置backend/onyx/server/features/build/configs.py单元测试backend/tests/unit/onyx/server/features/craft/sandbox/test_prune_prior_snapshots.py【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考