我们通过删除文件系统使其速度提升了47倍
摘要
microsandbox将其缓慢的用户空间FUSE文件系统替换为内核挂载的EROFS磁盘映像,在文件系统操作上实现了几何平均47倍的速度提升,并消除了虚拟机/主机往返瓶颈。
暂无内容
查看缓存全文
缓存时间: 2026/05/23 21:34
# 我们通过删除文件系统使其快了47倍
来源:https://microsandbox.dev/blog/oci-filesystem-47x-faster
一位用户在 Discord 上说 microsandbox 感觉很慢。在沙箱内列出 Python 标准库中的所有文件花了 5.3 秒;而在 Docker 中只需几毫秒。我们开始了调查(https://github.com/superradcompany/microsandbox/issues/499)。
我们在 v0.4 中修复了这个问题:我们将用户空间文件系统替换为虚拟机直接挂载的 Linux 磁盘镜像。在混合的客户可见文件系统基准测试套件中,几何平均加速比为 47 倍,最慢的行提升了超过 1000 倍,并且主机文件系统代码减少了约 5300 行。
我的第一次尝试是 monofs(https://github.com/superradcompany/monofs):一个内容寻址文件系统,具有块级去重、压缩和分布式读取副本。它在磁盘上的镜像存储大小是原始大小的 1.3 倍,而 microsandbox 是本地优先的,因此长尾去重的收益不值得前期成本。对于 v0.3,我切换到 OCI 加上构建在 libkrun 钩子上的用户空间覆盖层;我们获得了层去重,并且在 Linux 和 macOS 上行为一致,但所有操作仍然在内核之外运行。
虚拟机内的每个文件操作都必须通过 FUSE 跳出到主机,FUSE 是 Linux 允许普通程序充当文件系统的机制。要打开一个文件,虚拟机将请求交给我们的主机进程,该进程遍历每一层查找文件并将答案发送回来;每次 `stat`、每次 `readdir` 和每次缓存未命中都会发生同样的往返。一个简单的 Python `import` 在代码开始运行之前就会触发几十次这样的往返,而一个十层的镜像会使每次代价倍增。
我们在 v0.3 的剩余时间里试图加速这条路径:更好的缓存、更少的系统调用、更小的响应。每次更改都只提升几个百分点。没有一个能改变数量级。
Docker 没有这个问题,因为 Docker 使用内核自己的分层文件系统驱动程序(overlayfs),所以文件操作从不离开内核。我们试图从内核外部匹配一个内核文件系统;没有缓存能弥合这一差距。
所以我们删除了文件系统。
新计划是停止在虚拟机和主机之间来回传递每个文件操作。我们将提前构建一个 Linux 文件系统镜像,将其作为虚拟磁盘交给虚拟机,让虚拟机自己的内核挂载它。随着 FUSE 退出路径,虚拟机内的文件操作将留在虚拟机内部。
**之前**
应用
客户 VFS
virtiofs / FUSE 边界
主机文件系统代码
层查找 / 覆盖逻辑
响应回到虚拟机
**之后**
应用
客户 VFS
客户 overlayfs
客户 EROFS
virtio-blk
缓存块支持的镜像
之前,每次查找都跨越虚拟机/主机边界。之后,正常的读取和查找留在客户内核内。
我们选择的文件系统是 EROFS(https://docs.kernel.org/filesystems/erofs.html):只读,自内核为 Android 需要它以来就在树中,并且易于创建。EROFS 也解决了 macOS 问题:虚拟机自己的内核是 Linux,无论外部运行什么,因此一旦磁盘镜像构建完成,主机的文件系统就不再重要。
microsandbox 在 Linux 和 macOS 上运行,而 macOS 缺少通常用于构建文件系统镜像的主机端工具:没有 `mkfs.ext4`,没有 `mkfs.erofs`,没有 loopback 挂载。如果我们的镜像流水线依赖其中任何一个,我们将不得不要么附带一个帮助虚拟机(重,启动慢),要么忍受平台之间的永久分裂,而这两种选择都不符合 microsandbox 的“单一自包含二进制”的承诺。因此我们自己用 Rust 编写了镜像写入器。文件系统是磁盘上的字节布局;写入器只需生成该布局。三个小组件完成工作:
- 一个 **EROFS 写入器**,生成 OCI 层的只读镜像。
- 一个 **ext4 写入器**,生成每个沙箱获得的稀疏、有日志的临时区域。
- 一个 **VMDK 描述符**,将所有内容拼接成单个虚拟磁盘。
流水线中没有任何内容调用外部命令、请求 root 权限或挂载 loopback 设备,并且相同的 Rust 代码路径在 Linux 和 Apple Silicon 上构建镜像,无需依赖仅主机的文件系统工具。EROFS 产物通过我们也编写的读取器进行往返验证,CI 在真实的虚拟机内核下启动完整栈。如果某个字节出错,两个不同的读取器会告诉我们。
使用这些写入器的明显方式是为每个 OCI 层创建一个 EROFS 镜像(https://github.com/superradcompany/microsandbox/pull/548)。虚拟机会获得每个层一个虚拟磁盘,外加一个用于临时区域的磁盘,然后内核的 overlayfs 在启动时合并它们。它奏效了:第一批测量结果显示,根据工作负载,比 v0.3 快 10 倍到 175 倍,我们准备发布。
**第一版**
层 1
/dev/vda
层 2
/dev/vdb
层 3
/dev/vdc
...
...
层 30
/dev/vd?
每个 OCI 层一个 EROFS 镜像。Python 镜像附加了大约 10 个磁盘;一些自定义构建超过了 microVM 的 virtio 设备上限。
然后我们数了层数。一个 Python 镜像大约十层;CUDA 镜像更多;一些用户构建的镜像达到三十或四十层。microVM 对它们能携带的设备数量有限制,而我们为每层附加一个磁盘。我们提高了上限(https://github.com/superradcompany/libkrun/pull/46),但真正的修复是停止使用虚拟磁盘告诉虚拟机“这个镜像有层”,而让文件系统本身携带这些信息。
EROFS 团队(https://github.com/superradcompany/libkrun/pull/46#issuecomment-4241050440)向我们指出了我们未曾使用的功能:EROFS 可以构建一个仅元数据的镜像,只包含合并的目录树以及每个文件指向其底层 blob 及其偏移的指针。内核读取该镜像,将整个包作为单个虚拟磁盘处理,并通过一次计算而不是跨层搜索来回答每个查找。
流水线变为:
- 像往常一样拉取 OCI 层。
- 构建一个描述合并树的小型元数据镜像。
- 将元数据和层 blob 拼接在一起的单个虚拟磁盘交给虚拟机。
现在无论原始镜像有多少层,虚拟机只需附加两个根文件系统块设备:一个只读的 VMDK 支持的堆栈用于镜像(内部引用合并元数据镜像加上每个层的 EROFS 区域),以及一个可写的 ext4 上层用于沙箱。Overlayfs 只合并这两者。这是我们发布的版本(https://github.com/superradcompany/microsandbox/pull/576),带有一个小的 libkrunfw 内核配置调整(https://github.com/superradcompany/libkrunfw/pull/4)(`CONFIG_EROFS_FS_XATTR` + `CONFIG_EROFS_FS_SECURITY`),以便 EROFS 暴露 overlayfs 用于 whiteout 所需的 xattr。
在拉取时,主机将每个 OCI 层物化成由其 diff ID 键控的 EROFS 产物,合并层元数据与来源,写入 `fsmeta.erofs`,并发出一个覆盖 `fsmeta.erofs` 和层区域的 VMDK 描述符。在沙箱创建时,microsandbox 为该沙箱创建一个稀疏的 `upper.ext4`。在启动时,客户看到 `/dev/vda` 作为只读下层堆栈,`/dev/vdb` 作为可写上层,Linux overlayfs 组装出 `/`。
**镜像堆栈**
OCI 层
每层 EROFS 产物
合并元数据 + 来源
fsmeta.erofs
覆盖 fsmeta + 层区域的 VMDK 描述符
/dev/vda(只读下层)
**沙箱上层**
稀疏 upper.ext4
/dev/vdb(可写上层/工作)
客户 overlayfs
/
无论原始镜像声明了多少层,每个沙箱引导只需两个块设备。主机在拉取时一次性支付层遍历成本;客户在运行时无需为此付出任何代价。
我们在同一个 `python` 镜像上分别对两个版本运行了三次基准测试,运行之间使用全新状态。在十四个混合的客户可见文件系统工作负载中,几何平均加速比为 **47.18 倍**,八个最大变动如下所示。
这些柱状图分为两组:
- **根文件系统路径**:新 OCI 路径的最纯粹度量;这些操作现在留在客户内核内,而不是通过主机来回传递。
- **`/tmp` tmpfs**:真实的客户可见提升,但来自消除客户 tmpfs 工作负载上的 FUSE 往返,而不是来自新的 EROFS 下层根文件系统路径。
1×10×100×1,000×
file_delete_1k
1109.94
×
rename_1k
876.58
×
small_file_create_1k
240.78
×
metadata_scan_stdlib
240.28
×
read_all_py_stdlib
116.40
×
deep_tree_traverse
47.16
×
concurrent_read_4t
20.93
×
random_read_stdlib
4.01
×
根文件系统路径
/tmp tmpfs
对数尺度。v0.3.14 基线 = 1×。更高更好。`metadata_scan_stdlib` 扫描 Python 标准库中每个文件的元数据。之前需要半秒。现在大约 2 毫秒。
Linux 的 overlayfs 是一个庞大的规范,涵盖 whiteout、不透明目录、跨 copy-up 的硬链接、目录重命名以及必须完全正确行为的一系列 xattr 约定。我们的 v0.3 在用户空间重新实现了大部分内容,并且在我们删除它的那天我们仍在追逐边缘情况。v0.4 没有重新实现任何部分,因为虚拟机自己的内核执行合并,而我们过去拥有的错误并没有被修复;它们消失了。
主机仍然需要理解 OCI 层语义,但仅在拉取时一次。Whiteout、不透明目录、硬链接、xattr 和大小写敏感路径在 `fsmeta.erofs` 写入之前被规范化到合并的元数据树中。之后,运行时路径是普通的内核 EROFS 加上 overlayfs。
macOS 的 APFS 默认大小写不敏感。许多 Linux 镜像包含仅大小写不同的文件名,在 Mac 上提取时第二个文件会被合并到第一个中。v0.4 从不提取到主机文件系统;EROFS 写入器将 tar 直接流式传输到二进制镜像中,两个文件名作为磁盘上的不同条目存在。
由于根文件系统现在是真正的磁盘镜像,围绕的产品面变得更便宜。
- **OCI 补丁。**用户希望在镜像之上应用的根文件系统补丁在启动前被烘焙到 `upper.ext4` 中,而不是通过运行时覆盖协议外挂。
- **共享下层。**每层 EROFS 产物通过 diff ID 进行内容寻址,因此共享基础镜像的两个沙箱共享磁盘和缓存中的这些字节。
- **快照。**沙箱的可写状态是单个 ext4 文件;保留或复制它就是一个文件复制。
- **磁盘镜像根。**自定义的非 OCI 磁盘镜像根文件系统重用相同的块设备启动机制,减去前面的 fsmerge 步骤。
**仅 OCI 根文件系统。**绑定卷(您共享到虚拟机的主机目录)仍然走旧路径。它们的内容在虚拟机读取时可能随时更改,而只读磁盘镜像无法表示这一点。
**首次拉取并没有更快。**我们现在在拉取时做了更多工作来构建镜像,尽管它在层之间是并行的,并且受限于 tar 解压缩,因此与之前大致相当。随后的沙箱*创建*更快,因为我们只发出稀疏的临时镜像。
**对镜像的写入仍然通过 overlayfs 进行写时复制。**修改镜像中的文件会将其复制到可写上层,与任何覆盖配置完全相同。这里的根文件系统优势在于查找和读取密集的路径;图表中的 `/tmp` 生命周期提升来自 `/tmp` 默认是客户端的 tmpfs,这是一个单独的运行时决策。
**内核中朴素的基元通常胜过用户空间中巧妙的基元。**monofs 和我们的 v0.3 覆盖层都是雄心勃勃的设计,但 EROFS 是一个朴素的内核内部文件格式,对于沙箱根文件系统,朴素的那个赢了。我们花了几个月调整用户空间代码,然后才接受结构性的答案是停止与内核竞争并直接使用它。
**当现有东西破坏你的设计时,重新发明轮子没问题。**调用 `mkfs.ext4` 或 `mkfs.erofs` 将意味着要么需要一个帮助虚拟机,要么是 Linux 专属的分裂,这两者都会破坏 microsandbox 的“单一自包含二进制”承诺。自己编写写入器是保持这个承诺的代价,我们会再次做出同样的取舍。
**在发布的同时保持对更好想法的开放态度。**我们的第一版已经是一个巨大胜利,我们曾想直接发布它。EROFS 建议的更干净形状在当时看起来像是锦上添花,但将 PR 再保持一周以吸收它,将一个一次性的优化变成了我们乐于长期支持的东西。
**在虚拟机内部运行基准测试。**从主机计时会隐藏最坏的 FUSE 往返成本,并使胜利看起来比实际小。计时你的用户真正等待的东西。
此功能随 microsandbox 0.4 及更高版本发布。安装 CLI:
```bash
curl -sSL https://install.microsandbox.dev | sh
```
或者使用对应语言的 SDK:
```bash
uv add microsandbox # python
npm install microsandbox # typescript
cargo add microsandbox # rust
```
基准测试存放在它们自己的仓库中,以便成长为跨运行时的比较。在 `PATH` 上有 `msb` 并且有全新的 `~/.microsandbox` 时:
```bash
git clone https://github.com/superradcompany/sandbox-bench
cd sandbox-bench/benches/fs
just bench-quick
```
需要 `just` (https://github.com/casey/just) 和 `uv` (https://docs.astral.sh/uv/)。
相似文章
@rauchg: 这太强大了。▲ Sandbox 现在可以无限制地运行 Docker 和 FUSE。MicroVM 是不断给予的礼物…
Vercel 的 Sandbox 现在支持无限制地运行 Docker 和 FUSE 文件系统,使开发者能够挂载 S3 存储桶和网络文件系统,并在沙盒之间共享状态。此更新基于 MicroVM 构建,实现即时启动和无约束运行时。
交换表、闪存友好的交换、swap_ops 等
本文介绍了 Linux 内核交换子系统的最新改进和未来计划,包括减少每页开销、基于 folio 的辅助函数,以及使交换更适配固态存储的努力。相关内容在 2026 年 Linux 存储、文件系统、内存管理与 BPF 峰会上进行了讨论。
从本地存储引擎中移除 fsync
FractalBits 推出了一种专为单节点设计的 KV 存储引擎,通过在硬件层级直接管理数据持久性来消除 fsync 调用,从而在 NVMe SSD 上实现显著提升的写入吞吐量。
@nash_su: 昨晚还在跟 @i5ting 聊 Sandbox 是 agent 不可或缺的重要组成,今天就看到腾讯开源的这个 Cube Sandbox 1. 极速启动 (< 60ms) — 得益于快照克隆和资源池预热 2. 硬件级安全 — KVM 微虚拟…
Tencent open-sources Cube Sandbox, a KVM-based micro-VM for agents that boots in <60 ms, runs in <5 MB RAM, and is E2B-compatible.
帮助数据中心以更少的硬件实现更高性能
麻省理工学院的研究人员开发了Sandook,这是一种基于软件的系统,通过同时解决SSD的三个变异性来源,将数据中心存储性能提升近一倍,效率远超传统方法。