数据截至 (上游 commit d7bc4affdac9)
分层去重的写时复制块存储:rootfs 与内存 diff
30 秒导读: E2B 的每个沙箱都有一块根文件系统(rootfs)和一份内存快照。它们不是各存一整份大文件,而是被切成块,表达成一叠 diff 层:base 模板占底层,之后每次改动只记录变过的块。读某个偏移时,先按一张映射表定位到"这个块归哪个 build 层",本地没有就从远端只拉这一块。这套设计让"从快照秒级恢复沙箱"和"增量地攒出模板"共用同一套块存储引擎。
本章讲这套块存储引擎:块设备怎么抽象、写时复制怎么做、层怎么叠、块怎么去重、缺块怎么惰性拉、快照头是什么格式。至于内存缺页(UFFD)如何消费这些块,见 03-fast-resume-uffd.md;模板构建如何写出这些层只在本章点到为止,细节见生命周期章节。
1. 这是什么(零基础也能懂)
一句话定义: 把 rootfs 和内存快照当成一块虚拟磁盘,这块磁盘的内容不是一整份文件,而是若干"只记录改动"的层(diff)按内容叠加而成;运行时的写入落到一个独立的可写层,原层永不被改。
它解决什么问题? 想象你要同时跑上千个沙箱,每个都基于同一个几百 MB 的模板:
- 如果每个沙箱都复制一整份 rootfs + 内存,磁盘和网络会瞬间爆炸。
- 如果恢复一个沙箱要先把整份内存快照下载下来,"秒级恢复"就无从谈起。
这套块存储的答案是三件事叠在一起:
| 技巧 | 白话 | 好处 |
|---|---|---|
| 分层(layering) | 只存"和父层不一样的块" | 上千沙箱共享同一个 base,增量极小 |
| 去重(dedup) | 连和父层一样的块都不存 | diff 更小、恢复要拉的更少 |
| 写时复制(COW) | 运行时的写落到独立可写层 | 只读底层可被无数沙箱共享 |
| 惰性拉取(lazy fetch) | 只有真正被读到的块才从远端下载 | 恢复不必等整份快照到齐 |
一句话直觉: 像 Git。base 模板是初始 commit;每次 pause 沙箱产出一个"只含 diff 的 commit";恢复时你 checkout 出的那份磁盘,是把这一串 commit 叠起来算出来的,而且文件内容按需才从远端仓库拉。
它长什么样(概念代码,示意非源码):
// 恢复一个沙箱:并不下载整份 rootfs,只是搭一个"叠好的视图"
device := assembleFromHeader(header) // 按 Header.Mapping 把各 build 层叠成一块虚拟盘
overlay := block.NewOverlay(device, cache) // 再套一层可写缓存:写落到 cache,读缺块回落到 device
// 沙箱开始跑;它读到某个偏移时,才触发那一块的惰性拉取
本节不出现底层细节。记住一件事:磁盘 = 一叠 diff 层 + 一个可写层,块按需才真正存在。