Sandbox 环境持久化:持久卷、磁盘保留与快照恢复
用户让 Agent 安装了一个工具,安装成功,任务也执行完了。过一段时间回来继续用,项目文件都在,再执行那个命令却变成了 command not found。
我们遇到的就是这类问题。用户可以在 Sandbox 里安装软件,而实例回收、重新创建以后,安装过的东西没有跟着回来。
上一篇文章讨论了如何把源码留在 NAS,把依赖安装和构建放到本地盘。这次要继续看工作区之外的内容:软件到底装在哪里,怎样让这些安装结果跨实例保存,以及三种持久化方案各自需要解决什么问题。
1. 文件还在,软件为什么没了
1.1. 先分清“重启”做了什么
先用一个简化场景说明。假设基础镜像没有 Graphviz,用户在 Debian 系的 Sandbox 中执行 apt-get install graphviz,随后用它的 dot 命令把项目里的关系图导出为图片。
这时有两份新内容:工作区里生成的图片,以及系统里新增的 Graphviz。两者虽然都能从同一个终端访问,实际可能写在完全不同的存储上。
容器启动时,根据基础镜像准备文件系统。镜像提供初始的系统文件,运行中新增和修改的内容进入这个实例的可写层。与此同时,平台把 NAS 上的项目目录挂进容器,作为工作区。这里把工作区简写为 /workspace。
如果只重启 Agent 进程,文件系统没有换掉,已经安装的文件通常还在。如果停止容器后继续启动原容器,能否保留文件,要看平台是否保留它的可写层。
我们遇到的另一条路径是:旧实例被销毁,再从基础镜像创建新实例。 新实例重新挂载原来的 NAS,所以代码和图片还在;它的系统文件重新来自基础镜像,后来安装的 Graphviz 就没有了。Docker 文档也明确区分了容器可写层和独立 Volume:删除容器会删除可写层,Volume 则有独立的生命周期。Docker Volume
因此,排查时需要看实际发生的是进程重启、原实例恢复,还是销毁后重建。业务层沿用同一个 Sandbox ID,也不能证明底下仍然是同一份文件系统。
1.2. 一次安装会修改哪些地方
apt、apk 是系统包管理器,分别常见于 Debian 系和 Alpine 系发行版。它们安装软件时,不会把所有内容都放进当前工程目录。
下面列的是系统安装可能涉及的位置,不是 Graphviz 某个版本的完整文件清单。
| 内容 | 常见位置 | 恢复时为什么需要它 |
|---|---|---|
| 可执行文件、共享库 | /usr/bin、/usr/lib 等 | 命令本身和运行时依赖 |
| 系统配置 | /etc | 软件运行所需的配置 |
| 包管理器记录 | Debian 的 /var/lib/dpkg 等 | 记录已装的包、版本与安装状态 |
| 安装脚本产生的修改 | 用户、组、链接,以及其他目录 | 让软件处于可用状态 |
例如,只复制一个可执行文件,可能因为缺共享库而无法运行;只保存包管理器的数据库,则可能出现“记录显示已经安装,文件却不存在”的情况。Debian 的安装流程还会执行包中的维护脚本,处理配置等工作,安装结果不只是一组解压出来的文件。Debian 包安装过程
这里需要保存的是一套相互匹配的文件与元数据。后面提到的根文件系统(RootFS),指的就是 Sandbox 从 / 开始看到的系统文件体系。独立挂载进去的 NAS、临时内存文件系统等,需要按各自的存储规则处理,不能默认算进一份 RootFS 备份里。
我们当时讨论的三条路,也就对应三个不同的保存范围:保留指定目录、保留原来的运行磁盘、把某一时刻的系统环境保存成快照。
2. 方案一:把需要的目录挂到持久卷
2.1. 新实例重新挂载原目录
这是最容易从现有工作区方案继续扩展的一条路。
平台为项目准备一份独立存储,在每个新实例中挂到固定路径。写入这个目录的内容留在存储里,实例删除后,下一个实例再挂载同一份数据。NAS 可以承载这种目录;自建容器平台也可以通过 Volume 或 Kubernetes PVC 声明需要的存储。
但 Volume、PVC 说明的是挂载与管理方式,具体数据存在哪里,仍要看后端。Docker 的本地 Volume 默认在宿主机上;需要换节点恢复时,还必须有相应的共享存储、磁盘迁移或数据恢复能力。Docker Volume 的存储与共享
平台至少需要记录项目对应的卷标识或远端目录、挂载路径、访问身份。创建新实例时,先完成挂载,再启动依赖这些目录的工具。挂错项目目录、改变安装路径,或者恢复后使用不同的用户权限,都可能让文件还在,却无法正常使用。
2.2. 适合能控制安装位置的工具
例如,一个工具允许把自身及依赖安装到 /home/user/tools,我们可以把这个目录持久保存,启动时再配置好 PATH。对于能明确管理目录的用户级工具,这样做简单,也不需要备份整个系统。
语言依赖需要再看一层。保存 Python 虚拟环境的目录,并不保证换一个基础镜像后仍能运行:它可能依赖原来的解释器路径、系统库或本地编译扩展。恢复时应保持兼容的基础环境,或者根据依赖声明重新安装。
上一篇文章中的 node_modules 就采用了后者:保存源码、包声明和锁文件,在新的本地盘上重新准备依赖。这里主动保留的是重建输入,本地依赖目录可以丢弃。这有助于兼顾持久性和小文件访问性能。
2.3. 系统包为什么不能照搬
对于任意 apt install 或 apk add,平台无法事先假定所有修改都落在一个用户目录。多挂一个 /home,仍然覆盖不到 /usr、/etc 和系统包数据库。
把这些系统目录逐个挂出去,还会引出新的问题。挂载到已有目录上的外部文件系统,会遮住那个位置原有的内容;它不会自动把旧系统与新镜像正确合并。基础镜像升级后,外部目录里的旧库、旧配置和新系统之间,也需要处理兼容关系。Docker 挂载覆盖行为
我们的 FC 环境还有平台限制:NAS 挂载文档要求使用适合的数据目录,不能把通用系统目录及其子目录作为挂载点,例如 /bin、/opt、/var、/dev。FC 配置 NAS
所以,这个方案适合保存边界明确的目录。要支持用户自由安装系统软件,就需要把保存范围扩大到整套系统环境。
3. 方案二:停止计算,保留运行磁盘
3.1. 把磁盘的生命周期单独管理
第二种方案直接保留已经安装好的那份文件系统。
对于虚拟机,可以是保留系统盘;对于容器,可以由运行平台保留根文件系统的可写部分,并继续使用匹配的基础镜像。用户安装软件后,文件和包管理器记录都在这份环境里。停止计算时保留它,恢复时继续使用它。
这样,前面的 Graphviz 无须重新安装。恢复时取得原来的磁盘,软件文件、配置和包记录就一起回来了。进程则通常要重新启动,磁盘持久化本身不包含进程内存。
Daytona 的普通 Container Sandbox 提供了这类使用语义:stop/start 保留文件与安装包,停止时清除内存;删除 Sandbox 则是另一种操作。具体选型仍需核对 Sandbox 类型和自动删除配置,不能只看一个“支持持久化”的介绍。Daytona 持久化说明
3.2. 平台需要记录什么
业务层不能只保存“项目正在使用哪个实例”。还需要保存项目的环境磁盘标识、匹配的基础镜像版本,以及当前由哪个实例使用。
正常停止时,平台应让安装和写入结束,再按存储系统的要求完成落盘,最后停止计算、保留磁盘。恢复时找到这份磁盘,确认旧实例已经停止写入,再交给新的运行实例。删除项目时,才根据保留策略删除它。
这里有一个容易忽略的情况:旧实例网络断开,平台认为它失联,于是又启动了一个新实例。旧实例其实还活着,两个实例就可能同时修改包数据库。管理服务中的一把锁还不够,平台需要通过磁盘挂载约束或存储侧的写入控制,阻止已经失去所有权的旧实例继续写。
3.3. 磁盘保留在哪,决定了能恢复到哪里
如果只是让容器停在原机器上,并保留那台机器的本地可写层,原地启动确实方便。但宿主机永久故障以后,能否恢复就取决于有没有其他持久副本。
要允许换节点恢复,需要磁盘本身能脱离原计算节点,例如使用可重新挂载的持久块存储,并满足可用区、文件系统和运行时的约束。异常断电后还可能面对文件系统恢复、包安装中断等状态,保存磁盘不会自动把安装过程变成一个事务。
这条路的主要成本是长期保留每个项目的环境空间,以及磁盘挂载、迁移和故障处理。计算停止后,存储仍然占用资源。它适合经常回来继续工作的开发环境;若有大量长期不活跃的项目,还需要归档和回收机制。
4. 方案三:把环境保存成文件系统快照
4.1. 新实例从保存的环境启动
第三种方案允许原实例彻底退出。退出之前,先把某个时刻的根文件系统保存为一个可恢复版本;下次根据这个版本创建新实例。
快照可以是完整磁盘副本,也可以只记录相对基础镜像的变化。后者能够复用公共镜像,项目只保存自己新增、修改和删除的内容。这里讨论的是文件系统快照;要继续执行原来的进程,还涉及另外的内存与设备状态恢复。
例如,Modal 的文件系统快照记录 RootFS 相对基础镜像的差异,生成的 Image 可以创建新 Sandbox。它的文档也明确说明,挂载的 Volume 不包含在这份快照里;快照还需要配置保留期限。Modal 文件系统快照
恢复 Graphviz 时,平台不再重新执行安装命令,而是从快照取得已经安装好的文件与状态。快照生成以后,即使原实例被删除,只要快照及其依赖仍在,便可以据此准备另一个实例。
保存差异时,删除操作也必须被记录。假设用户删除了基础镜像中的一个配置文件,只备份“当前新增的文件”,恢复后那个旧配置又会出现。以 OverlayFS 为例,它会用 whiteout 等元数据表达对下层文件的删除;这类信息不能在复制过程中丢失。Linux OverlayFS
4.2. 在什么时刻保存
最直接的做法是正常关闭前保存一次。但节点故障、强制回收等情况,不保证有机会执行关闭脚本。要减少丢失,就要在环境修改后建立保存点。
用户安装 Graphviz 后,当前实例里已经有新软件,但持久存储中还是上一次保存的环境。只有新快照生成、上传并确认可恢复之后,才能把它设为项目下次启动时使用的环境。
一种可采用的保存流程是:
- 暂停同一环境上的其他修改,确认安装进程及其后续配置已经完成。
- 按快照实现的要求,完成写入刷新,并取得一致的文件系统视图。
- 生成新快照,保存其数据与元信息,检查它已经能够被恢复。
- 把新版本标记为可用,再原子地切换项目的当前版本。
单独调用一次 sync 不会阻止其他进程继续写,也不保证跨多个存储的一致性。如果软件还在修改文件,需要先让相关写入停下来,或使用平台提供的协调机制。快照内部完成的原子性范围,也应由具体实现说明。
如果第三步失败,下次启动仍使用上一次保存成功的环境。这样不会把半份快照交给下一次启动,但尚未保存的新安装也不能保证回来。因此,产品需要区分“本次安装可用了”和“环境已保存,可以重建恢复”。如果承诺每次安装成功后都能恢复,就必须把保存完成纳入这次操作的成功条件;允许任意终端命令时,则还需要明确的环境保存点或受控修改入口。
4.3. 快照记录不能只有一个文件地址
管理服务需要在实例之外保存项目与环境版本的关系。可以在数据库中记录当前使用哪一版环境,每个版本关联以下信息。
| 保存内容 | 用途 |
|---|---|
| 环境标识与版本 | 确定这是哪个项目的哪一版环境 |
| 基础镜像的内容摘要 | 绑定生成快照时使用的确切镜像 |
| 快照位置 | 找到已保存的文件系统数据及依赖 |
| 保存状态 | 区分正在保存、可恢复和保存失败 |
| 恢复条件 | 记录架构、运行时等兼容要求 |
| 工作区关联 | 找到独立保存的项目工作区 |
并发保存时,还需要检查当前版本和写入所有权。保存任务开始时记下项目使用的环境版本,更新时确认它没有被其他任务替换,且自己仍持有有效写入权,再切换到新快照。检查与切换需要原子完成,避免超时后继续执行的旧任务覆盖已经发布的新环境。
NAS 工作区不随 RootFS 快照一起回退。如果还希望把代码与环境一起回到过去,就要额外保存代码提交或工作区版本,并协调两者的对应关系。只记录工作区标识,找到的仍然是同一个目录,并不代表这个目录在历史时刻的内容。
5. 三种方案放在一起看
三种方案都能保存文件,区别在于保存范围和恢复方式。
| 比较项 | 指定目录持久化 | 保留运行磁盘 | 文件系统快照 |
|---|---|---|---|
| 保存范围 | 选定的挂载目录 | 原来的系统磁盘或完整可写部分 | 某个保存点的根文件系统 |
| 任意系统包安装 | 仅目录方案无法完整覆盖 | 在保留范围内可以覆盖 | 在快照范围内可以覆盖 |
| 恢复方式 | 新实例挂载原目录 | 继续使用原环境磁盘 | 从已保存版本创建实例 |
| 原节点永久故障 | 取决于卷的存储后端 | 取决于磁盘是否独立持久 | 取决于快照与依赖是否独立持久 |
| 回到历史环境 | 需要另做目录版本 | 需要另做磁盘快照 | 可选择仍保留的快照版本 |
| 主要额外工作 | 目录边界、路径与权限 | 磁盘所有权、迁移与回收 | 保存时机、版本提交与垃圾回收 |
手机上可左右滑动查看完整对比。
表里的“可以覆盖”有前提:保存的文件范围完整,元数据正确,并使用兼容的恢复环境。外部服务状态、进程内存、单独挂载的数据卷,不会因为保留 RootFS 就全部跟着保存。
性能也需要按用户真正等待的路径来测。目录方案要看挂载后访问文件的成本;保留磁盘要看重新挂载与启动的时间;快照方案则要看生成、传输、恢复,以及恢复后第一次真正使用软件的时间。延迟加载可以把一部分恢复工作推迟到首次读文件,单看“实例启动完成”容易漏掉这部分等待。
这次回顾没有三种方案在同一负载下的耗时和费用实测,因此不能给出谁快多少的排名。更适合先按需求缩小范围:固定工具链与可控目录优先考虑持久卷;需要频繁继续使用同一套环境,可以评估保留磁盘;需要释放原实例、回滚或从同一环境分出多个副本,快照提供的版本能力更直接。
三者也可以组合。例如,活跃项目保留磁盘,长期不活跃时归档为快照;源码继续放 NAS,构建缓存留在可丢弃的本地盘。
6. 放回我们的 FC 架构,怎样接入
6.1. 现有实现保存到了哪一层
现有实现把 NAS 挂到项目工作区;恢复时,根据保存的记录选择镜像和运行配置,再创建实例。部分配置变化还会触发旧实例删除与重新创建。
这条创建、恢复链路没有携带每项目的 RootFS 快照或持久系统盘标识。基础镜像预装的工具会随镜像重新出现,用户在旧实例中临时安装的软件则没有相应的恢复步骤。FC 文档也说明,自定义容器的可写层随容器销毁而删除。FC 自定义容器
目前能从代码中确认的,是公共工具预装、NAS 工作区,以及本地项目依赖的重新准备。项目级环境磁盘、RootFS 快照和下面的派生镜像流程,仍属于这里讨论的设计,不能据此声称已经上线。
6.2. 继续使用 FC,可以先约束可保存的安装
如果暂时继续使用当前的 FC 运行方式,又无法取得和恢复容器的完整可写层,可以把可持久化的安装收敛为声明式配置:记录项目需要哪些系统包、版本和来源,再构建一份项目专用镜像。
例如,用户通过受控入口增加 Graphviz,平台记录基础镜像 digest、发行版与软件源、包名和版本。构建任务在隔离环境中生成派生镜像,验证后推送到镜像仓库,再把项目的环境镜像指针切换过去。下次重建时,创建逻辑使用这个 digest。
软件源也要纳入设计。只记一个包版本,如果上游已经不再提供对应包,仍可能构建失败。长期重建需要可保留的包来源、仓库快照或构建产物。已经构建成功的镜像可以复用,不必在每次启动时重新下载和安装。
这种方式保存的是明确声明并成功构建的环境。用户随手修改 /etc,或者运行一个会改动多个位置的安装脚本,不能仅靠记录命令就完整恢复。允许的包管理入口、额外配置文件和安装脚本范围,需要作为产品能力明确下来。
它是现有平台约束下的一条接入路径,与前面的 RootFS 快照有不同的保存语义。构建失败时也不能先更新项目镜像;应该保留上一版可用环境,并说明这次修改尚未持久保存。
6.3. 完整环境持久化需要运行平台配合
如果产品要让用户自由安装系统软件,并在实例重建后继续使用,就应选择能保存完整环境的运行平台,或者自行提供磁盘与快照能力。这已经涉及 Sandbox 创建和回收的接口,工作区里的脚本无法单独完成。
创建接口需要接受环境版本,保存接口需要返回可恢复的磁盘或快照标识,删除接口则要区分“释放这次计算”和“删除项目环境”。恢复完成后,确认软件真正可用,再让 Agent 继续执行任务。
对应的回收也要分开:本地缓存可以随实例删除;项目当前环境不能被普通实例清理顺手删掉;旧快照只有在不再被项目、恢复任务和派生版本引用时,才能回收。还要设置容量配额,并在写入时返回明确错误,避免用户安装了很大的软件以后,环境迟迟无法保存。
环境快照也不宜包含运行时注入的临时凭证。凭证应放在独立的临时挂载或注入机制中,恢复时重新取得,而不是把一个可能过期的访问令牌随环境版本长期保存。
7. 怎样确认软件确实保住了
验收不能只看“Sandbox 又启动了”。对于开头的例子,应在新实例中执行 dot,实际生成一张图,同时检查包管理器记录、安装配置和工作区文件。
下面是选型时应执行的验证场景,并非本次已经完成的测试结果。
| 场景 | 要确认的结果 |
|---|---|
| 正常停止,再恢复 | 软件、配置、包记录相互匹配,程序能实际运行 |
| 删除运行实例,再创建 | 按方案约定使用保留磁盘或快照,不悄悄退回公共镜像 |
| 原计算节点不可用 | 独立持久数据仍可取得;无法恢复时明确报错 |
| 安装中断或保存失败 | 不发布半成品环境,恢复点与用户看到的状态一致 |
| 两次修改或恢复并发 | 旧实例、旧保存任务不能覆盖新的环境 |
| 删除软件后再恢复 | 被删除的文件不会从基础镜像中重新出现 |
| 工作区独立变化 | 明确代码与环境分别恢复到哪一版 |
性能测试则应固定基础镜像、软件集合和修改量,分别记录保存耗时、保存大小、冷恢复时间,以及第一个真实命令的完成时间。只有这些数据齐全,才能把方案能力、用户等待和存储费用放在同一张表里比较。
8. 环境留下来了,基础镜像怎样升级
持久化解决了安装结果丢失,也带来了下一个问题:公共基础镜像升级以后,用户保存的旧环境要怎样跟上。
一个项目可能在原基础镜像上安装了很多包。直接换成新版基础镜像,再覆盖旧环境的差异,可能混入两版系统库,甚至把已经修复的旧文件重新盖回去。因此,环境版本需要绑定基础镜像,升级应当是一次可验证的迁移。
声明式环境可以尝试在新镜像上重新构建;保存了任意文件修改的环境,则需要更谨慎地迁移和验证。可以先创建一份新环境,运行项目的真实任务,成功后再切换,并保留旧版本作为回退点。
到了这一步,平台管理的已经不只是一个会启动和销毁的 Sandbox,还包括用户逐渐积累起来的开发环境。它需要明确的保存点、版本和升级过程。这也是持久化方案选定以后,下一步要继续解决的事情。
文中实现状态截至 2026 年 10 月 13 日;项目级环境持久化流程为方案设计。