Contents

Sandbox 环境持久化:持久卷、磁盘保留与快照恢复

用户让 Agent 安装了一个工具,安装成功,任务也执行完了。过一段时间回来继续用,项目文件都在,再执行那个命令却变成了 command not found。

我们遇到的就是这类问题。用户可以在 Sandbox 里安装软件,而实例回收、重新创建以后,安装过的东西没有跟着回来。

上一篇文章讨论了如何把源码留在 NAS,把依赖安装和构建放到本地盘。这次要继续看工作区之外的内容:软件到底装在哪里,怎样让这些安装结果跨实例保存,以及三种持久化方案各自需要解决什么问题。

1. 文件还在,软件为什么没了

1.1. 先分清“重启”做了什么

先用一个简化场景说明。假设基础镜像没有 Graphviz,用户在 Debian 系的 Sandbox 中执行 apt-get install graphviz,随后用它的 dot 命令把项目里的关系图导出为图片。

这时有两份新内容:工作区里生成的图片,以及系统里新增的 Graphviz。两者虽然都能从同一个终端访问,实际可能写在完全不同的存储上。

容器启动时,根据基础镜像准备文件系统。镜像提供初始的系统文件,运行中新增和修改的内容进入这个实例的可写层。与此同时,平台把 NAS 上的项目目录挂进容器,作为工作区。这里把工作区简写为 /workspace。

旧 Sandbox 的软件安装写入临时可写层,项目文件写入 NAS;新 Sandbox 从原基础镜像创建并重新挂载 NAS,因此只找回了项目文件
图 1:从原镜像重建实例时,工作区与系统环境的恢复路径不同

如果只重启 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 的存储与共享

新旧实例都把 workspace 和用户工具目录挂到独立持久存储;系统包写入的 usr、etc 和包数据库仍在挂载范围之外
图 2:目录持久化覆盖哪些内容,由挂载位置和安装位置共同决定

平台至少需要记录项目对应的卷标识或远端目录、挂载路径、访问身份。创建新实例时,先完成挂载,再启动依赖这些目录的工具。挂错项目目录、改变安装路径,或者恢复后使用不同的用户权限,都可能让文件还在,却无法正常使用。

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. 把磁盘的生命周期单独管理

第二种方案直接保留已经安装好的那份文件系统。

对于虚拟机,可以是保留系统盘;对于容器,可以由运行平台保留根文件系统的可写部分,并继续使用匹配的基础镜像。用户安装软件后,文件和包管理器记录都在这份环境里。停止计算时保留它,恢复时继续使用它。

计算实例停止后保留项目的根磁盘或可写层,恢复时继续使用;磁盘删除由单独的生命周期操作控制
图 3:计算可以停止,环境磁盘仍由项目持有

这样,前面的 Graphviz 无须重新安装。恢复时取得原来的磁盘,软件文件、配置和包记录就一起回来了。进程则通常要重新启动,磁盘持久化本身不包含进程内存。

Daytona 的普通 Container Sandbox 提供了这类使用语义:stop/start 保留文件与安装包,停止时清除内存;删除 Sandbox 则是另一种操作。具体选型仍需核对 Sandbox 类型和自动删除配置,不能只看一个“支持持久化”的介绍。Daytona 持久化说明

3.2. 平台需要记录什么

业务层不能只保存“项目正在使用哪个实例”。还需要保存项目的环境磁盘标识、匹配的基础镜像版本,以及当前由哪个实例使用。

正常停止时,平台应让安装和写入结束,再按存储系统的要求完成落盘,最后停止计算、保留磁盘。恢复时找到这份磁盘,确认旧实例已经停止写入,再交给新的运行实例。删除项目时,才根据保留策略删除它。

这里有一个容易忽略的情况:旧实例网络断开,平台认为它失联,于是又启动了一个新实例。旧实例其实还活着,两个实例就可能同时修改包数据库。管理服务中的一把锁还不够,平台需要通过磁盘挂载约束或存储侧的写入控制,阻止已经失去所有权的旧实例继续写。

3.3. 磁盘保留在哪,决定了能恢复到哪里

如果只是让容器停在原机器上,并保留那台机器的本地可写层,原地启动确实方便。但宿主机永久故障以后,能否恢复就取决于有没有其他持久副本。

要允许换节点恢复,需要磁盘本身能脱离原计算节点,例如使用可重新挂载的持久块存储,并满足可用区、文件系统和运行时的约束。异常断电后还可能面对文件系统恢复、包安装中断等状态,保存磁盘不会自动把安装过程变成一个事务。

这条路的主要成本是长期保留每个项目的环境空间,以及磁盘挂载、迁移和故障处理。计算停止后,存储仍然占用资源。它适合经常回来继续工作的开发环境;若有大量长期不活跃的项目,还需要归档和回收机制。

4. 方案三:把环境保存成文件系统快照

4.1. 新实例从保存的环境启动

第三种方案允许原实例彻底退出。退出之前,先把某个时刻的根文件系统保存为一个可恢复版本;下次根据这个版本创建新实例。

快照可以是完整磁盘副本,也可以只记录相对基础镜像的变化。后者能够复用公共镜像,项目只保存自己新增、修改和删除的内容。这里讨论的是文件系统快照;要继续执行原来的进程,还涉及另外的内存与设备状态恢复。

旧实例安装软件后生成环境快照,基础镜像和快照共同准备新实例的系统文件;NAS 工作区通过独立挂载恢复
图 4:快照保存一个确定的环境版本,新实例从这个版本恢复

例如,Modal 的文件系统快照记录 RootFS 相对基础镜像的差异,生成的 Image 可以创建新 Sandbox。它的文档也明确说明,挂载的 Volume 不包含在这份快照里;快照还需要配置保留期限。Modal 文件系统快照

恢复 Graphviz 时,平台不再重新执行安装命令,而是从快照取得已经安装好的文件与状态。快照生成以后,即使原实例被删除,只要快照及其依赖仍在,便可以据此准备另一个实例。

保存差异时,删除操作也必须被记录。假设用户删除了基础镜像中的一个配置文件,只备份“当前新增的文件”,恢复后那个旧配置又会出现。以 OverlayFS 为例,它会用 whiteout 等元数据表达对下层文件的删除;这类信息不能在复制过程中丢失。Linux OverlayFS

4.2. 在什么时刻保存

最直接的做法是正常关闭前保存一次。但节点故障、强制回收等情况,不保证有机会执行关闭脚本。要减少丢失,就要在环境修改后建立保存点。

用户安装 Graphviz 后,当前实例里已经有新软件,但持久存储中还是上一次保存的环境。只有新快照生成、上传并确认可恢复之后,才能把它设为项目下次启动时使用的环境。

环境修改完成后创建快照,保存并检查成功才更新项目的当前环境;中途失败继续使用上次保存的环境
图 5:安装完成与环境保存完成,是两个不同的时刻;流程为方案设计

一种可采用的保存流程是:

  1. 暂停同一环境上的其他修改,确认安装进程及其后续配置已经完成。
  2. 按快照实现的要求,完成写入刷新,并取得一致的文件系统视图。
  3. 生成新快照,保存其数据与元信息,检查它已经能够被恢复。
  4. 把新版本标记为可用,再原子地切换项目的当前版本。

单独调用一次 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 创建和回收的接口,工作区里的脚本无法单独完成。

目标布局由公共基础镜像、每项目环境版本、NAS 工作区和本地缓存组成;管理服务分别保存这些资源的标识并在创建实例时恢复
图 6:环境与工作区分别持久保存,本地缓存按需重建;这是目标布局

创建接口需要接受环境版本,保存接口需要返回可恢复的磁盘或快照标识,删除接口则要区分“释放这次计算”和“删除项目环境”。恢复完成后,确认软件真正可用,再让 Agent 继续执行任务。

对应的回收也要分开:本地缓存可以随实例删除;项目当前环境不能被普通实例清理顺手删掉;旧快照只有在不再被项目、恢复任务和派生版本引用时,才能回收。还要设置容量配额,并在写入时返回明确错误,避免用户安装了很大的软件以后,环境迟迟无法保存。

环境快照也不宜包含运行时注入的临时凭证。凭证应放在独立的临时挂载或注入机制中,恢复时重新取得,而不是把一个可能过期的访问令牌随环境版本长期保存。

7. 怎样确认软件确实保住了

验收不能只看“Sandbox 又启动了”。对于开头的例子,应在新实例中执行 dot,实际生成一张图,同时检查包管理器记录、安装配置和工作区文件。

下面是选型时应执行的验证场景,并非本次已经完成的测试结果。

场景要确认的结果
正常停止,再恢复软件、配置、包记录相互匹配,程序能实际运行
删除运行实例,再创建按方案约定使用保留磁盘或快照,不悄悄退回公共镜像
原计算节点不可用独立持久数据仍可取得;无法恢复时明确报错
安装中断或保存失败不发布半成品环境,恢复点与用户看到的状态一致
两次修改或恢复并发旧实例、旧保存任务不能覆盖新的环境
删除软件后再恢复被删除的文件不会从基础镜像中重新出现
工作区独立变化明确代码与环境分别恢复到哪一版

性能测试则应固定基础镜像、软件集合和修改量,分别记录保存耗时、保存大小、冷恢复时间,以及第一个真实命令的完成时间。只有这些数据齐全,才能把方案能力、用户等待和存储费用放在同一张表里比较。

8. 环境留下来了,基础镜像怎样升级

持久化解决了安装结果丢失,也带来了下一个问题:公共基础镜像升级以后,用户保存的旧环境要怎样跟上。

一个项目可能在原基础镜像上安装了很多包。直接换成新版基础镜像,再覆盖旧环境的差异,可能混入两版系统库,甚至把已经修复的旧文件重新盖回去。因此,环境版本需要绑定基础镜像,升级应当是一次可验证的迁移。

声明式环境可以尝试在新镜像上重新构建;保存了任意文件修改的环境,则需要更谨慎地迁移和验证。可以先创建一份新环境,运行项目的真实任务,成功后再切换,并保留旧版本作为回退点。

到了这一步,平台管理的已经不只是一个会启动和销毁的 Sandbox,还包括用户逐渐积累起来的开发环境。它需要明确的保存点、版本和升级过程。这也是持久化方案选定以后,下一步要继续解决的事情。

文中实现状态截至 2026 年 10 月 13 日;项目级环境持久化流程为方案设计。