WASM vs seccomp:为代码评测沙箱测量启动延迟
上周我们上线了 sandbox_exec——一个用 224 行 C 代码编写的程序,利用 seccomp-bpf 在 AWS Lambda 里隔离学生代码。当时的诚实回答是:「WASM 更干净,但 Python 生态系统还没准备好。」
这周我们精确地测量了「Python 生态系统还没准备好」在毫秒层面的代价。答案比预期的更加微妙。
测试环境
- 运行时:Wasmtime v42.0.1
- 平台:macOS arm64
- 方法:每个场景 50 次运行,5 次预热,取平均值
- 对比基准:包裹 Python 3.x 的 sandbox_exec
第一阶段:启动开销
第一个问题很简单:用简单代码时沙箱启动需要多久?
| 环境 | 均值 | P95 |
|---|---|---|
| WASM(JIT) | 9.79ms | 10.86ms |
| WASM(AOT 预编译) | 9.25ms | 10.14ms |
| Python(无沙箱) | 14.71ms | 15.29ms |
WASM 启动比 Python 本身还快。这是这里反直觉的结果——人们以为「VM = 慢」,但 Wasmtime 的启动比 CPython 解释器初始化更紧凑。
这 ~10ms 的分解:
0–2ms: fork() + exec(wasmtime)
2–7ms: Wasmtime 运行时初始化
├── 命令行解析
├── 配置加载
└── WASI 环境设置
7–9ms: WASM 模块处理
├── 文件读取
├── 验证(类型检查)
└── JIT 编译
9–10ms: 执行 + 清理大部分时间在 Wasmtime 自身的初始化,不在模块解析或 JIT。
第二阶段:模块大小有影响吗?
| 模块大小 | 耗时 | 差值 |
|---|---|---|
| ~100B | 9.58ms | 基线 |
| ~4KB | 9.68ms | +0.1ms |
| ~40KB | 10.97ms | +1.4ms |
模块大小扩大 400 倍,只多花 1.4ms。初始化开销主导了一切。
第三阶段:计算性能
这是 WASM JIT 优势变得明显的地方。
| 负载 | WASM | Python | 加速比 |
|---|---|---|---|
| fib(10) | 10.06ms | 15.12ms | 1.5x |
| fib(20) | 9.63ms | 16.80ms | 1.7x |
| fib(25) | 10.77ms | 25.94ms | 2.4x |
| fib(30) | 15.91ms | 128.97ms | 8.1x |
在 fib(30) 时,WASM 总时间约 16ms(10ms 启动 + 6ms 计算)。Python 需要 129ms。WASM 整体变得更快的交叉点大约在 fib(20-25)——大致是计算开销不再相对于启动可以忽略不计的位置。
对于一个评测算法题目的作业评分器来说,这个差距很重要。
第四阶段:I/O 开销
| 操作 | WASM | Python |
|---|---|---|
| 1× fd_write | 10.16ms | 15.15ms |
| 100× fd_write | 9.97ms | 15.23ms |
100 次写操作和 1 次花的时间一样。启动开销完全主导,WASI I/O 开销在运行时内可忽略不计。
第五阶段:内存分配
| 内存 | 耗时 |
|---|---|
| 64KB | 9.62ms |
| 1MB | 9.86ms |
| 4MB | 10.04ms |
| 16MB | 9.74ms |
WASM 使用懒分配。声明 16MB 内存在启动时几乎没有代价。
第六阶段:安全特性零开销
| 配置 | 耗时 | 备注 |
|---|---|---|
| 无限制 | 9.58ms | 基线 |
| +fuel(指令计数器) | 7.94ms | 略快 |
| +内存限制 | 7.76ms | 略快 |
| +目录预开放 | 10.50ms | +0.9ms |
| 全部限制 | 7.91ms | — |
添加 fuel 和内存限制比不加更快——可能是因为它们触发了优化过的执行路径。唯一可测量的开销是目录预开放(+0.9ms 用于文件系统能力设置)。
安全性在这里有负开销。 这很不寻常。
安全模型的差距
性能之外,安全对比非常鲜明:
| 维度 | sandbox_exec | WASM |
|---|---|---|
| 隔离级别 | 进程 | VM |
| 内存隔离 | 共享地址空间 | 线性内存(硬边界) |
| 系统调用控制 | seccomp 白名单 | 根本没有系统调用 |
| 文件系统 | 需要外部清理 | 能力门控 |
| 网络 | seccomp 阻断 | 默认不存在 |
WASM 不过滤系统调用——它没有系统调用。在 WASI 下运行的 WASM 模块无法调用 socket()、ptrace() 或 io_uring_setup(),不是因为我们屏蔽了它们,而是因为没有任何机制可以发起这些调用——它们从沙箱内部根本不存在。
这从根本上比 seccomp 白名单更强。用 seccomp,你是在说「屏蔽这 62 个系统调用」。用 WASM,你是在说「没有系统调用」。攻击面的差异是本质性的。
我们为何还没用 WASM
安全模型更好,CPU 密集型代码的计算性能更好,启动开销相当。
问题是 Python:
| Python WASM 运行时 | 大小 | C 扩展 | 结论 |
|---|---|---|---|
| MicroPython | 370KB | ❌ | 标准库有限 |
| RustPython | ~5MB | 部分 | 实现不完整 |
| Pyodide | ~15MB | ✅ | 仅限浏览器,启动 500ms+ |
作业评分器需要 numpy、scipy 和任意 C 扩展。Pyodide 支持这些,但需要浏览器 JavaScript 引擎——它不能在 Wasmtime 下运行。MicroPython 和 RustPython 不支持完整的科学 Python 栈。
这不是性能问题,而是生态系统问题。WASM Python 工具链在快速演进,但对于「运行任意学生 numpy 代码」这个需求,还没到位。
路线图
现在: sandbox_exec(seccomp + rlimit)
└── 完整 Python + C 扩展
└── ~1.5ms 沙箱 + ~15ms Python 启动
└── 62 个被阻断的系统调用
1–2 年: 非 Python 语言使用 WASM
└── JS、Rust、Go 学生 → 直接 WASM
└── 更好的安全性,可比的性能
2–3 年: 生态成熟后的 WASM Python
└── Component Model + WASI Preview 2
└── 混合方案:Python → sandbox_exec,其他 → WASM混合架构是最可能的最终形态:Python 用 seccomp(C 扩展支持不可或缺),其他一切用 WASM(生态已经成熟)。
数字总结
如果你在为类似场景评估 WASM:
- 启动:~10ms(与 Python 本身的启动相当或更快)
- JIT 计算:CPU 密集型代码比 CPython 快 2–8 倍
- 安全开销:零(安全特性免费,甚至是负代价)
- Python 兼容性:如果你需要 numpy/scipy/C 扩展,还不可行
- 其他所有语言:已经可行
10ms 的启动开销不是障碍,Python 生态系统才是。
基准测试由 Akashi(CTO)进行。所有测量:Wasmtime v42.0.1,macOS arm64,50 次运行平均值。
