WebAssembly 在 MOSN 中的实践 - 基础框架篇
本文将介绍 WebAssembly 技术在 MOSN 中的实践,首先介绍了当前 MOSN 在扩展隔离方面所面临的痛点,并对 Wasm 技术的相关背景知识进行介绍。随后描述了Wasm 扩展框架的整体架构,并介绍了我们在 Proxy-Wasm 社区规范中所做的贡献,最后描述了框架在性能、异常调试等方面的实践内容。
总体设计
上图为 MOSN Wasm 扩展框架的整体示意图。如图所示,对于 MOSN 的任意扩展点(Codec、NetworkFilter、StreamFilter 等),用户均能够通过 Wasm 扩展框架,以隔离沙箱的形式运行自定义的扩展代码。而 MOSN 与 Wasm 扩展代码之间的交互,则是通过 Proxy-Wasm 标准 ABI 来完成的。
隔离沙箱
当我们在讨论 Wasm 时,都明白 Wasm 能够提供一个安全隔离的沙箱环境,但并不是每个人都了解 Wasm 实现隔离沙箱的技术原理。这时又要搬出计算机科学中的至理名言: “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。Wasm 实际上也是通过引用一个“中间层”来实现的安全隔离。简单来说,Wasm 通过一个运行时 (Runtime) 来运行 Wasm 沙箱扩展,每个 Wasm 沙箱都有其独立的线性内存空间和一组导入/导出模块。
另一方面,Wasm 也规定了代码中任何可能产生外部影响的操作只能通过导入/导出模块来实现。当我们在编写 C 语言源码时,可以直接通过系统调用来访问系统的环境变量、文件、网络等资源。而在 Wasm 的世界中,并不存在系统调用相关的指令,任何对外部资源的访问必须通过导入模块来间接实现。以文件读写为例,在 Wasm 中要想进行文件读写,需要宿主机提供实现文件读写功能的导入函数,Wasm 代码调用该导入函数,由宿主机间接进行文件读写,再将操作结果返回给 Wasm 扩展。在上述过程中,实际的文件读写操作由宿主机完成,宿主机对这一过程有绝对的控制权,包括但不限于只允许读写指定文件、限制读写内容、完全禁止读写等。
扩展框架
MOSN 以 插件(Plugin) 的形式对 Wasm 扩展进行统一管理,插件是指一组 Wasm 沙箱实例及其配置的集合。用户通过配置来加载、更新以及卸载 Wasm 插件,并通过配置来描述沙箱实例的运行规格(使用的执行引擎、Wasm 文件路径、实例数量等)。下面展示了一个典型的 Wasm 插件配置:
当 MOSN 加载上述插件配置时,会按照以下流程生成插件对应的 Wasm 沙箱实例:
如下图所示,Wasm 扩展框架主要分为 Manager、VM 和 ABI 三个子模块。其中
Manager 模块负责对 Wasm 插件的配置进行统一管理,提供插件的增删查改功能,并负责将用户提供的插件配置渲染成一组的 Wasm 沙箱实例
VM 模块提供对 Wasm Runtime(虚拟机) 的统一封装,负责 .wasm 文件的编译、执行,以及 Wasm 沙箱实例的资源管理
ABI 模块则提供对外的使用接口,可以看作是 MOSN 与 Wasm 扩展代码之间交互的胶水层
本文不再对框架的具体实现细节进行介绍,感兴趣的读者可以阅读开源 PR 文档了解细节。
Proxy-Wasm ABI 规范
本小节将介绍 MOSN 具体是如何跟 Wasm 扩展程序进行交互的。先说结论: MOSN 跟 Wasm 扩展代码之间的交互采用的是社区规范: Proxy-Wasm。
Proxy-Wasm 规范定义了宿主机与 Wasm 扩展程序之间的交互细节,包括 API 列表、函数调用规范以及数据传输规范这几个方面。其中,API 列表包含了 L4/L7、property、metrics、日志等方面的扩展点,涵盖了网络代理场景下所需的大部分交互点,且可以划分为宿主侧扩展和 Wasm 侧扩展。这里简单展示规范中的部分内容,完整内容请参考 spec。
规范的实现需要宿主侧和 Wasm 侧两边配合才能正常工作。对于 Wasm 侧,社区已经有 C++、Rust 和 Go 三种语言实现的 SDK,用户可以直接使用这些 SDK 来编写与宿主无关的 Wasm 扩展程序。而对于宿主侧,社区只提供了 C++ 和 Rust 的宿主侧实现。为此,我们在项目中使用 Go 语言对 Proxy-Wasm 规范的宿主侧进行了实现,并将其贡献给开源社区,使之成为社区推荐的 Go-Host 实现 (如下图所示)。需要强调的是,宿主侧实现并不依赖具体的网络代理程序,理论上任何直接通过 Host 程序与 Wasm 扩展进行交互。
我们以 HTTP 场景为例,介绍在 MOSN 中是如何通过 Proxy-Wasm 规范来与 Wasm 扩展程序进行交互,处理 HTTP 请求的。
MOSN 收到 HTTP 请求时,将请求解码成 Header、Body、Trailer 三元组结构,按照配置依次执行 StreamFilters。 执行到 Wasm StreamFilter 时,MOSN 将请求三元组传递给 Proxy-Wasm 宿主侧实现 proxy-wasm-go-host。 宿主侧 go-host 将 MOSN 请求三元组编码成规范指定的格式,并调用规范中的 proxy_on_request_headers 等接口,将请求信息传递至 Wasm 侧。 Wasm 侧 SDK 将请求数据从规范格式转换为便于用户使用的格式,随后调用用户编写的扩展代码。 用户代码返回,Wasm 侧将返回结果按规范格式传递回 MOSN 侧。 MOSN 继续执行后续 StreamFilter。
上述示例中,我们并不限制 Wasm 侧的语言实现,用户可以使用 C++/Rust/Go 几种语言来编写自定义的扩展代码。与之相对的,只需要用相应语言的 Proxy-Wasm-SDK 一起编译成 .wasm 文件,即可运行在 MOSN 之上。
工程实践
Quick Start
在演示中,我们通过配置让 Wasm 扩展插件来处理 MOSN 接收的 HTTP 请求,MOSN 的监听端口为 2045。在 Wasm 处理请求的源码中,我们通过 Proxy-Wasm 规范中的 proxy_dispatch_http_call 接口向外部 HTTP 服务器发起请求,Wasm 源码内指定外部 HTTP 服务器的监听端口为 2046。演示场景的流程如下图所示:
将扩展程序编译成 .wasm 文件 启动 MOSN 并加载 Wasm 插件 启动外部 HTTP 服务器 请求验证
1. 编译 Wasm 扩展程序
我们在示例工程中提供了 C 和 Go 两种语言实现的 Wasm 扩展源码,对 Proxy-Wasm 规范的采用使得我们能够利用多种语言 (C++/Rust/Go) 来编写 Wasm 扩展代码。出于编译的便利性,这里使用 Go 源码实现进行演示。
make
上述操作会将目录下的 filter-go.go 源码文件编译成 filter-go.wasm 文件
2. 启动 MOSN
示例工程提供了一份加载 filter-go.wasm 扩展文件的配置,通过以下命令即可启动:
./mosn start -c config.json
上述命令中使用的 MOSN 可执行程序可以通过以下命令由源码构建:
3. 启动外部 HTTP 服务器
该示例工程中,Wasm 扩展源码会通过 MOSN 向外部 HTTP 服务器发起请求,请求的 URL 为:
http://127.0.0.1:2046/
为此,示例工程也提供了一段 HTTP 服务器代码,当其收到 HTTP 请求时,均会返回响应头: from: external http server,返回响应体: response body from external http server。
go run server.go
4. 请求验证
curl -v http://127.0.0.1:2045/
执行上述命令后,MOSN 终端将能够观察到以下日志:
性能测试
测试环境:
OS: macOS Catalina 10.15.4
CPU: Intel(R) Core(TM) i7-7660U CPU @ 2.50GHz 4Core
MEM: 16 GB 2133 MHz LPDDR3
Go Version: go1.14.13 darwin/amd64
测试场景:
拓扑: client --http1.1--> MOSN
操作: MOSN 收到 H1 请求后,往请求头中添加一个 Header 随后返回 200
测试数据:
「native」表示添加 Header 的操作使用 MOSN 原生的 Stream Filter 完成;
「wasm」表示添加 Header 的操作使用 Wasm 扩展完成
固定 QPS 模式,将 QPS 固定为 2000 进行压测
压测命令: sofaload --h1 -c 100 -t 4 --qps=2000 -D 30 http://127.0.0.1:2045/
压测模式,不限制压测 QPS,将流量打到最大
压测命令: sofaload --h1 -c 100 -t 4 -n 1000000 http://127.0.0.1:2045/
异常调试
由于 Wasm 本身的定位是与编程语言无关的字节码规范,不同语言的源代码 (C++/Go/JavaScript 等) 均能够编译为统一的 Wasm 字节码,因此如何屏蔽具体编程语言的细节模型,制定语言无关的调试信息规范,是社区需要解决的难题之一。
针对这一问题,在当前的工程实践中,JavaScript 语言采用的是 Source Map 格式,而 C++、Rust 和 Go 语言采用的是 Dwarf 格式的调试信息。对具体调试信息格式的介绍并不在本文的范围之内,读者可自行参考外部文章。这里需要强调的是,对于 Wasm 而言,还需要对调试信息的格式进行一定的扩展,才能满足实际的应用需要。与其他编程语言不同的是,.wasm 文件是能够被转换成 .wat 格式,并手动编辑内容的,编译好的 .wasm 文件仍然有修改段内容的可能。为了适应这种场景,Wasm 调试规范对 Dwarf 格式中的位置信息编码进行了调整,指令的偏移值被设置成基于 Code 段的偏移:
With WebAssembly, the .debug_line section maps Code section-relative instruction offsets to source locations.
为此,我们在解析指令偏移时,需要偏移数值进行调整,减去 Code 段的偏移量,才能得到 Wasm 指令的实际偏移值,进而利用 .debug_line 段定位到准确的源码行。下图展示了利用 MOSN 输出的错误日志定位 Wasm 故障源码行的示例。
协议编解码能力: https://www.atatech.org/articles/199319 编解码插件开发指南:
https://www.atatech.org/articles/200651
总结
总而言之,WebAssembly 技术的出现仍然为我们提供了一种启发和希望,促使我们进一步思考如何在云原生时代更好地践行安全可信这一信条。
延伸阅读