本文将探讨在基于WASM的牧云插件系统开发中WASM运行时的选择。我们将对比不同WASM运行时在性能、安全性、易用性等方面的优缺点,并解释我们为什么以及最终选择了哪种WASM运行时。
这是系列文章“主机Agent插件引擎开发故事”的第八篇,后续将会持续更新。该系列文章将带领您深入探究长亭牧云团队主机Agent插件引擎的开发历程,内容涵盖技术选型、插件接口设计、组件通信框架等多个方面,并详细讲解背后的原理和实现方式,无论您是网络安全专业人员还是对技术开发感兴趣的读者,都可以从中得到收获。我们希望通过分享在开发过程中面临的挑战、解决方案以及实践经验,提供深入见解和有价值的技术参考,帮助读者了解如何构建高效可靠的安全产品,共同推动安全技术社区的发展。
在前面的文章中我们逐渐确定了插件系统的技术选型、探针的技术选型、插件的技术选型、通信框架的设计、序列化方法的选择等。接下来距离真正跑起来还差最后一步,即 WebAssembly 运行时的选择。
在本文中,我们将详细讨论 WebAssembly 运行时相关的问题,首先说清楚一个 WebAssembly 运行时需要什么完成什么样的工作,然后就相关工作作为一个主机安全的 Agent 引擎我们有什么样的期望做一个大致的探讨,最后逐个分析社区中的流行选择。
从最简单的角度来说,一个 WebAssembly 运行时其实就是充当了一个操作系统的角色,负责加载并执行一个 WebAssembly 字节码构成的程序,并管理程序执行过程中涉及的文件、网络、内存等资源。
展开来讲,加载的过程涉及到导入导出符号的解析和映射,需要一个加载器;执行的过程涉及解释或编译执行的步骤,需要一个执行器。执行过程中需要申请内存,因此需要一个内存管理器。执行过程中还需要访问文件和网络等,因此需要一个桥接器帮助提供和隔离相关资源的访问。多个 WebAssembly 程序执行的过程中,需要管理何时执行哪个程序,因此需要一个调度器。
在《创新之路》那篇文章中,我们提出了 “分离安全业务能力和安全基础能力” 的原则来指导解决主机 Agent 引擎中的灵活性与健壮性的平衡问题。在这里,这条原则将再次发挥作用。
回顾之前的文章,利用这条原则做出的第一个重大抉择就是我们需要一个插件系统。而这个系统中将会由 Agent 程序提供安全基础能力,由插件提供安全业务能力。而这里插件是在 WebAssembly 运行时中执行的,所以我们对运行时的要求是可以满足对插件的要求,即:实时、轻量、安全、兼容、稳定、跨平台、灵活、易用。虽然 WebAssembly 本身已经为我们提供了这里的大部分能力,但在高效轻量稳定的基础上我们肯定还是想要选一个最高效轻量稳定的。
社区当前(2023.6)开发最为火热且持续的项目包括:Wasmtime、Wasmer、WasmEdge、wasm3、WAMR、Deno、Bun、Wasm2c。
以上运行时我们将其分为四类分别讨论:解释执行、基于 LLVM/Cranelift/V8、基线编译器、其他。
wasm3
是其中唯一的一个解释器,其显著特点是广泛兼容何种指令集和许多编程语言,通过了 WebAssembly spec testsuite,承诺了很小的可执行文件体积和内存运行要求。在 wasm3 的主页上解释了为什么选择解释执行而非其他更快的方式,而答案是“在很多情况下,速度不是主要关心的事情,而可执行文件体积、内存占用、启动延迟等可以用解释执行的方式更好地解决”。好吧,虽然我们也很关心内存占用等问题,但速度真的对我们来说很重要,其他资源并不像其举的例子中使用 MCU 的嵌入式设备那样紧张。
这三类一起讨论是因为其效率表现和兼容性表现都近似。
Wasmtime
/ WasmEdge
/ Wasmer
是其中最受瞩目的三个运行时,均为通用运行时,但各有不同的侧重点。wasmtime 的重点是高效(fast)安全(secure);WasmEdge 重在为云原生和无服务器带来边缘计算(Edge Computing)支持;而 Wasmer 意在提供一种通用(Universal)的运行时。
Wasmtime 是字节码联盟(Bytecode Alliance)的项目,而字节码联盟是一个以提供“细粒度沙箱、基于功能的安全性、模块化和标准化的最先进的基础设施”为目标的非盈利组织,愿景是“为所有平台提供默认安全的WebAssembly生态系统”。
从 wasmtime 项目的发展进程来看,其首先花费了大量的时间在标准化和许多提案的公开探讨上,并构建了从低级到高级的各个层次的工具和项目,当子项目逐渐成熟后便会集成到主仓库。
从 wasmtime 的运作过程来看,其花费了大量精力提供供应链安全信息、模糊测试和漏洞公开披露上。wasmtime 关联了许多 CVE 漏洞,其中有一些是非常严重的问题,甚至可能导致隐私泄露或任意代码执行,但这并不是一件坏事。事实上,wasmtime 是迄今为止唯一一个公开披露漏洞的 WebAssembly 运行时。这些动作体现了字节码联盟对安全的重视,同时也赋予了我们对 wasmtime 安全性的信任。
在最初的时候,wasmtime 项目包含了大量的 C++ 代码,因此对其安全性是有些隐忧的,但很快随着 Rust 语言的发展逐渐从采用 Nightly 版本替换 C++ 代码逐渐过渡到了完全使用 Stable 版本的 Rust 1.69 编译。从大约 4.0 的版本开始,其步入了一种积极的、稳定的开发节奏,同时大量经过长时间讨论的提案进入了接近成熟的状态。
如果是为了在浏览器环境外安全运行插件代码,Wasmtime 感觉是最合适的选择。
Wasmer 由一家独立的初创公司控制,于 2018 年 10 月创立。
当时 wasmtime 项目进入了一段较为沉默的时期,于是 Wasmer 的创始人们觉得是时候接过大梁了。最初两伙人是共同发展 wasmtime 的,后来看 wasmtime 变慢了就建议一起转到 Wasmer 上但是对方没同意(毕竟愿景不同),于是自立门户并赋予了不同的目标。
两者最主要的不同是 Wasmer 重点在于为尽可能多的 CPU 架构和客户端环境以及宿主语言环境提供尽可能丰富的生态支持,而 wasmtime 的目标还是原来的 fast and secure state-of-the-art foundations
。因此,在技术选择上,Wasmer 主推使用 LLVM 作为编译的后端,而 wasmtime 则专注于发展自己的 cranelift 编译器。
cranelift 与 LLVM 在某种程度上是非常相似的,都意在提供一种语言中立和平台中立的 IR,区别在于 cranelift 实际上只提供 WASM 字节码到机器码的生成,而不同语言到 WASM 字节码的生成则由各个语言自行使用自己的方式解决。这为实现一些 WebAssembly 的针对性优化提供了机会。
有趣的是,尽管 wasmer 分家独立发展的势头非常坚定,而且把比 wasmtime 速度更快,具有更广泛的支持等作为宣传的噱头,然而 wasmer 实际上仍然在积极采取 wasmtime 团队发明的 cranelift 和 witx 等工具。
经过不断的针对性优化,当前 cranelift 生成的质量和效率表现在 WebAssembly 这个领域上已经与 LLVM 相差无几了(甚至更快,虽然 Wasmer 仍然声称自己至少快 50%),相信在专用领域这种自底向上的打通优化最终会有更加值得期待的未来。
而 witx
和配套的项目 witx-bindgen
在演进中逐渐抛弃了旧的实验性格式的支持,而转而提供了标准化的新的更好的 wit 格式和新版本的 wit-bindgen
。Wasmer 由于过早采用了 witx
又无法得到来自 wit-bindgen
的持续支持,不得不分叉出自己的 wai 格式并维护自己的 bindgen 工具。
总的来说,Wasmer 实实在在地以自己的努力扩大了 WebAssembly 社区的使用范围和影响力,对社区整体毫无疑问是十分有益的,但多少还是有些太心急了。
WasmEdge 是云原生计算基金会(CNCF,Cloud Native Computing Foundation)的项目,自然从诞生起就是为云原生场景服务的。
而在云原生场景中,WebAssembly 主要的场景是 Sidecar,Serverless 和 Edge Computing,而在 Serverless 领域中基于 WebAssembly 的容器则又是最为火热的。前一段时间 Docker 支持运行基于 WasmEdge 运行时的 WebAssembly 容器的新闻着实让相关领域的从业人员兴奋了一把。而在 Docker Desktop 的第二个相关的技术预览版中则一口气又加了三个其他的运行时,其中就包括 Wasmtime。
WasmEdge 的显著优势包括提供了网络能力以及 wasi-nn
和 wasi-crypto
的抢先支持,十分适合其定位。
WAMR 即 WebAssembly Micro Runtime
,编译出的可执行文件名为 iWasm
。这是 W3C 的 Wasm 标准的一个最小可行化产品(MVP)。其具有最小的体积、最快的速度、最成熟的支持,但是很差的生态。从易用和持续维护的角度来看,这不会是我们希望选择的项目。
Deno 和 Node 都使用了 V8 引擎,这使得其直接继承了 Chrome 浏览器对 WebAssembly 的成熟支持能力,并具有首屈一指的 WASI 能力和目前独一份的 WASM GC 实现支持。然而,如果要集成 V8 引擎就必须集成整个 V8 引擎,这是一个非常巨大而复杂的依赖。如果同时还需要 JavaScript 的支持,这是完全合理的,但对于只需要 WebAssembly 的情况来说实在是过于沉重了。
Bun 是一个新兴的 JavaScript 执行引擎,基于 JavaScriptCore(Safari 的 JS 引擎),主要使用 Zig 语言实现,号称比 Deno 更快,而且可以通过 wasmer-js 完美模拟 WebAssembly 的 WASI 特性。但不幸的是 JavaScriptCore 在 WebAssembly 的支持上并不像对 JavaScript 的支持那么出色,尤其是在 wasmer-js 的兼容层上性能表现也非常差,因此不会是当前理想的选择。
wasm2c 是一个 WASM 到 C 的转译器,在这些运行时中绝对是个另类。其运行原理是将 *.wasm
文件翻译成 *.c
文件,然后利用 zig 语言提供的构建工具链将其编译出来。也因此,其运行速度毫无疑问是最快的,但同时也是最不安全的。
回顾上述引擎的背景和特点可以发现,尽管 WebAssembly 运行时看起来有许多选择,然而各自都有非常鲜明的特征,并不适合所有的场景。
我们不想放弃速度,不想放弃安全,不想放弃易用,不想要沉重的负担,更重要的是希望迎合未来的发展。如果再考虑到逐渐成熟的 WebAssembly Interface Type 和 WebAssembly GC 的支持,我想值得纠结的主要是选择 Deno 还是 Wasmtime。Deno 没有 Interface Type 因此写插件 binding 会是一个繁琐的任务,Wasmtime 暂时还没有 GC 无法高效执行更加易用的语言。然而可以预期 Wasmtime 将会在今年内提供 GC 标准的实现,却无法预期任何其他运行时增加对 Interface Type 的支持。因此,只有 Wasmtime 作为我们权衡之下的最佳选择。
上一篇:1. 【牧云插件系统技术选型之自己人都会吐槽的序列化方法选择之争】
系列文章目录:【预告】主机Agent插件引擎开发故事汇总