查看原文
其他

Protocol Extension Base On Wasm——协议扩展篇

商宗海 金融级分布式架构 2021-08-09
目前 Mesh 本身就有非常多的协议支持的诉求,原来的方式是直接用 Golang 写协议解析的代码,现在通过 Wasm 可以让 MOSN 更好地以更加灵活以及可扩展的方式去支持协议开发。本文主要介绍基于 Wasm 实现开放协议扩展流程和原理,更好的帮助开发者理解和更容易接入 Mesh。


背景

在云原生趋势下,对厂商和客户扩展语言应该是包容的,不应该将用户的技术栈绑定到 Sidecar 语言中,我们关注到 Wasm 能解决这个问题,它提供沙箱隔离机制,允许多语言编写代码打包成 Wasm文件,然后嵌入到 Sidecar 中执行。

理想情况下,MOSN 保证核心组件稳定,厂商私有代码不应该合并到 MOSN 中,应该具备按需扩展能力即可,MOSN 具备灵活的扩展能力,以及对多语言友好、稳定和安全性高等能力。本文重点会以 Go 语言为例,讲解如何基于 Wasm 对 MOSN 的协议进行扩展。


协议拓展

在具体讲解扩展前,简单介绍一下使用 Wasm 扩展的优缺点。

使用 Wasm 扩展将带来以下好处:
  • 隔离性 : Wasm 扩展将运行在资源受限的沙箱中,扩展代码的漏洞及崩溃都无法传导到沙箱之外,沙箱所使用的 CPU、内存资源等受宿主机(MOSN)控制。
  • 安全性 : Wasm 扩展只能通过一组有限的、明确定义的 ABI 与 MOSN 进行通信,MOSN 对该 ABI 具有完全的控制权,这使得 Wasm 扩展只能使用 MOSN 允许的能力、访问受许可的资源。
  • 敏捷性 : Wasm 扩展框架允许在不重启 MOSN 的前提下,动态加载、更新、卸载 Wasm 扩展插件。
  • 灵活性 : 可以使用多种语言编写 Wasm 扩展,例如: Go、C++、Rust 等,甚至直接复用社区扩展插件。
使用 Wasm 扩展包含以下缺点:
  • 增加性能开销:目前沙箱和宿主机内存隔离,插件和 MOSN 数据交换需要通过 ABI 和内存 Copy,会增加性能开销。
  •  成熟度相对不足:目前 Wasm runtime 还需要进行生产验证,目前基于 C 的 Wasm 实现相对比较成熟。

1. 模块装载流程

在讲解模块装载流程前,我们先看一段最小化激活 MOSN Wasm 扩展的配置:



可以看到,激活扩展插件的配置相对来说比较清晰明了。主要提供插件路径和指定引擎(标记 1、2 和 3),在 MOSN 扩展点位置引用插件 ID 即可(标记 4、5 和 6)。接下来,我们看下装载流程:


开发同学编写好协议插件后,然后编译为.wasm文件,在 MOSN 主进程启动期间,根据提供的配置文件(插件路径、filter 中正确指定插件 ID),MOSN 会根据配置的引擎名称、配置文件装载 .wasm 文件,初始化 Wasm Module,然后创建对应的沙箱实例。以协议举例,当有请求进来时,会触发插件的解码逻辑,插件解码完成后,会主动告知 MOSN 解码完成,由MOSN 生成解码后的请求对象(Frame)。

2. 请求/响应流程

在 MOSN 分层中包括四层,IO、Protocol、Stream 和 Proxy 层,IO 层主要用来读取网络层数据。Protocol 层提供协议编解码能力。Stream 层用于封装请求/响应帧,也会保存一些链接(downstream&upsteam)信息。Proxy 层用来做路由寻址转发等能力。在 RPC 场景中,请求和响应流程都会经过以下步骤:
  • 请求/响应到达 NETWORK/IO 层。
  • 通过协议去解码 Buffer 数据流,创建上下文。
  • 生成 Stream,封装帧以及连接信息。
  • 经过 Proxy 层进行路由转发, 编码请求/响应。



在支持基于 Wasm 虚拟机扩展协议时,会在 Protocol 层扩展一个被称为 Wasm Protocol 代理实现,在协议模块装载时,创建 Wasm Protocol 实例去代理开发者协议插件。因为在 MOSN 中 Protocol 是单实例的,需要在 MOSN 中同时支持多个协议扩展,所以需要为开发者协议插件实现创建非共享的 Wasm Protocol 实例。

3. 编解码流程

一次标准的请求响应,需要考虑很多情况,不仅仅是编解码,还有心跳和 Hijacker 等过程,为了通俗易懂,本文主要以编解码为场景,讲解使用 Wasm 进行协议扩展。
一次标准请求到达 MOSN 时,在整个转发周期中,需要先经过 Decode(协议解码),然后再通过 Encode(协议编码)生成二进制流,通过 Socket 转发出去。基于 Wasm 协议扩展,会经过以下步骤:
  • 数据报文委托给扩展协议 Wasm Protocol 解码。
  • 沙箱内扩展协议解码被调用,返回 Command。
  • Command 在转发前,委托给 Wasm Protocol 编码。
  • 沙箱内开发者扩展编码被调用,返回 Buffer。



Question:
1. MOSN 中 Protocol 是单实例的,基于 Wasm 是如何支持多个协议同时扩展的?
在 Wasm Module 装载时,解析 filter_chains 时会给扩展协议注册一份 Wasm Protocol 扩展实例, 这样 Wasm Protcol 就能准确识别代理具体协议了。
沙箱内存隔离,如何解决协议数据类型透传? 因为不像动态链接库那样,同一个类型加载插件和 Host 共享类型。Host 的 Command 传给 Plugin,Plugin 的 Command 是开发者私有对象,需要类型匹配。
在协议扩展中,沙箱内插件请求 Command 类型不能和 Host 共享,因此 Command 类型无法传递。因为插件和 Host 之间通信采用 ABI 交互,因此我们在 ABI 接口设计上采用通用协议传输,以解码 ABI(插件上报给 Host)格式举例:


接下来,我们更进一步拆解编解码内部流程 Host 是如何与沙箱插件进行交互的:
  • 当 IO 数据流到达时,Connection 会分发(dispatch)Buffer, 会创建 downstream 的上下文 Context。
  • 在调用 Wasm Protocol 的解码之前,会调用沙箱插件 OnContextCreate 方法创建插件上下文(简称 Wasm Context), Wasm Context 对象会保存在 Host 的上下文中,用于回调插件生命周期方法。
  • Host 调用插件解码方法,会通过abi规范方法传递 Buffer 字节和长度,同时也会把当前会话的 contextID 透传给插件。
  • 沙箱内 SDK 会根据 contextID 查找已经创建的 Protocol Context (开发者提供的协议插件),调用协议解码并生成 Command。
  • 在整个 Decode 的流程中,Host 和沙箱插件已经获得锁。沙箱插件会根据解出的 Command,生成 Host 侧能理解的 Command 结构,由 Wasm Protocol 生成 Request 或者 Response 类型 Command。
  • 在 Host 转发请求到远端主机时,会再次调用 Wasm Protocol 进行一次编码, 这里会通过编码 ABI 接口,同时会把 contextID 透传给插件。
  • 沙箱内 SDK 会根据 contextID 查找已经创建的 ProtocolContext (开发者提供的协议插件),会先用第 4 步生成的 Command 作为参数,传递给协议插件编码(encode)入参, 如果此时 Host 侧 Header 和 Content 有变更,在传递给协议插件之前,会更新 Command 的 Header 和 Content,保证 Host 侧的内容不会丢失。
  • 当协议插件编码生成 Buffer 时,沙箱 SDK 会负责将编码数据 Copy 到 Host侧(通过 ABI 接口),然后通过 Connection 发送出去。
  • 当收到响应时,针对 Response 的  Command,会创建新的 Context,步骤 1~8 会重新执行一遍。特殊的情况,在收到响应时,Host 清理资源时,会将请求的 Wasm Context 和响应的 Wasm Context 一并清除, 防止内存泄露。


沙箱 SDK 将编码数据 Copy 到 Host 侧,通过以下 ABI 接口对 Host 发起调用:



为了澄清 Context 之间的关系,在这里做一些简要说明。在下图中的 Context 我们是指沙箱内插件的上下文( Wasm Context )。在 Wasm 文件被加载进 Host 时,Host 会解析并创建对应的 Module,基于 Module 创建等价于 CPU 数量的沙箱实例。Root Context 一般用于全局的生命周期回调,比如虚拟机启动通知、插件激活通知和定时器等。Host 侧会为每个会话(请求级别)创建对应的 Context (这里对应于沙箱内部上下文),沙箱实例能够处理多笔请求。Host 本身实现会创建 Context,这个和沙箱的上下文是独立的,没有 Wasm 扩展之前它会存一些连接 ID、当前 listener 类型和协议信息等,有了 Wasm 之后,我们会在 Host 的 Context 中存储 Wasm Context 信息而已。


Qiuck Start

本小节主要演示快速跑通协议扩展流程,我们基于 Wasm 扩展机制实现 wasm-bolt 协议插件(基于原生 bolt 协议),跑通主流程比较简单,分为以下步骤:

  • 提供插件代码,并打包成 bolt-go.wasm 文件。

  • 启动 MOSN 并装载 bolt-go.wasm 插件。

  • 启动 JAVA SOFABoot服务端和客户端程序。

演示场景:MOSN 端会监听端口 2045 和 2046,当 2045 端口监听到 SOFABoot 客户端发来的请求时,会转发给 2046 端口,最终会把请求转发给 SOFABoot 服务端应用程序。


1. 编写协议扩展

基于 wasm-bolt 插件示例我们已经编译好了,可以作为演示程序直接使用。如果希望自己手动编译, 提供 2 种方式进行编译。
采用镜像的方式开发环境编译,在 examples 目录 bolt (路径附在文章末端)中执行命令,编译成功后会在 build 目录生成 bolt-go.wasm 文件:

如果直接在本地编译,需要 tiny-go >= 0.17.0 版本, 可以在 examples 目录 bolt 中执行命令:

2. 启动 MOSN

目前提供了一份用于 Wasm 启动的配置文件(路径附在文章末端) mosn_rpc_config_wasm.json,可以使用以下命令启动 MOSN:

提示:mosn_rpc_config_wasm 中已经配置了 bolt-go.wasm,在项目根目录 etc/wasm/ 目录中。
其中, mosnd 可执行文件可以通过编译 MOSN 获取, 执行以下命令:

如果是研发同学,可以根据 Step 2 拉取代码,直接通过 intellij idea 右键项目根目录 Debug(这样就不用手动去编译且不需要命令行启动 MOSN 了),在 Edit Configurations... 调试配置页签中修改包路径和程序入口参数:

3. 启动 SOFABoot

目前 SOFABoot 应用测试程序已经托管到 Github 上,可以通过以下命令获取:

启动 SOFABoot 服务端程序:

java -DMOSN_ENABLE=true -Drpc_tr_port=12199 -Dspring.profiles.active=dev -Drpc_register_registry_ignore=true -jar sofa-echo-server-web-1.0-SNAPSHOT-executable.jar

然后启动 SOFABoot 客户端程序:

java -DMOSN_ENABLE=true -Drpc_tr_port=12198 -Dspring.profiles.active=dev -Drpc_register_registry_ignore=true -jar sofa-echo-client-web-1.0-SNAPSHOT-executable.jar

当客户端启动成功后,会在终端输出以下信息(每隔 1 秒发起一次 Wasm 请求):

当前扩展特性已经合并进开源社区:
- wasm protocol #1579:

https://github.com/mosn/mosn/pull/1597?spm=ata.21736010.0.0.4e6513eeCnOrtd

- mosn api #31:

https://github.com/mosn/api/pull/31?spm=ata.21736010.0.0.4e6513eeCnOrtd

- wasm sdk-go:

https://github.com/zonghaishang/proxy-wasm-sdk-go?spm=ata.21736010.0.0.4e6513eeCnOrtd


附:

Wasm 启动配置文件:

https://github.com/mosn/mosn/blob/master/configs/mosn_rpc_config_wasm.json?spm=ata.21736010.0.0.4e6513eeQRr96Y&file=mosn_rpc_config_wasm.json

example 目录:

https://github.com/zonghaishang/proxy-wasm-sdk-go/tree/master/examples/bolt


 延伸阅读


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存