本文转载自 The New Stack。
本文是系列博文(共两篇)的第二篇。点击此处,阅读第一篇。
NGINX 对 WebAssembly(Wasm)能够为社区带来的可能性充满期待,尤其是在可扩展性方面。我们开发了多种能够充分利用模块化和插件的产品,包括 NGINX 开源版和 NGINX Plus。同时也包括开源 NGINX Agent,它是一个辅助守护进程,可实现对 NGINX 配置的远程管理,并可收集和报告实时 NGINX 性能与操作系统指标。
NGINX Agent 在设计时充分考虑了模块化,而且是用一种流行且对 Wasm 友好的常用语言 Go 编写而成,它还使用发布-订阅事件系统向合作插件推送消息。不过,其当前发展阶段,插件创建仅限于 Go 语言和静态链接。
鉴于 NGINX Agent 拥有强大而灵活的架构,我们开始思考如何用外部插件模型来改善开发人员体验(注意:不是作为路线图项目,而是评估在生产级系统中使用 Wasm 的功效)。
我们可以有很多选择,既可直接使用正在开发中的众多运行引擎之一,构建一些定制工具和绑定插件,也可以采用社区中正在蓬勃发展的某个插件软件开发套件(SDK)。Extism 和 waPC 这两个SDK 就是浏览器之外围绕 Wasm 不断发展的生态系统的典型范例。
Extism 和 waPC 项目采用互补但不同的方法将 Wasm 嵌入到应用中。它们提供了服务器端 SDK,以简化运行时接口、加载并执行 Wasm 二进制文件、进行生命周期管理和服务器函数导出,同时还扩展了程序员可用的语言集。
另一个项目 Wasmtime 为从 Rust、C、Python、.NET、Go、BASH 和 Ruby 中使用 Wasm 提供了应用程序接口。Extism 不仅在此基础上增加了 OCaml、Node、Erlang/Elixir、Haskell 和 Zig,而且还提供了大量客户端 API,即插件开发套件(PDK)。waPC 项目采用了类似的方法,提供服务器端和客户端 SDK,以简化与底层运行时引擎的交互。
不过,Extism 和 waPC 之间仍有一些显著差异。基本的对比表如下:
Extism | waPC |
辅助 API(如内存分配、函数使用) | 客户端 API 较少(无法访问内存) |
直接运行时调用 | 抽象运行时调用、间接服务器和客户端 API |
单一运行时引擎 | 多个运行时引擎 |
主机函数导出 | 主机函数导出 |
复杂的路由输入和输出系统 | 简化的输入和语言原生函数输出 |
大量的服务器语言支持 | 有限的服务器语言支持(Rust、Go、JavaScript) |
大量的客户端语言支持 | 有限的客户端语言支持(Rust、Go、AssemblyScript、Zig) |
需要 C 命名空间代码 | C 命名空间和绑定隐藏在抽象之后 |
早期、正式上市前开发版本 | 早期、正式上市前开发版本 |
活跃 | 活跃 |
较小的支持群体 | 由具有较大潜在支持的 dapr 使用 |
可通过支持的 API 配置状态 | 持久状态必须通过自定义初始化阶段进行传递 |
基本哈希验证 | 无字节码自定义验证 |
支持主机调用用户数据 | 不支持主机调用用户数据 |
根据您的使用情况,Extism 或 waPC 可能更适合你:
- Extism 仅支持一个运行时引擎 — Wasmtime;waPC 可支持多个运行时引擎,并具有更高的可配置性。
- Extism 允许从服务器端和客户端直接调用导出的符号。waPC 项目通过导出特定的调用符号并在查找表中跟踪用户注册的函数,在服务器端和客户端之间构建抽象。
- Extism 将数据序列化完全交给了用户。waPC 项目集成了接口定义语言(IDL),可自动完成部分序列化或反序列化工作。
我们通过这两个项目扩展了 NGINX Agent,并使用 Wasmtime 作为专用引擎来确保简单性。确定了候选的软件开发工具包(SDK)和运行时(runtime)后,引入外部插件机制通常是一个简单直接的过程。
我们扩展 NGINX Agent 的过程分为以下几个阶段:
- 扩展了 NGINX Agent 配置语义,以定义外部插件及其字节码源。
- 创建了一个适配器抽象作为具体的 Go 结构,以便将 Go 函数调用与 Wasm 对应的函数调用连接起来。
- 将客户端 API(Guest)定义为预期的客户端函数导出。
- 将服务器 API(Host)定义为预期的服务器端函数导出。
- 为 Host 和 Guest 调用定义了数据语义。(Wasm 的类型系统严格但有限,其内存模型是未解释字节的连续数组,因此传递复杂数据需要使用接口定义以及序列化和反序列化实用程序。)
- 最后,我们通过初始化运行时、注册预期的服务器 API 导出、以字节码形式加载示例插件、验证预期的客户端 API 并运行基本不变的 NGINX Agent 核心代码,将一切连接起来。
下图显示了使用 Extism 的插件组件的简化数据流。它与 waPC 略有不同,因为 waPC 在主机和客户机系统之间引入了自己的抽象。尽管如此,我们还是可以得出同样的结论:在新系统或现有系统中添加外部插件系统确实会带来一些开销和复杂性,但我们的插件也能从开发人员的选择和可移植性中获益匪浅。相比网络延迟、微服务复杂性、分布式竞赛条件、更大的攻击面以及保护线路和端点数据的需求相比,这种取舍是合理的。
在这个简化视图中,您可以看到我们在 NGINX Agent 核心可执行文件与 Wasm “客户机”(或客户端)代码之间的分流。我们使用 “Go Runtime” 作为 NGINX Agent 系统和可执行文件的简称。NGINX Agent 已经支持插件,并提供了 “插件接口”。然后,我们构建了一个小型缓冲结构,用于在 Go 原生调用和相应的 SDK 调用(如对插件的调用)之间进行分流,例如调用 Plugin。进程简单的生成了对 Extism.Plugin.Call
(进程)的调用。在客户端插件执行之前,SDK(适用于 Extism 和 waPC)完成有关内存、Wasmtime 集成和函数调用的其余工作。如图所示,插件还可通过 Wasm 导出回调“主机”,在这种情况下,插件也可以发布新消息和事件。
Wasm 作为插件架构的通用后端控制和配置平面
Wasm 环境和生态系统正快速发展。在浏览器之外使用 Wasm 已不仅仅是科幻小说–它已成为现实,越来越多的运行时引擎、SDK、实用程序、工具及文档可供开发人员选择使用。未来将很快实现进一步的改进。 Wasm 社区正积极开发组件模型,以及 WIT 等规范和 wit-bindgen 等代码生成工具,这些工具定义了互操作 Wasm 组件、服务器及客户端 API。标准化接口也会无处不在,就像编写 protobuf 文件一样方便高效。
毫无疑问,未来还会有更多的挑战。举例来说:高阶语言阻抗,例如“服务器端 Go 上下文对 Haskell 源客户端字节码意味着什么呢?”即便如此,我们将 Wasm 嵌入现有项目中的有限尝试还是非常令人振奋,并具有启发性。我们计划进行深入探究,因为 Wasm 显然将在未来应用运行中发挥着重要作用。
从理论上讲,许多其他具有插件架构的应用程序也可以从类似的 Wasm 堆栈中受益。我们将继续探索在开源 NGINX 项目中使用 Wasm 的更多方法。就服务器端而言,这是一个全新的 Wasm 环境,而我们才刚刚开始窥见其中的无限可能。随着 Wasm 工具链日趋成熟和兼容性问题得以解决,Wasm 的未来不可限量,它不仅可以提高应用性能,而且还能够提升开发人员的体验。
高性能负载均衡的进阶使用指南