<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>存储 - Tag - SaiyuDoc</title><link>https://www.wangsaiyu.com/tags/%E5%AD%98%E5%82%A8/</link><description>存储 - Tag - SaiyuDoc</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>panicwang@outlook.com (王赛宇)</managingEditor><webMaster>panicwang@outlook.com (王赛宇)</webMaster><lastBuildDate>Tue, 13 Oct 2026 19:40:00 +0800</lastBuildDate><atom:link href="https://www.wangsaiyu.com/tags/%E5%AD%98%E5%82%A8/" rel="self" type="application/rss+xml"/><item><title>Sandbox 环境持久化：持久卷、磁盘保留与快照恢复</title><link>https://www.wangsaiyu.com/posts/engineering/sandbox-environment-persistence/</link><pubDate>Tue, 13 Oct 2026 19:40:00 +0800</pubDate><author>王赛宇</author><guid>https://www.wangsaiyu.com/posts/engineering/sandbox-environment-persistence/</guid><description><![CDATA[<p>用户让 Agent 安装了一个工具，安装成功，任务也执行完了。过一段时间回来继续用，项目文件都在，再执行那个命令却变成了 <code>command not found</code>。</p>
<p>我们遇到的就是这类问题。用户可以在 Sandbox 里安装软件，而实例回收、重新创建以后，安装过的东西没有跟着回来。</p>
<p><a href="/posts/engineering/sandbox-build-storage/" rel="">上一篇文章</a>讨论了如何把源码留在 NAS，把依赖安装和构建放到本地盘。这次要继续看工作区之外的内容：软件到底装在哪里，怎样让这些安装结果跨实例保存，以及三种持久化方案各自需要解决什么问题。</p>
<h2 id="1-文件还在软件为什么没了">1. 文件还在，软件为什么没了</h2>
<h3 id="11-先分清重启做了什么">1.1. 先分清“重启”做了什么</h3>
<p>先用一个简化场景说明。假设基础镜像没有 Graphviz，用户在 Debian 系的 Sandbox 中执行 <code>apt-get install graphviz</code>，随后用它的 <code>dot</code> 命令把项目里的关系图导出为图片。</p>
<p>这时有两份新内容：工作区里生成的图片，以及系统里新增的 Graphviz。两者虽然都能从同一个终端访问，实际可能写在完全不同的存储上。</p>
<p>容器启动时，根据基础镜像准备文件系统。镜像提供初始的系统文件，运行中新增和修改的内容进入这个实例的可写层。与此同时，平台把 NAS 上的项目目录挂进容器，作为工作区。这里把工作区简写为 <code>/workspace</code>。</p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/01-recreated-filesystem.svg" title="旧 Sandbox 的软件安装写入临时可写层，项目文件写入 NAS；新 Sandbox 从原基础镜像创建并重新挂载 NAS，因此只找回了项目文件" data-thumbnail="/images/sandbox-environment-persistence/01-recreated-filesystem.svg" data-sub-html="<h2>图 1：从原镜像重建实例时，工作区与系统环境的恢复路径不同</h2>">
        
    </a><figcaption class="image-caption">图 1：从原镜像重建实例时，工作区与系统环境的恢复路径不同</figcaption>
    </figure>
<p>如果只重启 Agent 进程，文件系统没有换掉，已经安装的文件通常还在。如果停止容器后继续启动原容器，能否保留文件，要看平台是否保留它的可写层。</p>
<p>我们遇到的另一条路径是：<strong>旧实例被销毁，再从基础镜像创建新实例。</strong> 新实例重新挂载原来的 NAS，所以代码和图片还在；它的系统文件重新来自基础镜像，后来安装的 Graphviz 就没有了。Docker 文档也明确区分了容器可写层和独立 Volume：删除容器会删除可写层，Volume 则有独立的生命周期。<a href="https://docs.docker.com/engine/storage/volumes/" target="_blank" rel="noopener noreffer ">Docker Volume</a></p>
<p>因此，排查时需要看实际发生的是进程重启、原实例恢复，还是销毁后重建。业务层沿用同一个 Sandbox ID，也不能证明底下仍然是同一份文件系统。</p>
<h3 id="12-一次安装会修改哪些地方">1.2. 一次安装会修改哪些地方</h3>
<p><code>apt</code>、<code>apk</code> 是系统包管理器，分别常见于 Debian 系和 Alpine 系发行版。它们安装软件时，不会把所有内容都放进当前工程目录。</p>
<p>下面列的是系统安装可能涉及的位置，不是 Graphviz 某个版本的完整文件清单。</p>
<table>
  <thead>
      <tr>
          <th>内容</th>
          <th>常见位置</th>
          <th>恢复时为什么需要它</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>可执行文件、共享库</td>
          <td><code>/usr/bin</code>、<code>/usr/lib</code> 等</td>
          <td>命令本身和运行时依赖</td>
      </tr>
      <tr>
          <td>系统配置</td>
          <td><code>/etc</code></td>
          <td>软件运行所需的配置</td>
      </tr>
      <tr>
          <td>包管理器记录</td>
          <td>Debian 的 <code>/var/lib/dpkg</code> 等</td>
          <td>记录已装的包、版本与安装状态</td>
      </tr>
      <tr>
          <td>安装脚本产生的修改</td>
          <td>用户、组、链接，以及其他目录</td>
          <td>让软件处于可用状态</td>
      </tr>
  </tbody>
</table>
<p>例如，只复制一个可执行文件，可能因为缺共享库而无法运行；只保存包管理器的数据库，则可能出现“记录显示已经安装，文件却不存在”的情况。Debian 的安装流程还会执行包中的维护脚本，处理配置等工作，安装结果不只是一组解压出来的文件。<a href="https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html" target="_blank" rel="noopener noreffer ">Debian 包安装过程</a></p>
<p>这里需要保存的是一套相互匹配的文件与元数据。后面提到的<strong>根文件系统（RootFS）</strong>，指的就是 Sandbox 从 <code>/</code> 开始看到的系统文件体系。独立挂载进去的 NAS、临时内存文件系统等，需要按各自的存储规则处理，不能默认算进一份 RootFS 备份里。</p>
<p>我们当时讨论的三条路，也就对应三个不同的保存范围：保留指定目录、保留原来的运行磁盘、把某一时刻的系统环境保存成快照。</p>
<h2 id="2-方案一把需要的目录挂到持久卷">2. 方案一：把需要的目录挂到持久卷</h2>
<h3 id="21-新实例重新挂载原目录">2.1. 新实例重新挂载原目录</h3>
<p>这是最容易从现有工作区方案继续扩展的一条路。</p>
<p>平台为项目准备一份独立存储，在每个新实例中挂到固定路径。写入这个目录的内容留在存储里，实例删除后，下一个实例再挂载同一份数据。NAS 可以承载这种目录；自建容器平台也可以通过 Volume 或 Kubernetes PVC 声明需要的存储。</p>
<p>但 Volume、PVC 说明的是挂载与管理方式，具体数据存在哪里，仍要看后端。Docker 的本地 Volume 默认在宿主机上；需要换节点恢复时，还必须有相应的共享存储、磁盘迁移或数据恢复能力。<a href="https://docs.docker.com/engine/storage/volumes/" target="_blank" rel="noopener noreffer ">Docker Volume 的存储与共享</a></p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/02-persistent-directories.svg" title="新旧实例都把 workspace 和用户工具目录挂到独立持久存储；系统包写入的 usr、etc 和包数据库仍在挂载范围之外" data-thumbnail="/images/sandbox-environment-persistence/02-persistent-directories.svg" data-sub-html="<h2>图 2：目录持久化覆盖哪些内容，由挂载位置和安装位置共同决定</h2>">
        
    </a><figcaption class="image-caption">图 2：目录持久化覆盖哪些内容，由挂载位置和安装位置共同决定</figcaption>
    </figure>
<p>平台至少需要记录项目对应的卷标识或远端目录、挂载路径、访问身份。创建新实例时，先完成挂载，再启动依赖这些目录的工具。挂错项目目录、改变安装路径，或者恢复后使用不同的用户权限，都可能让文件还在，却无法正常使用。</p>
<h3 id="22-适合能控制安装位置的工具">2.2. 适合能控制安装位置的工具</h3>
<p>例如，一个工具允许把自身及依赖安装到 <code>/home/user/tools</code>，我们可以把这个目录持久保存，启动时再配置好 <code>PATH</code>。对于能明确管理目录的用户级工具，这样做简单，也不需要备份整个系统。</p>
<p>语言依赖需要再看一层。保存 Python 虚拟环境的目录，并不保证换一个基础镜像后仍能运行：它可能依赖原来的解释器路径、系统库或本地编译扩展。恢复时应保持兼容的基础环境，或者根据依赖声明重新安装。</p>
<p>上一篇文章中的 <code>node_modules</code> 就采用了后者：保存源码、包声明和锁文件，在新的本地盘上重新准备依赖。这里主动保留的是重建输入，本地依赖目录可以丢弃。这有助于兼顾持久性和小文件访问性能。</p>
<h3 id="23-系统包为什么不能照搬">2.3. 系统包为什么不能照搬</h3>
<p>对于任意 <code>apt install</code> 或 <code>apk add</code>，平台无法事先假定所有修改都落在一个用户目录。多挂一个 <code>/home</code>，仍然覆盖不到 <code>/usr</code>、<code>/etc</code> 和系统包数据库。</p>
<p>把这些系统目录逐个挂出去，还会引出新的问题。挂载到已有目录上的外部文件系统，会遮住那个位置原有的内容；它不会自动把旧系统与新镜像正确合并。基础镜像升级后，外部目录里的旧库、旧配置和新系统之间，也需要处理兼容关系。<a href="https://docs.docker.com/engine/storage/volumes/#mounting-a-volume-over-existing-data" target="_blank" rel="noopener noreffer ">Docker 挂载覆盖行为</a></p>
<p>我们的 FC 环境还有平台限制：NAS 挂载文档要求使用适合的数据目录，不能把通用系统目录及其子目录作为挂载点，例如 <code>/bin</code>、<code>/opt</code>、<code>/var</code>、<code>/dev</code>。<a href="https://help.aliyun.com/zh/functioncompute/configure-a-nas-file-system-for-fc" target="_blank" rel="noopener noreffer ">FC 配置 NAS</a></p>
<p>所以，这个方案适合<strong>保存边界明确的目录</strong>。要支持用户自由安装系统软件，就需要把保存范围扩大到整套系统环境。</p>
<h2 id="3-方案二停止计算保留运行磁盘">3. 方案二：停止计算，保留运行磁盘</h2>
<h3 id="31-把磁盘的生命周期单独管理">3.1. 把磁盘的生命周期单独管理</h3>
<p>第二种方案直接保留已经安装好的那份文件系统。</p>
<p>对于虚拟机，可以是保留系统盘；对于容器，可以由运行平台保留根文件系统的可写部分，并继续使用匹配的基础镜像。用户安装软件后，文件和包管理器记录都在这份环境里。停止计算时保留它，恢复时继续使用它。</p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/03-retained-root-disk.svg" title="计算实例停止后保留项目的根磁盘或可写层，恢复时继续使用；磁盘删除由单独的生命周期操作控制" data-thumbnail="/images/sandbox-environment-persistence/03-retained-root-disk.svg" data-sub-html="<h2>图 3：计算可以停止，环境磁盘仍由项目持有</h2>">
        
    </a><figcaption class="image-caption">图 3：计算可以停止，环境磁盘仍由项目持有</figcaption>
    </figure>
<p>这样，前面的 Graphviz 无须重新安装。恢复时取得原来的磁盘，软件文件、配置和包记录就一起回来了。进程则通常要重新启动，磁盘持久化本身不包含进程内存。</p>
<p>Daytona 的普通 Container Sandbox 提供了这类使用语义：stop/start 保留文件与安装包，停止时清除内存；删除 Sandbox 则是另一种操作。具体选型仍需核对 Sandbox 类型和自动删除配置，不能只看一个“支持持久化”的介绍。<a href="https://www.daytona.io/docs/en/persistence/" target="_blank" rel="noopener noreffer ">Daytona 持久化说明</a></p>
<h3 id="32-平台需要记录什么">3.2. 平台需要记录什么</h3>
<p>业务层不能只保存“项目正在使用哪个实例”。还需要保存项目的环境磁盘标识、匹配的基础镜像版本，以及当前由哪个实例使用。</p>
<p>正常停止时，平台应让安装和写入结束，再按存储系统的要求完成落盘，最后停止计算、保留磁盘。恢复时找到这份磁盘，确认旧实例已经停止写入，再交给新的运行实例。删除项目时，才根据保留策略删除它。</p>
<p>这里有一个容易忽略的情况：旧实例网络断开，平台认为它失联，于是又启动了一个新实例。旧实例其实还活着，两个实例就可能同时修改包数据库。管理服务中的一把锁还不够，平台需要通过磁盘挂载约束或存储侧的写入控制，阻止已经失去所有权的旧实例继续写。</p>
<h3 id="33-磁盘保留在哪决定了能恢复到哪里">3.3. 磁盘保留在哪，决定了能恢复到哪里</h3>
<p>如果只是让容器停在原机器上，并保留那台机器的本地可写层，原地启动确实方便。但宿主机永久故障以后，能否恢复就取决于有没有其他持久副本。</p>
<p>要允许换节点恢复，需要磁盘本身能脱离原计算节点，例如使用可重新挂载的持久块存储，并满足可用区、文件系统和运行时的约束。异常断电后还可能面对文件系统恢复、包安装中断等状态，保存磁盘不会自动把安装过程变成一个事务。</p>
<p>这条路的主要成本是长期保留每个项目的环境空间，以及磁盘挂载、迁移和故障处理。计算停止后，存储仍然占用资源。它适合经常回来继续工作的开发环境；若有大量长期不活跃的项目，还需要归档和回收机制。</p>
<h2 id="4-方案三把环境保存成文件系统快照">4. 方案三：把环境保存成文件系统快照</h2>
<h3 id="41-新实例从保存的环境启动">4.1. 新实例从保存的环境启动</h3>
<p>第三种方案允许原实例彻底退出。退出之前，先把某个时刻的根文件系统保存为一个可恢复版本；下次根据这个版本创建新实例。</p>
<p>快照可以是完整磁盘副本，也可以只记录相对基础镜像的变化。后者能够复用公共镜像，项目只保存自己新增、修改和删除的内容。这里讨论的是文件系统快照；要继续执行原来的进程，还涉及另外的内存与设备状态恢复。</p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/04-snapshot-restore.svg" title="旧实例安装软件后生成环境快照，基础镜像和快照共同准备新实例的系统文件；NAS 工作区通过独立挂载恢复" data-thumbnail="/images/sandbox-environment-persistence/04-snapshot-restore.svg" data-sub-html="<h2>图 4：快照保存一个确定的环境版本，新实例从这个版本恢复</h2>">
        
    </a><figcaption class="image-caption">图 4：快照保存一个确定的环境版本，新实例从这个版本恢复</figcaption>
    </figure>
<p>例如，Modal 的文件系统快照记录 RootFS 相对基础镜像的差异，生成的 Image 可以创建新 Sandbox。它的文档也明确说明，挂载的 Volume 不包含在这份快照里；快照还需要配置保留期限。<a href="https://modal.com/docs/guide/sandbox-snapshots" target="_blank" rel="noopener noreffer ">Modal 文件系统快照</a></p>
<p>恢复 Graphviz 时，平台不再重新执行安装命令，而是从快照取得已经安装好的文件与状态。快照生成以后，即使原实例被删除，只要快照及其依赖仍在，便可以据此准备另一个实例。</p>
<p>保存差异时，删除操作也必须被记录。假设用户删除了基础镜像中的一个配置文件，只备份“当前新增的文件”，恢复后那个旧配置又会出现。以 OverlayFS 为例，它会用 whiteout 等元数据表达对下层文件的删除；这类信息不能在复制过程中丢失。<a href="https://docs.kernel.org/filesystems/overlayfs.html" target="_blank" rel="noopener noreffer ">Linux OverlayFS</a></p>
<h3 id="42-在什么时刻保存">4.2. 在什么时刻保存</h3>
<p>最直接的做法是正常关闭前保存一次。但节点故障、强制回收等情况，不保证有机会执行关闭脚本。要减少丢失，就要在环境修改后建立保存点。</p>
<p>用户安装 Graphviz 后，当前实例里已经有新软件，但持久存储中还是上一次保存的环境。只有新快照生成、上传并确认可恢复之后，才能把它设为项目下次启动时使用的环境。</p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/05-environment-commit.svg" title="环境修改完成后创建快照，保存并检查成功才更新项目的当前环境；中途失败继续使用上次保存的环境" data-thumbnail="/images/sandbox-environment-persistence/05-environment-commit.svg" data-sub-html="<h2>图 5：安装完成与环境保存完成，是两个不同的时刻；流程为方案设计</h2>">
        
    </a><figcaption class="image-caption">图 5：安装完成与环境保存完成，是两个不同的时刻；流程为方案设计</figcaption>
    </figure>
<p>一种可采用的保存流程是：</p>
<ol>
<li>暂停同一环境上的其他修改，确认安装进程及其后续配置已经完成。</li>
<li>按快照实现的要求，完成写入刷新，并取得一致的文件系统视图。</li>
<li>生成新快照，保存其数据与元信息，检查它已经能够被恢复。</li>
<li>把新版本标记为可用，再原子地切换项目的当前版本。</li>
</ol>
<p>单独调用一次 <code>sync</code> 不会阻止其他进程继续写，也不保证跨多个存储的一致性。如果软件还在修改文件，需要先让相关写入停下来，或使用平台提供的协调机制。快照内部完成的原子性范围，也应由具体实现说明。</p>
<p>如果第三步失败，下次启动仍使用上一次保存成功的环境。这样不会把半份快照交给下一次启动，但尚未保存的新安装也不能保证回来。因此，产品需要区分“本次安装可用了”和“环境已保存，可以重建恢复”。如果承诺每次安装成功后都能恢复，就必须把保存完成纳入这次操作的成功条件；允许任意终端命令时，则还需要明确的环境保存点或受控修改入口。</p>
<h3 id="43-快照记录不能只有一个文件地址">4.3. 快照记录不能只有一个文件地址</h3>
<p>管理服务需要在实例之外保存项目与环境版本的关系。可以在数据库中记录当前使用哪一版环境，每个版本关联以下信息。</p>
<table>
  <thead>
      <tr>
          <th>保存内容</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>环境标识与版本</td>
          <td>确定这是哪个项目的哪一版环境</td>
      </tr>
      <tr>
          <td>基础镜像的内容摘要</td>
          <td>绑定生成快照时使用的确切镜像</td>
      </tr>
      <tr>
          <td>快照位置</td>
          <td>找到已保存的文件系统数据及依赖</td>
      </tr>
      <tr>
          <td>保存状态</td>
          <td>区分正在保存、可恢复和保存失败</td>
      </tr>
      <tr>
          <td>恢复条件</td>
          <td>记录架构、运行时等兼容要求</td>
      </tr>
      <tr>
          <td>工作区关联</td>
          <td>找到独立保存的项目工作区</td>
      </tr>
  </tbody>
</table>
<p>并发保存时，还需要检查当前版本和写入所有权。保存任务开始时记下项目使用的环境版本，更新时确认它没有被其他任务替换，且自己仍持有有效写入权，再切换到新快照。检查与切换需要原子完成，避免超时后继续执行的旧任务覆盖已经发布的新环境。</p>
<p>NAS 工作区不随 RootFS 快照一起回退。如果还希望把代码与环境一起回到过去，就要额外保存代码提交或工作区版本，并协调两者的对应关系。只记录工作区标识，找到的仍然是同一个目录，并不代表这个目录在历史时刻的内容。</p>
<h2 id="5-三种方案放在一起看">5. 三种方案放在一起看</h2>
<p>三种方案都能保存文件，区别在于保存范围和恢复方式。</p>
<div style="overflow-x: auto;">
<div style="min-width: 42rem;">
<table>
  <thead>
      <tr>
          <th>比较项</th>
          <th>指定目录持久化</th>
          <th>保留运行磁盘</th>
          <th>文件系统快照</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>保存范围</td>
          <td>选定的挂载目录</td>
          <td>原来的系统磁盘或完整可写部分</td>
          <td>某个保存点的根文件系统</td>
      </tr>
      <tr>
          <td>任意系统包安装</td>
          <td>仅目录方案无法完整覆盖</td>
          <td>在保留范围内可以覆盖</td>
          <td>在快照范围内可以覆盖</td>
      </tr>
      <tr>
          <td>恢复方式</td>
          <td>新实例挂载原目录</td>
          <td>继续使用原环境磁盘</td>
          <td>从已保存版本创建实例</td>
      </tr>
      <tr>
          <td>原节点永久故障</td>
          <td>取决于卷的存储后端</td>
          <td>取决于磁盘是否独立持久</td>
          <td>取决于快照与依赖是否独立持久</td>
      </tr>
      <tr>
          <td>回到历史环境</td>
          <td>需要另做目录版本</td>
          <td>需要另做磁盘快照</td>
          <td>可选择仍保留的快照版本</td>
      </tr>
      <tr>
          <td>主要额外工作</td>
          <td>目录边界、路径与权限</td>
          <td>磁盘所有权、迁移与回收</td>
          <td>保存时机、版本提交与垃圾回收</td>
      </tr>
  </tbody>
</table>
</div>
</div>
<p><em>手机上可左右滑动查看完整对比。</em></p>
<p>表里的“可以覆盖”有前提：保存的文件范围完整，元数据正确，并使用兼容的恢复环境。外部服务状态、进程内存、单独挂载的数据卷，不会因为保留 RootFS 就全部跟着保存。</p>
<p>性能也需要按用户真正等待的路径来测。目录方案要看挂载后访问文件的成本；保留磁盘要看重新挂载与启动的时间；快照方案则要看生成、传输、恢复，以及恢复后第一次真正使用软件的时间。延迟加载可以把一部分恢复工作推迟到首次读文件，单看“实例启动完成”容易漏掉这部分等待。</p>
<p>这次回顾没有三种方案在同一负载下的耗时和费用实测，因此不能给出谁快多少的排名。更适合先按需求缩小范围：固定工具链与可控目录优先考虑持久卷；需要频繁继续使用同一套环境，可以评估保留磁盘；需要释放原实例、回滚或从同一环境分出多个副本，快照提供的版本能力更直接。</p>
<p>三者也可以组合。例如，活跃项目保留磁盘，长期不活跃时归档为快照；源码继续放 NAS，构建缓存留在可丢弃的本地盘。</p>
<h2 id="6-放回我们的-fc-架构怎样接入">6. 放回我们的 FC 架构，怎样接入</h2>
<h3 id="61-现有实现保存到了哪一层">6.1. 现有实现保存到了哪一层</h3>
<p>现有实现把 NAS 挂到项目工作区；恢复时，根据保存的记录选择镜像和运行配置，再创建实例。部分配置变化还会触发旧实例删除与重新创建。</p>
<p>这条创建、恢复链路没有携带每项目的 RootFS 快照或持久系统盘标识。基础镜像预装的工具会随镜像重新出现，用户在旧实例中临时安装的软件则没有相应的恢复步骤。FC 文档也说明，自定义容器的可写层随容器销毁而删除。<a href="https://www.alibabacloud.com/help/en/functioncompute/custom-container/" target="_blank" rel="noopener noreffer ">FC 自定义容器</a></p>
<p>目前能从代码中确认的，是公共工具预装、NAS 工作区，以及本地项目依赖的重新准备。项目级环境磁盘、RootFS 快照和下面的派生镜像流程，仍属于这里讨论的设计，不能据此声称已经上线。</p>
<h3 id="62-继续使用-fc可以先约束可保存的安装">6.2. 继续使用 FC，可以先约束可保存的安装</h3>
<p>如果暂时继续使用当前的 FC 运行方式，又无法取得和恢复容器的完整可写层，可以把可持久化的安装收敛为声明式配置：记录项目需要哪些系统包、版本和来源，再构建一份项目专用镜像。</p>
<p>例如，用户通过受控入口增加 Graphviz，平台记录基础镜像 digest、发行版与软件源、包名和版本。构建任务在隔离环境中生成派生镜像，验证后推送到镜像仓库，再把项目的环境镜像指针切换过去。下次重建时，创建逻辑使用这个 digest。</p>
<p>软件源也要纳入设计。只记一个包版本，如果上游已经不再提供对应包，仍可能构建失败。长期重建需要可保留的包来源、仓库快照或构建产物。已经构建成功的镜像可以复用，不必在每次启动时重新下载和安装。</p>
<p>这种方式保存的是<strong>明确声明并成功构建的环境</strong>。用户随手修改 <code>/etc</code>，或者运行一个会改动多个位置的安装脚本，不能仅靠记录命令就完整恢复。允许的包管理入口、额外配置文件和安装脚本范围，需要作为产品能力明确下来。</p>
<p>它是现有平台约束下的一条接入路径，与前面的 RootFS 快照有不同的保存语义。构建失败时也不能先更新项目镜像；应该保留上一版可用环境，并说明这次修改尚未持久保存。</p>
<h3 id="63-完整环境持久化需要运行平台配合">6.3. 完整环境持久化需要运行平台配合</h3>
<p>如果产品要让用户自由安装系统软件，并在实例重建后继续使用，就应选择能保存完整环境的运行平台，或者自行提供磁盘与快照能力。这已经涉及 Sandbox 创建和回收的接口，工作区里的脚本无法单独完成。</p>
<figure><a class="lightgallery" href="/images/sandbox-environment-persistence/06-layered-environment.svg" title="目标布局由公共基础镜像、每项目环境版本、NAS 工作区和本地缓存组成；管理服务分别保存这些资源的标识并在创建实例时恢复" data-thumbnail="/images/sandbox-environment-persistence/06-layered-environment.svg" data-sub-html="<h2>图 6：环境与工作区分别持久保存，本地缓存按需重建；这是目标布局</h2>">
        
    </a><figcaption class="image-caption">图 6：环境与工作区分别持久保存，本地缓存按需重建；这是目标布局</figcaption>
    </figure>
<p>创建接口需要接受环境版本，保存接口需要返回可恢复的磁盘或快照标识，删除接口则要区分“释放这次计算”和“删除项目环境”。恢复完成后，确认软件真正可用，再让 Agent 继续执行任务。</p>
<p>对应的回收也要分开：本地缓存可以随实例删除；项目当前环境不能被普通实例清理顺手删掉；旧快照只有在不再被项目、恢复任务和派生版本引用时，才能回收。还要设置容量配额，并在写入时返回明确错误，避免用户安装了很大的软件以后，环境迟迟无法保存。</p>
<p>环境快照也不宜包含运行时注入的临时凭证。凭证应放在独立的临时挂载或注入机制中，恢复时重新取得，而不是把一个可能过期的访问令牌随环境版本长期保存。</p>
<h2 id="7-怎样确认软件确实保住了">7. 怎样确认软件确实保住了</h2>
<p>验收不能只看“Sandbox 又启动了”。对于开头的例子，应在新实例中执行 <code>dot</code>，实际生成一张图，同时检查包管理器记录、安装配置和工作区文件。</p>
<p>下面是选型时应执行的验证场景，并非本次已经完成的测试结果。</p>
<table>
  <thead>
      <tr>
          <th>场景</th>
          <th>要确认的结果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>正常停止，再恢复</td>
          <td>软件、配置、包记录相互匹配，程序能实际运行</td>
      </tr>
      <tr>
          <td>删除运行实例，再创建</td>
          <td>按方案约定使用保留磁盘或快照，不悄悄退回公共镜像</td>
      </tr>
      <tr>
          <td>原计算节点不可用</td>
          <td>独立持久数据仍可取得；无法恢复时明确报错</td>
      </tr>
      <tr>
          <td>安装中断或保存失败</td>
          <td>不发布半成品环境，恢复点与用户看到的状态一致</td>
      </tr>
      <tr>
          <td>两次修改或恢复并发</td>
          <td>旧实例、旧保存任务不能覆盖新的环境</td>
      </tr>
      <tr>
          <td>删除软件后再恢复</td>
          <td>被删除的文件不会从基础镜像中重新出现</td>
      </tr>
      <tr>
          <td>工作区独立变化</td>
          <td>明确代码与环境分别恢复到哪一版</td>
      </tr>
  </tbody>
</table>
<p>性能测试则应固定基础镜像、软件集合和修改量，分别记录保存耗时、保存大小、冷恢复时间，以及第一个真实命令的完成时间。只有这些数据齐全，才能把方案能力、用户等待和存储费用放在同一张表里比较。</p>
<h2 id="8-环境留下来了基础镜像怎样升级">8. 环境留下来了，基础镜像怎样升级</h2>
<p>持久化解决了安装结果丢失，也带来了下一个问题：公共基础镜像升级以后，用户保存的旧环境要怎样跟上。</p>
<p>一个项目可能在原基础镜像上安装了很多包。直接换成新版基础镜像，再覆盖旧环境的差异，可能混入两版系统库，甚至把已经修复的旧文件重新盖回去。因此，环境版本需要绑定基础镜像，升级应当是一次可验证的迁移。</p>
<p>声明式环境可以尝试在新镜像上重新构建；保存了任意文件修改的环境，则需要更谨慎地迁移和验证。可以先创建一份新环境，运行项目的真实任务，成功后再切换，并保留旧版本作为回退点。</p>
<p>到了这一步，平台管理的已经不只是一个会启动和销毁的 Sandbox，还包括用户逐渐积累起来的开发环境。它需要明确的保存点、版本和升级过程。这也是持久化方案选定以后，下一步要继续解决的事情。</p>
<p><em>文中实现状态截至 2026 年 10 月 13 日；项目级环境持久化流程为方案设计。</em></p>]]></description></item></channel></rss>