开发 Wasm 协议插件指南
本文主要详细介绍如何基于 wasm go sdk 实现协议扩展以及相关细节,更好的帮助开发者支持更多协议场景。
创建插件工程
一、前置准备
安装 go
链接:https://golang.org/doc/install
安装 tinygo
链接:https://tinygo.org/getting-started/linux/
提示:如果已有 go 不需要重复安装,tinygo 用于编译成 wasm 插件。tinygo 也可以从 github 直接下载解压,把解压后的 bin 目录加入到 PATH 目录。
二、创建项目工程
# 1. 查看GOPATH路径
go env | grep GOPATH
# 2. 在GOPATH/src目录中创建
mkdir plugin-repo
cd plugin-repo
# 3. 执行项目初始化
go mod init
# 4. 创建协议插件目录名称,假设叫做
boltmkdir -p bolt/main
执行完成后,目录结构如下:
plugin-repo // 插件仓库根目录
├── go.mod // 项目依赖管理
└── bolt // 插件名称,开发者扩展代码放到这里
└── main // 注册插件逻辑,开发者编写注册插件逻辑
└── build // 插件编译后,自动生成
因为在开始编写插件时,需要依赖 wasm sdk,需要在插件根目录,执行以下命令,拉取依赖:
go get github.com/zonghaishang/proxy-wasm-sdk-go
go mod vendor
提示:完整实例程序已经包含在 github 仓库, 请参考 plugin-repo(https://github.com/zonghaishang/plugin-repo)。
三、 编写插件扩展
plugin-repo // 插件仓库根目录
├── go.mod // 项目依赖管理
├── Makefile // 编译插件成wasm文件
└── bolt
├── protocol.go
├── command.go
├── codec.go
├── api.go
├── main
│ ├── main.go
│ └── main_test.go
├── build // 插件编译后,自动生成
└── bolt-go.wasm
1 、编解码实现
Decode:需要开发者将data中的字节数据解码成请求或者响应
Encode:需要开发者将请求或者响应编码成字节 buffer
type Codec interface {
Decode(ctx context.Context, data Buffer) (Command, error)
Encode(ctx context.Context, cmd Command) (Buffer, error)
}
2、 编解码对象
请求对象:主要包括请求(request-response)、请求(oneway)、心跳类型
响应对象:主要包括响应请求结果对象
请求对象除了表达 request-response 模型、oneway 和心跳,也会承载超时等属性,与之对应响应会承载响应状态码。
目前请求和响应的接口契约如下:
type Request interface {
Command
// IsOneWay Check that the request does not care about the response
IsOneWay() bool
GetTimeout() uint32 // request timeout}
type Response interface {
Command
GetStatus() uint32 // response status}
不管请求还是响应,除了识别 command 类型,还承担请求头部和请求体 2 部分,头部是普通的 key-value 结构,data 部分应该是协议的 content 部分,而不是完整报文内容。
// Command base request or response command
type Command interface {
// Header get the data exchange header, maybe return nil.
GetHeader() Header
// GetData return the full message buffer, the protocol header is not included
GetData() Buffer
// SetData update the full message buffer, the protocol header is not included
SetData(data Buffer)
// IsHeartbeat check if the request is a heartbeat request
IsHeartbeat() bool
// CommandId get command id
CommandId() uint64
// SetCommandId update command id
// In upstream, because of connection multiplexing,
// the id of downstream needs to be replaced with id of upstream
// blog: https:mosn.io/blog/posts/multi-protocol-deep-dive/#%E5%8D%8F%E8%AE%AE%E6%89%A9%E5%B1%95%E6%A1%86%E6%9E%B6
SetCommandId(id uint64)
}
目前提供了示例编解码对象实现,请参考 command(https://github.com/zonghaishang/plugin-repo/blob/master/bolt/command.go)。
3、 协议层
KeepAlive: 根据请求 id 生成一个心跳请求 command
ReplyKeepAlive: 根据收到的请求,返回一个心跳响应 command
type KeepAlive interface {
KeepAlive(requestId uint64) Request
ReplyKeepAlive(request Request) Response
}
注意:如果扩展协议不支持心跳或者不需要心跳,协议层 KeepAlive 方法返回 nil 即可
在 service mesh 场景中,因为增加了一跳,mesh 在转发过程中可能被控制面拦截,比如限流熔断,需要协议层构造并返回响应,因此开发者需要提供 Hijacker 接口实现:
Hijack: 根据请求和拦截状态码,返回一个响应 command
type Hijacker interface {
// Hijack allows sidecar to hijack requests
Hijack(request Request, code uint32) Response
}
目前协议层接口采用组合方式,主要讲编解码独立拆分出去, protocol 接口定义:
type Protocol interface {
Name() string Codec() Codec
KeepAlive
Hijacker
Options
}
接口中方法描述:
Name:返回协议名称 Codec:返回协议编解码对象 KeepAlive:协议心跳实现 Hijacker:处理控制面拦截逻辑 Options:协议层配置选项开发,一般协议组合默认配置 proxy.DefaultOptions
目前提供了示例协议实现,请参考 protocol(https://github.com/zonghaishang/plugin-repo/blob/master/bolt/protocol.go)。
4、 注册协议
在完成协议扩展后,需要将我们编写的插件进行注册,在 wasm 扩展中,我们一切是以 Context 为核心来转的,比如 host 侧触发解码,在沙箱内会调用开发者 protocol context 的回调来解码。
因此注册协议我们需要提供一个 ProtocolContext 接口实现,和 protocol 接口极其类似:
// L7 layer extension
type ProtocolContext interface {
Name() string // protocol name
Codec() Codec // frame encode & decode
KeepAlive() KeepAlive // protocol keep alive
Hijacker() Hijacker // protocol hijacker
Options() Options // protocol options
}
以 bolt 协议插件为例,我们提供 boltProtocolContext 实现:
// 1. 提供bolt插件protocolContext实现
type boltProtocolContext struct {
proxy.DefaultRootContext // notify on plugin start.
proxy.DefaultProtocolContext // 继承默认协议实现,比如使用默认Options()
bolt proxy.Protocol // 插件真实协议实现
contextID uint32
}
// 2. 创建bolt单实例协议实例
var boltProtocol = bolt.NewBoltProtocol()
func boltContext(rootContextID, contextID uint32) proxy.ProtocolContext {
return &boltProtocolContext{
bolt: boltProtocol,
contextID: contextID,
}
}
// 3. 注册boltContext协议钩子
func main() {
proxy.SetNewProtocolContext(boltContext)
}
// 4. 如果协议不支持心跳,这里允许返回nil
func (proto *boltProtocolContext) KeepAlive() proxy.KeepAlive {
return proto.bolt
}
// 5. 如果需要获取插件参数,可以override对应方法
func (proto *boltProtocolContext) OnPluginStart(conf proxy.ConfigMap) bool {
proxy.Log.Infof("proxy_on_plugin_start from Go!")
return true
}
5、 调试&打包
目前 wasm sdk 提供了模拟器实现(Emulator), 可以模拟完整的 MOSN 处理流程,并且可以回调开发者插件对应生命周期方法。基本用法:
// 1. 注册对应context和配置,boltContext在同一个main包下已经实现
opt := proxy.NewEmulatorOption().
WithNewProtocolContext(boltContext).
WithNewRootContext(rootContext).
WithVMConfiguration(vmConfig)
// 2. 创建一个sidecar模拟器
host := proxy.NewHostEmulator(opt)
// release lock and reset emulator state
defer host.Done()
// 3. 调用host对应实现,比如启动沙箱
host.StartVM()
// 4. 调用启动插件
host.StartPlugin()
// 5. 模拟新请求到来,创建插件上下文
ctxId := host.NewProtocolContext()
// 6. 模拟host接收客户端请求,并解码
cmd, err := host.Decode(...)
// 7. 模拟host转发请求,并编码
upstreamBuf, err := host.Encode(...)
// 8. 模拟host处理完请求
host.CompleteProtocolContext(ctxId)
如果要在 GoLand 中直接调试集成测试, 需要执行以下操作:
GoLand->Preferences...->Go->Build Tags & Vendoring->Custom tags填写proxytest 调试窗口 Edit Configurations...-> 勾选 Use all custom build tags
# 1. 本地编译,bolt替换成开发者插件名
make name=bolt
# 2. 基于镜像编译
make build-image name=bolt
四、启动 MOSN
目前提供了一份用于 wasm 启动的配置文件 mosn_rpc_config_wasm.json(https://github.com/mosn/mosn/blob/master/configs/mosn_rpc_config_wasm.json),可以使用以下命令启动 MOSN:
./mosnd start -c /path/to/mosn_rpc_config_wasm.json
目前提供的配置,会开启 2045 和 2046 端口,2045 接收客户端请求,通过 2046 转发给服务端 mosn_rpc_config_wasm 中已经配置了 bolt-go.wasm,在项目根目录 etc/wasm/目录中 如果是自定义协议插件,配置 mosn_rpc_config_wasm.json 中有几点需要修改 vm_config.path 指向的 wasm 路径 wasm_global_plugins.plugin_name和codecs.config.from_wasm_plugin 要相同 codecs.config.from_wasm_plugin 和 extend_config.sub_protocol 要相同(一般协议有 2 个 listener 都要改)
# 下载mosn代码到本地GOPATH, 可以通过本地shell执行:go env | grep GOPATH 查看
# step 1:
mkdir -p $GOPATH/src/mosn.io
cd $GOPATH/src/mosn.io
# step 2:
# clone mosn源码
git clone https://github.com/mosn/mosn.git
# step 3:
# 本地编译
sudo make build-local tags=wasmer
# 编译成功后,会在项目根目录下
build/bundles/v0.21.0/binary/mosnd
如果是研发同学,可以根据 step 2 拉取代码,直接通过 GoLand 右键项目根目录 Debug(这样就不用手动去编译以及不需要命令行启动 MOSN 了), 在 Edit Configurations... 调试配置页签中修改包路径和程序入口参数:
Package path: mosn.io/mosn/cmd/mosn/main
Program arguments: start -c /path/to/mosn_rpc_config_wasm.json
/path/to 需要替换成 MOSN 根目录中到 mosn_rpc_config_wasm.json 文件的完整路径 如果要在 GoLand 中直接调试 MOSN(默认 wasm 模块没有编译), 需要执行以下操作: GoLand->Preferences...->Go->Build Tags & Vendoring->Custom tags追加 wasmer 调试窗口 Edit Configurations...-> 勾选 Use all custom build tags
五、启动应用服务
目前 SOFABoot 应用测试程序已经托管到 github 上,可以通过以下命令获取:
git clone https://github.com/sofastack-guides/sofastack-mesh-demo.git
# checkout到wasm_benchmark分支
git checkout wasm_benchmark
cd sofastack-mesh-demo/sofa-samples-springboot2
# 本地打包sofaboot应用程序
mvn clean package
# 打包成功后,会在sofa-echo-server和sofa-echo-client下生成target目录,
# 其中分别包含服务端和客户端可执行程序,文件名分别为:
# sofa-echo-server-web-1.0-SNAPSHOT-executable.jar
# sofa-echo-client-web-1.0-SNAPSHOT-executable.jar
启动 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 请求):
>>>>>>>> [57,21,7ms]2021-03-16 20:57:05 echo result: Hello world!
>>>>>>>> [57,22,5ms]2021-03-16 20:57:06 echo result: Hello world!
>>>>>>>> [57,23,7ms]2021-03-16 20:57:07 echo result: Hello world!
>>>>>>>> [57,24,7ms]2021-03-16 20:57:08 echo result: Hello world!
>>>>>>>> [57,25,8ms]2021-03-16 20:57:09 echo result: Hello world!
>>>>>>>> [57,26,7ms]2021-03-16 20:57:10 echo result: Hello world!
>>>>>>>> [57,27,5ms]2021-03-16 20:57:11 echo result: Hello world!
>>>>>>>> [57,28,7ms]2021-03-16 20:57:12 echo result: Hello world!
当前扩展特性已经合并进开源社区,感兴趣同学可以查看实现原理:
wasm protocol #1579 (https://github.com/mosn/mosn/pull/1597) mosn api #31 (https://github.com/mosn/api/pull/31) wasm sdk-go (https://github.com/zonghaishang/proxy-wasm-sdk-go)
Protocol Extension Base On Wasm
延伸阅读
活动报名
SOFAStack 开源社区将于 04 月 24 日(周六) 14:00 在北京举办“ SOFA开源三周年,Let's have fun together!”
三年的时间,我们共同见证了 SOFAStack 在各个行业环境中的成长,
我们将在北京迎来 SOFA 三周岁的生日,期待与你一同分享!
现场游戏大奖——HHKB 键盘等你拿!,报名请点击“阅读原文”。