几十 GiB 快照秒回、克隆“零拷贝”:Cube 快照克隆回滚技术原理深层揭秘

发布时间:2026/6/26 16:06:22
几十 GiB 快照秒回、克隆“零拷贝”:Cube 快照克隆回滚技术原理深层揭秘 如果你在一台普通 Linux 服务器上跑过 Cube Sandbox v0.3.0可能注意到几个反直觉的现象 磁盘快照秒回对一个文件系统几十 GiB 的沙箱发起快照命令几乎瞬间返回磁盘没有发生数 GiB 的写入。 内存快照只写少量页一个跑着大模型推理、占着几十 GiB guest RAM 的沙箱频繁 checkpoint 的转储量明显小于 guest 实际占用的内存——快照不再是把整块 RAM 重新落盘一遍。 克隆零拷贝对一个运行中的沙箱一次派生 10 份独立副本磁盘空间几乎没增加10 份副本却各自能在自己的内存和文件系统里写入而互不干扰。这些看起来像魔法的现象背后其实由三个互相咬合的底层机制共同支撑。本文从这三个谜题切入一层一层揭开 v0.3.0 版本中快照 / 克隆 / 回滚三项核心能力的底层原理。◆引言三个让人意外的现象◆如果你在一台普通的 Linux 服务器上跑过 Cube Sandbox v0.3.0可能注意到几个反直觉的现象◆磁盘快照秒回对一个文件系统几十 GiB 的沙箱发起快照命令几乎瞬间返回磁盘没有发生数 GiB 的写入。◆内存快照只写少量页一个跑着大模型推理、占着几十 GiB guest RAM 的沙箱频繁 checkpoint 的转储量明显小于 guest 实际占用的内存——快照不再是把整块 RAM 重新落盘一遍。◆克隆零拷贝对一个运行中的沙箱一次派生 10 份独立副本磁盘空间几乎没增加10 份副本却各自能在自己的内存和文件系统里写入而互不干扰。这些看起来像魔法的现象背后其实由三个互相咬合的底层机制共同支撑。本文从这三个谜题切入一层一层揭开 v0.3.0 快照 / 克隆 / 回滚能力的底层原理。◆一、揭秘的总入口三个谜题与五层架构◆整个快照 / 克隆 / 回滚体系的复杂度来自一个事实——VM 状态 磁盘 内存二者必须保持一致地被捕获、被还原、被复制。Cube Sandbox 把它拆成两个独立又协作的子系统◆磁盘子系统基于 XFS reflink 的文件级 CoW 引擎。◆内存子系统在传统 hypervisor 快照框架之上引入 pagemap_anon 与 soft-dirty 真增量。它们各自跨越五层调用读完接下来三章引言里的三个谜题就会变成三个清晰的内核机制谜题核心机制磁盘秒回XFS Reflink FICLONE ioctl内存少写/proc/self/pagemap soft-dirty bit55克隆零拷贝把 clone(n) 拆成 snapshot n 次基于快照的 create◆二、谜题一磁盘快照为什么秒回◆2.1 表象对一个运行中的沙箱打快照时磁盘在整个过程动作的本质是一次 ioctl。核心 ioctlFICLONE向目标文件发起源文件的 copy-on-write 克隆。它具有以下特性维度CubeCow Reflink工作层次文件系统层时间复杂度O(1)单次 ioctl持久化方式文件系统本身即 source of truth无独立 ledger崩溃恢复每次操作是单个 fs 事务天然 crash-safe内核依赖FICLONE ioctlXFS -m reflink1内核层面只共享 extent 元数据写入时才分裂数据块——这就是秒回的真相。2.2 揭秘XFS Reflink 内部到底做了什么◆① inode / Extent MapBMBT/ 物理块 三层结构要理解 reflink先得看 XFS 是怎么把逻辑文件偏移映射到物理磁盘块的BMBTBlock Mapping B-Tree是 XFS 中 inode 内嵌的 B 树存储逻辑偏移到物理块的映射表即 Extent Map。每条 Extent 记录格式(logical_offset, physical_block, length, shared_flag)。shared_flag 置位表示该物理块被多个文件 inode 引用写入时必须触发 CoW unshare。◆② FICLONE ioctl 执行路径O(1) 元数据操作FICLONE 之所以能在毫秒级完成是因为它只动元数据Refcount B-Treexfs_rmap_btreeXFS 维护一棵全局 B-Tree记录每个物理块的引用计数。FICLONE 后被共享的块的 refcount ≥ 2refcount 降为 1 时该块重新成为独占状态可被直接写入无需 CoW。◆③ 写入时 CoW Unshare 路径那共享之后再写入会发生什么答案是写时分裂写入只影响源卷的 Extent Map快照的 BMBT 和物理块完全不变实现了快照隔离。2.3 揭秘CubeCow 在 reflink 之上额外做的事裸 reflink 只解决如何快不解决如何管。CubeCow 在引擎层叠加了三个工程化设计◆① 快照链扁平化避免 snap-of-snap 链式追踪标准 reflink 支持快照的快照但 CubeCow 把所有快照的 origin_volume 统一记录为最终祖先卷快照文件也物理上放在祖先卷的目录下。扁平化的好处所有快照在文件系统视角是与主卷平级的独立文件——各自拥有独立的块映射相互之间没有父子隶属关系血统只是 CubeCow 内存索引里的一条逻辑信息。删除任意一个中间快照退化成对一个普通文件的 unlink目录里去掉一个目录项曾经共享的物理块由 XFS Refcount B-Tree 自动减一其它快照完全不受影响。目录结构与 origin_volume 一一对应无需递归查找。删除原卷主文件后目录因快照文件存在而保留最后一个快照删除时目录自动回收。◆② 文件系统即 Source of Truth无 on-disk ledger所有元数据均可从目录结构重建卷列表 readdir(volumes/)快照列表 readdir(volumes/vol/) 去掉主文件大小 stat时间戳 mtime引擎启动时 scan_and_rebuild_index() 扫盘重建索引按以下规则处理崩溃残留磁盘状态处理方式vol/vol 主文件存在注册为 Volume目录存在但主文件缺失且无子文件删除空目录孤儿目录存在但主文件缺失且有子文件警告恢复子快照但不注册卷零字节快照文件删除崩溃时 FICLONE 未完成重名冲突警告并跳过◆③ 内存命名空间扁平化ReflinkEngine 维护一个 RwLockHashMapString, NameKind 作为全局命名空间卷名与快照名共享同一全局命名空间写锁下原子预占防止并发命名冲突。这意味着任何一次新建卷或新建快照都不需要做先查血统再确认无重名的递归校验直接一次锁内查重即可。2.4 揭秘小结一次快照在引擎层只有三类不可省略的动作抢名 - 一次 FICLONE - 落盘目录项。三者都是 O(1) 量级全部不涉及数据块拷贝——这就是秒回的全部秘密。◆三、谜题二内存快照如何只写少量页◆3.1 表象与挑战VM 内存动辄几十 GiB如果每次快照都把整块 guest RAM 写盘IO 放大会让频繁 checkpoint完全不可用。v0.3.0 的内存快照同时引入了两条优化路径配合磁盘 reflink把稳态下的内存写入量压到最小。性能数据可参考CubeSandbox v0.3.0让 AI Agent 拥有时光机和“分身术”3.2 三种模式的定义Cube Sandbox 这里针对内存快照有三种模式对应三种我到底要把哪些页写到镜像里的策略模式写入对象适用场景Full完整 guest 内存镜像所有页都写第一次快照、强一致性归档Incremental仅 CoW 匿名页即 guest 真正分配过、有内容的页大多数稳态快照SoftDirty真增量仅自上次复位以来被写过的匿名页高频 checkpoint内核需要 CONFIG_MEM_SOFT_DIRTY3.3 揭秘Incremental —— 匿名页恰好就是自启动以来写过的页关键前提v0.3.0 的沙箱是基于快照启动的理解 Incremental 的精妙之处必须先认清一个前提在 Cube Sandbox 里几乎所有的 VM 都是从一份内存快照恢复出来的——首次创建沙箱时从模板的内存镜像启动克隆出来的副本从临时快照启动回滚之后的 VM 从目标快照启动。冷启动到完全零状态的场景在生产路径上几乎不存在。VMM 在恢复时不会把整份内存镜像 read() 到一段匿名 mmap 里——那样既慢又浪费。它的做法是用 mmap(MAP_PRIVATE, fdmemory_image) 把内存镜像文件直接映射到 guest RAM 对应的虚拟地址空间。这一步只建立 VMA不读任何数据guest 跑起来后真正访问到哪一页内核才按需把那一页从 page cache 填进来。MAP_PRIVATE 的二分语义文件页 vs 匿名页MAP_PRIVATE 的核心语义是copy-on-write of a fileguest 对该页的行为内核侧的页类型物理占用从未访问不存在 PTE按需缺页0只读访问过文件页共享 page cachePTE 只读指向 page cache 帧与同进程内其它快照实例共享写过至少一次匿名页CoW unshare 出的进程私有页该 VM 进程独占注意第二行guest 只读访问过的页仍然是文件页——它们物理上停留在内存镜像文件的 page cache 里与从同一份镜像启动的其它 VM 共享不计入本进程的匿名页统计。只有当 guest 第一次往某页写入时内核才会触发 CoW把这一页从文件页分裂为本进程独占的匿名页。这就给出了一个白送的等价关系本 VM 进程的匿名页集合 ≡ 自该 VM 从快照启动以来真正写过的页集合这个等价关系不需要任何额外跟踪、没有任何运行时开销——它就是 MAP_PRIVATE 语义的自然产物。Linux 内核在每次写时缺页里早已为我们维护好了它。Incremental 怎么读出这个集合利用上面的等价哪些页需要写入快照被简化成哪些页是匿名页。Linux 在 /proc/pid/pagemap 里以每页 8 字节暴露每条虚拟页的状态关键位有位含义bit 63该 VPN 是否映射了物理页帧presentbit 62是否被换出到 swapbit 61是否为匿名页即被 CoW 分裂过的私有页bit 0–54物理页帧号 PFNpresent 时Incremental 的过滤条件正是present ∧ anonymous——直接对应自启动以来真正写入过的页。每页只需 8 字节元数据判断无需读取页内容。完整性如何保证Incremental 写出去的快照文件保留完整的 guest 物理地址布局——那些没写入的偏移继承上一份快照的内容。这要求◆目标文件已存在且内容是 reflink-clone 自上一份快照——未被本次写入覆盖的偏移自动等于上一份快照对应位置。◆ 文件页那部分guest 只读访问过、或从未访问的页在新快照文件里和上一份快照一字不差因为它们本来就是同一份内存镜像的内容。这就是 3.1 里说的磁盘子系统反过来支撑内存子系统——没有 reflink-clone 提供的廉价基线文件Incremental 就没法用只写一部分得到完整镜像。Incremental 解决了什么、还差什么Incremental 用零运行时开销实现了一次性优化把全部 guest RAM收敛到自启动以来写过的页。对短生命周期的沙箱典型场景一次性任务执行、短时克隆体这已经足够。但对长期运行的沙箱这个集合只增不减——guest 跑得越久被写过的页就越多自启动以来写过的页会逐渐逼近所有已分配页。这就是 SoftDirty 要解决的问题。3.4 揭秘SoftDirty —— 用 bit55 抓真正写过的页动机为什么 Incremental 不够考虑一个长期运行的沙箱比如一台跑着推理服务的 VM它的生命周期里被多次定期 checkpoint时刻guest 累计写过的页该次 Incremental 快照写入量启动后 t₁200 MiB200 MiBt₁ 后再跑 1 分钟t₂1 GiB1 GiBt₂ 后再跑 10 分钟t₃5 GiB5 GiBt₃ 后再跑 1 小时t₄15 GiB15 GiB虽然两次快照之间真正发生变化的可能只有几十 MiB但 Incremental每次都把自启动以来全部写过的页重新写一遍。匿名页集合是单调递增的转储量随运行时间线性增长最终逼近 Full 模式。要让频繁 checkpoint在长跑沙箱上仍然可用必须能识别上次快照之后才被写过的页。这就是 SoftDirty 要补上的一块。soft-dirty bit 的内核语义Linux 在每个 PTE 里预留了 soft-dirty bit在 /proc/pid/pagemap 里以 bit 55 暴露给用户态它的状态机非常简单触发动作效果进程往一页写入该页 PTE 的 soft-dirty 被内核置位用户态写 /proc/pid/clear_refs值 4内核遍历进程所有 PTE清掉所有 soft-dirty 标记并把对应 PTE 改成只读复位之后再次写入触发写保护缺页 - 内核还原可写 重新置位 soft-dirty复位之后的 bit551 就精确等价于自上次复位以来这一页有写过。过滤条件在匿名页之上加一层时间窗SoftDirty 模式不是抛弃 Incremental而是在它的输出集合上再叠加一层 soft-dirty 过滤要写入快照的页 { p | present(p) ∧ anonymous(p) ∧ soft_dirty(p) }└────── Incremental 集合 ──────┘ └ 增量过滤 ┘anonymous 把范围收敛到自启动以来写过的页参考 3.3soft_dirty 再把范围收敛到自上次复位以来写过的页。前者是累积窗后者是滑动窗——两者交集就是既属于该 VM 私有内存、又是这次快照真正需要刷新的页。落到代码视角就是同一段 pagemap 扫描里多读一位、多比一次按位与几乎无额外成本。也正因为这一项是叠加而非替换SoftDirty 在内核不支持时可以静默降级为 Incremental——丢掉的只是最后那个 ∧ soft_dirty 项正确性不受影响。首次快照为什么自动等价于 Incrementalsoft-dirty 的初始状态是关键内核在为页表建立 PTE 的瞬间会把soft-dirty 默认置 1—— 内核语义本身就是如果这页存在 PTE 但你从没复位过那就当它是脏的。所以第一次拍 SoftDirty 快照时过滤条件自动退化为 Incremental——把所有匿名页都写一遍正好就是这次快照需要的完整基线。然后我们才在写完之后调用 clear_refs 把 soft-dirty 全部清零给下一次稳态快照建立基准。这是个很优雅的性质SoftDirty 的首次快照不需要任何特殊分支过滤公式 anonymous ∧ soft_dirty 在两种状态下都是正确的——首次时 dirty 全 1 退化成 Incremental之后每次都拿到真增量。一致性的两条铁律要让上面这套机制正确必须保证两点◆复位与写入不能交错。如果在复位 - 拍下一份快照之间允许 guest 自由写入会出现我写了但 bit 已被清的窗口。v0.3.0 的策略是每次打 SoftDirty 快照时 guest 已被 pause在 pause 状态下完成读 pagemap - 写出快照 - 复位 bit - resume。◆复位必须发生在快照之后而不是快照之前。本次快照消费的是上一次复位以来累计的脏标记本次写完后再复位下一次快照才有正确的基准。首次快照同样遵循这条铁律——只不过它消费的是内核给的初始全 1写完之后第一次 clear_refs 才让真正的增量计时开始。副作用复位的代价被合理摊销clear_refs 不是免费的——内核要做一次全量页表项扫描把每个 PTE 改写为只读并清位。对多 GiB guest这是数百毫秒级的开销并且扫描期间 guest 后续的写入会引发额外的写保护缺页。得益于上面首次快照即 Incremental的性质v0.3.0 不需要为了 SoftDirty 在 VM 启动 / 恢复时提前付一次复位代价那会让 VM ready 之后第一次进入用户空间时卡住几百毫秒而是把第一次 clear_refs 自然地推迟到首次快照的写出之后——此时用户已经在等待快照命令返回复位的代价摊在了用户预期会消耗时间的操作里对体验是无感的。Incremental vs SoftDirty 的取舍维度IncrementalSoftDirty内核要求/proc/pagemap普遍可用额外要求 CONFIG_MEM_SOFT_DIRTY过滤强度已分配的匿名页已分配 ∧ 真写过 的匿名页状态副作用无每次都现读 pagemap有需要复位 PTE影响写保护缺页路径适合的频率中低频快照、首次快照、降级兜底高频 checkpoint失败时行为永远可用自动降级到 Incremental3.5 外部内存卷支持内存镜像可以写到独立存储介质独立卷或独立路径而不是和状态 JSON 同目录。这种模式对在不同存储池之间分担内存镜像的 I/O 压力很有帮助内部模式倾向于截断重建以保证快照之间的独立性外部卷模式倾向于打开后原地写入从而让外部卷可以在多次快照之间被 reflink 复用。◆四、谜题三克隆 N 份为何零拷贝◆4.1 表象对一个跑着的源沙箱一次派生 N 个克隆磁盘空间几乎没增加每个克隆都能独立读写。每个克隆都满足三性质性质含义继承性每个副本的初始状态与源沙箱在 clone 时刻完全一致隔离性副本之间的写入互不可见与源沙箱也互相隔离连续性源沙箱在 clone() 返回后仍在运行状态不受影响4.2 揭秘clone(n) 不是新原语而是三个旧原语的组合clone(n) 在协议层根本没有新增任何克隆 RPC它在概念上等价于def clone(self, n1, *, concurrency1):snap self.create_snapshot() # ① 一次源沙箱快照try:new_sbs [Sandbox.create(templatesnap.snapshot_id)for _ in range(n)] # ② n 次基于快照的 createfinally:Sandbox.delete_snapshot(snap.snapshot_id) # ③ best-effort 清理return new_sbs4.3 揭秘为什么零拷贝和完全隔离能同时成立这是磁盘子系统和内存子系统两套机制叠加的结果维度共享什么写入时怎么隔离磁盘 rootfs共享 XFS extentrefcount ≥ N1XFS reflink CoW unshareguest 内存共享 reflink-clone 的 memory-ranges 文件 物理页进程私有匿名页 内核 CoWfork 一样的语义所以 clone(n10) 之后磁盘几乎没增长——10 份 rootfs 共享同一批 XFS 物理块内存镜像也共享同一批 extent。任意副本写入时由内核负责分裂互不干扰。4.4 揭秘并发 clone 的 fail-safe 语义clones src.clone(n10, concurrency5)参数 concurrencyC 会把第一步快照与最后一步删除快照仍只各做一次只有中间的 N 次基于快照创建沙箱被并行化。全有或全无契约任一子任务失败时已成功创建的克隆会被自动销毁临时快照会被删除。调用方拿到的要么是 N 个沙箱要么是异常不留孤儿资源。4.5 揭秘源沙箱的连续性派生临时快照时VM 内部走的是 pause - snapshot - resume 三步整个 pause 时长通常不到 100 毫秒。返回后源沙箱继续以原 PID、原内存映射运行——这正是连续性的来源。◆最终章把三个机制串起来 —— Cubelet 的三层降级策略◆每一次提交快照时节点端会决定两件事内存模式选哪种 reflink 基础卷指向哪一份历史。三层降级保证可用性soft-dirty → pagemap_anon → full 的自动降级链确保任何异常基础快照被删除、快照断链、内核不支持 soft-dirty都不会让用户操作失败而是静默升级为正确但略大的快照。◆写在最后◆如果你正在构建需要代码执行、工具调用或多 Agent 协作的系统欢迎了解和试用 Cube Sandbox。如果觉得有帮助欢迎点个 ⭐ Star也欢迎提 Issue、PR 一起共建。你的每一个反馈都是项目持续演进的动力。Cube Sandbox 项目地址https://github.com/TencentCloud/CubeSandbox