WebAssembly 在 MOSN 中的实践 - 基础框架篇

本文涉及的产品
性能测试 PTS,5000VUM额度
简介: 本文将介绍 WebAssembly 技术在 MOSN 中的实践,首先介绍了当前 MOSN 在扩展隔离方面所面临的痛点,并对 Wasm 技术的相关背景知识进行介绍。
作者:叶永杰
来源:金融级分布式架构公众号
本文将介绍 WebAssembly 技术在 MOSN 中的实践,首先介绍了当前 MOSN 在扩展隔离方面所面临的痛点,并对 Wasm 技术的相关背景知识进行介绍。随后描述了Wasm 扩展框架的整体架构,并介绍了我们在 Proxy-Wasm 社区规范中所做的贡献,最后描述了框架在性能、异常调试等方面的实践内容。

作为金融级服务网格中的流量代理组件,MOSN 在承载蚂蚁数十万服务容器之间流量的同时,也承载着诸多例如限流、鉴权、路由等中间件基础能力。这些能力以不同的扩展形式与 MOSN 运行于同一进程内。非隔离的运行方式在保障性能的同时,却也给 MOSN 带来了不可预知的安全风险。

针对上述问题,我们采用 WebAssembly(Wasm) 技术,给 MOSN 实现了一个安全隔离的沙箱环境,让扩展程序能够运行在隔离沙箱之中,并对其资源、能力进行严格限制,使程序故障止步于沙箱,从而实现安全隔离的目标。本文将着重叙述 MOSN 中的 Wasm 扩展框架,并介绍我们在 Proxy-Wasm 这一开源规范上的贡献。

总体设计

image.png

上图为 MOSN Wasm 扩展框架的整体示意图。如图所示,对于 MOSN 的任意扩展点(Codec、NetworkFilter、StreamFilter 等),用户均能够通过 Wasm 扩展框架,以隔离沙箱的形式运行自定义的扩展代码。而 MOSN 与 Wasm 扩展代码之间的交互,则是通过 Proxy-Wasm 标准 ABI 来完成的。

隔离沙箱

当我们在讨论 Wasm 时,都明白 Wasm 能够提供一个安全隔离的沙箱环境,但并不是每个人都了解 Wasm 实现隔离沙箱的技术原理。这时又要搬出计算机科学中的至理名言: “计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。Wasm 实际上也是通过引用一个“中间层”来实现的安全隔离。简单来说,Wasm 通过一个运行时 (Runtime) 来运行 Wasm 沙箱扩展,每个 Wasm 沙箱都有其独立的线性内存空间和一组导入/导出模块。

image.png

一方面,每个 Wasm 沙箱都有其独立的线性内存空间,其内存模型如上图所示。Wasm 代码只能通过简单的 load/store 等指令访问线性内存空间的有限部分,并通过符号(下标)的方式来间接访问函数、全局变量等。上述限制杜绝了类似 C 语言中访问任意内存地址的骚操作。同时,用于间接调用函数的符号表对于 Wasm 代码而言是只读的,保证了 Wasm 代码的执行是受控的。此外,Wasm 沙箱的整个线性内存空间由宿主机 (Wasm Runtime) 分配及管理,通过严格的内存管理保证沙箱的隔离性。

另一方面,Wasm 也规定了代码中任何可能产生外部影响的操作只能通过导入/导出模块来实现。当我们在编写 C 语言源码时,可以直接通过系统调用来访问系统的环境变量、文件、网络等资源。而在 Wasm 的世界中,并不存在系统调用相关的指令,任何对外部资源的访问必须通过导入模块来间接实现。以文件读写为例,在 Wasm 中要想进行文件读写,需要宿主机提供实现文件读写功能的导入函数,Wasm 代码调用该导入函数,由宿主机间接进行文件读写,再将操作结果返回给 Wasm 扩展。在上述过程中,实际的文件读写操作由宿主机完成,宿主机对这一过程有绝对的控制权,包括但不限于只允许读写指定文件、限制读写内容、完全禁止读写等。

扩展框架

MOSN 以 插件(Plugin) 的形式对 Wasm 扩展进行统一管理,插件是指一组 Wasm 沙箱实例及其配置的集合。用户通过配置来加载、更新以及卸载 Wasm 插件,并通过配置来描述沙箱实例的运行规格(使用的执行引擎、Wasm 文件路径、实例数量等)。下面展示了一个典型的 Wasm 插件配置:

image.png

当 MOSN 加载上述插件配置时,会按照以下流程生成插件对应的 Wasm 沙箱实例:

image.png

在后续运行的过程中,用户通过 Wasm 扩展框架获取指定插件的沙箱实例, 然后通过沙箱实例暴露的 API 与扩展程序进行交互。本文的下一小节将对此交互过程进行详细描述。在 MOSN 中,Wasm 扩展框架与具体用途无关,在 MOSN 已有的任何一处扩展点,均可以直接使用 Wasm 框架来获取安全隔离的插件执行能力。

如下图所示,Wasm 扩展框架主要分为 Manager、VM 和 ABI 三个子模块。其中

  • Manager 模块负责对 Wasm 插件的配置进行统一管理,提供插件的增删查改功能,并负责将用户提供的插件配置渲染成一组的 Wasm 沙箱实例
  • VM 模块提供对 Wasm Runtime(虚拟机) 的统一封装,负责 .wasm 文件的编译、执行,以及 Wasm 沙箱实例的资源管理
  • ABI 模块则提供对外的使用接口,可以看作是 MOSN 与 Wasm 扩展代码之间交互的胶水层

image.png

本文不再对框架的具体实现细节进行介绍,感兴趣的读者可以阅读开源 PR 文档了解细节。

由于当前市面上几乎不存在使用 Go 语言直接编写的 Wasm Runtime,因此 MOSN 只能通过 CGO 调用的方式来间接地调用由 C++/Rust 编写的 Wasm 执行引擎。我们从 SDK 完善程度、性能、项目活跃度等角度综合考虑,经过一系列横向对比之后,选择了 Wasmer 作为 MOSN 默认的执行引擎。

Proxy-Wasm ABI 规范

本小节将介绍 MOSN 具体是如何跟 Wasm 扩展程序进行交互的。先说结论: MOSN 跟 Wasm 扩展代码之间的交互采用的是社区规范: Proxy-Wasm。

Proxy-Wasm 是开源社区针对「网络代理场景」设计的一套 ABI 规范,属于当前的事实规范。当前支持该规范的网络代理软件包括 Envoy、MOSN 和 ATS(Apache Traffic Server),支持该规范的 Wasm 扩展 SDK 包括 C++、Rust 和 Go。采用该规范的好处在于能让 MOSN 复用社区既有的 Wasm 扩展 (包括 Go 实现以及 C++/Rust 实现),也能让原本专门为 MOSN 开发的 Wasm 扩展运行在 Envoy 等网络代理产品上。

Proxy-Wasm 规范定义了宿主机与 Wasm 扩展程序之间的交互细节,包括 API 列表、函数调用规范以及数据传输规范这几个方面。其中,API 列表包含了 L4/L7、property、metrics、日志等方面的扩展点,涵盖了网络代理场景下所需的大部分交互点,且可以划分为宿主侧扩展和 Wasm 侧扩展。这里简单展示规范中的部分内容,完整内容请参考 spec。

image.png


image.png

规范的实现需要宿主侧和 Wasm 侧两边配合才能正常工作。对于 Wasm 侧,社区已经有 C++、Rust 和 Go 三种语言实现的 SDK,用户可以直接使用这些 SDK 来编写与宿主无关的 Wasm 扩展程序。而对于宿主侧,社区只提供了 C++ 和 Rust 的宿主侧实现。为此,我们在项目中使用 Go 语言对 Proxy-Wasm 规范的宿主侧进行了实现,并将其贡献给开源社区,使之成为社区推荐的 Go-Host 实现 (如下图所示)。需要强调的是,宿主侧实现并不依赖具体的网络代理程序,理论上任何直接通过 Host 程序与 Wasm 扩展进行交互。

image.png

我们以 HTTP 场景为例,介绍在 MOSN 中是如何通过 Proxy-Wasm 规范来与 Wasm 扩展程序进行交互,处理 HTTP 请求的。

image.png

  • 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

本小节主要演示如何在 MOSN 中进行配置并运行 Wasm 扩展插件流程。演示所需的源文件参考 example。

在演示中,我们通过配置让 Wasm 扩展插件来处理 MOSN 接收的 HTTP 请求,MOSN 的监听端口为 2045。在 Wasm 处理请求的源码中,我们通过 Proxy-Wasm 规范中的 proxy_dispatch_http_call 接口向外部 HTTP 服务器发起请求,Wasm 源码内指定外部 HTTP 服务器的监听端口为 2046。演示场景的流程如下图所示:

image.png

该演示流程主要分为以下步骤:

  • 将扩展程序编译成 .wasm 文件
  • 启动 MOSN 并加载 Wasm 插件
  • 启动外部 HTTP 服务器
  • 请求验证

1.编译 Wasm 扩展程序

我们在示例工程中提供了 C 和 Go 两种语言实现的 Wasm 扩展源码,对 Proxy-Wasm 规范的采用使得我们能够利用多种语言 (C++/Rust/Go) 来编写 Wasm 扩展代码。出于编译的便利性,这里使用 Go 源码实现进行演示。

进入 example/wasm/httpCall 目录,执行命令:

make

上述操作会将目录下的 filter-go.go 源码文件编译成 filter-go.wasm 文件

2.启动 MOSN

示例工程提供了一份加载 filter-go.wasm 扩展文件的配置,通过以下命令即可启动:

./mosn start -c config.json

上述命令中使用的 MOSN 可执行程序可以通过以下命令由源码构建:

image.png

3.启动外部 HTTP 服务器

该示例工程中,Wasm 扩展源码会通过 MOSN 向外部 HTTP 服务器发起请求,请求的 URL 为:

http://127.0.0.1:2046/

为此,示例工程也提供了一段 HTTP 服务器代码,当其收到 HTTP 请求时,均会返回响应头: from: external http server,返回响应体: response body from external http server。

执行以下命令将启动上述 HTTP 服务器:

go run server.go

4.请求验证

上述操作准备就绪后,便可通过 Curl 来进行请求验证了。

curl -v http://127.0.0.1:2045/

执行上述命令后,MOSN 终端将能够观察到以下日志:

image.png

性能测试

测试环境:

  • 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/

image.png

  • 压测模式,不限制压测 QPS,将流量打到最大
压测命令: sofaload --h1 -c 100 -t 4 -n 1000000 http://127.0.0.1:2045/

image.png

异常调试

对于实际的工程项目而言,光能运行是不够的,必须具备一定的问题排查和定位能力,才能在遇到程序故障时,解析异常源码的调用堆栈,快速定位第一现场,从而提高开发及调试的效率。
由于 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 故障源码行的示例。

image.png

https://www.atatech.org/articles/200651

总结

对于蚂蚁而言,安全可信永远是我们追求的目标,而面对越来越多的扩展场景,MOSN 需要一个安全可靠的隔离环境,以避免扩展代码给 MOSN 运行造成的安全风险。为此,我们采用 WebAssembly 技术,为 MOSN 实现了一个基于 Wasm 隔离沙箱的插件扩展框架。MOSN 采用网络代理社区中的 Proxy-Wasm 规范,实现了语言无关、宿主无关的网络代理扩展能力。同时,我们也向开源社区贡献了 Proxy-Wasm-Go-Host 实现,积极融入开源社区。

需要注意的是,当前 WebAssembly 技术仍处于发展阶段,Go 语言自身对 WebAssenbly 生态的支持仍有巨大的提升空间。我们在实践的过程中,也总是面临 Go 语言在 Wasm 生态中不够给力的情况。由于 Go 官方编译器还不支持将 Go 源码程序编译成 WASI 系统接口 (GOOS=wasi) 的 .wasm 文件,我们不得不借助 TinyGo 来完成 Go 扩展程序的编译,而这也导致我们需要面对 TinyGo 在语言特性支持程度、性能、稳定性等方面不足的痛点。与之相比,C++/Rust 对 Wasm 生态的支持程度就要完善得多。

总而言之,WebAssembly 技术的出现仍然为我们提供了一种启发和希望,促使我们进一步思考如何在云原生时代更好地践行安全可信这一信条。

延伸阅读

image.png

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
相关文章
|
7月前
|
监控 Kubernetes 持续交付
构建高效可扩展的微服务架构:后端开发实践指南
在数字化转型的浪潮中,企业对软件系统的要求日益提高,追求快速响应市场变化、持续交付价值成为核心竞争力。微服务架构以其灵活性、模块化和独立部署的特点,成为解决复杂系统问题的有效途径。本文将深入探讨如何构建一个高效且可扩展的微服务架构,涵盖关键设计原则、技术选型及实践案例,为后端开发者提供一条清晰的指导路线,帮助其在不断变化的技术环境中保持竞争力。
274 3
|
3月前
|
存储 Java Maven
从零到微服务专家:用Micronaut框架轻松构建未来架构
【9月更文挑战第5天】在现代软件开发中,微服务架构因提升应用的可伸缩性和灵活性而广受欢迎。Micronaut 是一个轻量级的 Java 框架,适合构建微服务。本文介绍如何从零开始使用 Micronaut 搭建微服务架构,包括设置开发环境、创建 Maven 项目并添加 Micronaut 依赖,编写主类启动应用,以及添加控制器处理 HTTP 请求。通过示例代码展示如何实现简单的 “Hello, World!” 功能,并介绍如何通过添加更多依赖来扩展应用功能,如数据访问、验证和安全性等。Micronaut 的强大和灵活性使你能够快速构建复杂的微服务系统。
130 5
|
3月前
|
Kubernetes Cloud Native Java
探索未来编程新纪元:Quarkus带你秒建高性能Kubernetes原生Java应用,云原生时代的技术狂欢!
Quarkus 是专为 Kubernetes 设计的全栈云原生 Java 框架,凭借其轻量级、快速启动及高效执行特性,在 Java 社区脱颖而出。通过编译时优化与原生镜像支持,Quarkus 提升了应用性能,同时保持了 Java 的熟悉度与灵活性。本文将指导你从创建项目、编写 REST 控制器到构建与部署 Kubernetes 原生镜像的全过程,让你快速上手 Quarkus,体验高效开发与部署的乐趣。
58 0
|
4月前
|
移动开发 小程序 前端开发
跨端技术演进问题之Web容器方案在跨端开发中的优势和不足如何解决
跨端技术演进问题之Web容器方案在跨端开发中的优势和不足如何解决
|
4月前
|
前端开发 开发者 容器
跨端技术演进问题之需要实现跨端容器的标准化如何解决
跨端技术演进问题之需要实现跨端容器的标准化如何解决
|
7月前
|
消息中间件 API 数据库
构建高性能微服务架构:后端开发的终极指南
【5月更文挑战第27天】随着现代业务需求的不断演进,传统的单体应用架构逐渐显得笨重且难以维护。微服务架构以其灵活性、可扩展性和技术多样性成为企业转型的首选。本文将深入探讨如何构建一个高性能的微服务系统,涵盖关键设计原则、常用技术和实践案例,为后端开发人员提供一份全面的指南。
|
7月前
|
弹性计算 Kubernetes 开发者
利用容器化技术实现跨平台部署的Web应用开发
本文将介绍如何利用容器化技术,例如Docker和Kubernetes,实现跨平台部署的Web应用开发。我们将探讨容器化的优势以及如何使用Docker容器打包应用程序,然后利用Kubernetes进行管理和部署。通过容器化技术,开发者可以更加便捷地进行Web应用的开发、测试和部署,提高开发效率和应用的可靠性。
|
7月前
|
消息中间件 运维 监控
构建高性能微服务架构:后端开发的实践指南
【4月更文挑战第23天】 在当今互联网应用的快速迭代与海量用户访问的背景下,传统的单体应用架构逐渐显露出其扩展性与维护上的局限性。微服务架构作为一种解决方案,以其服务的细粒度、独立性和弹性等特性,被广泛认为是提升系统可维护性和扩展性的有效途径。本文将深入探讨如何构建一个高性能的微服务架构,从基础理论到具体实践,为后端开发者提供一套系统的指导方案。我们将涵盖微服务设计原则、技术选型、性能优化、以及监控与故障处理等关键话题。
|
7月前
|
存储 Kubernetes 开发者
构建高效微服务架构:后端开发者的终极指南
【5月更文挑战第21天】随着现代软件开发的不断演进,微服务架构已成为实现可扩展、灵活和容错系统的关键。本文将深入探讨微服务的核心概念、设计原则以及如何利用最佳实践构建一个高效的微服务系统。我们将分析微服务的优势与挑战,并展示如何在现实世界中应用这些知识来优化后端应用程序的性能和可靠性。
|
开发框架 移动开发 前端开发
跨端框架盘点
跨端框架盘点
90 0