我们通过删除文件系统使其速度提升了47倍

Hacker News Top 产品

摘要

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/)。

相似文章

交换表、闪存友好的交换、swap_ops 等

Hacker News Top

本文介绍了 Linux 内核交换子系统的最新改进和未来计划,包括减少每页开销、基于 folio 的辅助函数,以及使交换更适配固态存储的努力。相关内容在 2026 年 Linux 存储、文件系统、内存管理与 BPF 峰会上进行了讨论。

从本地存储引擎中移除 fsync

Hacker News Top

FractalBits 推出了一种专为单节点设计的 KV 存储引擎,通过在硬件层级直接管理数据持久性来消除 fsync 调用,从而在 NVMe SSD 上实现显著提升的写入吞吐量。

帮助数据中心以更少的硬件实现更高性能

MIT News — Artificial Intelligence

麻省理工学院的研究人员开发了Sandook,这是一种基于软件的系统,通过同时解决SSD的三个变异性来源,将数据中心存储性能提升近一倍,效率远超传统方法。