開發 Wasm 協議插件指南

本文主要詳細介紹如何基於 wasm go sdk 實現協議擴展以及相關細節,更好的幫助開發者支持更多協議場景。

創建插件工程

前置準備

https://golang.org/doc/install

https://tinygo.org/getting-started/linux/

提示:如果已有 go 不需要重複安裝,tinygo 用於編譯成 wasm 插件。tinygo 也可以從 github 直接下載解壓,把解壓後的 bin 目錄加入到 PATH 目錄。

創建項目工程

在 $GOPATH/src 目錄中創建工程, 假設項目工程名爲 plugin-repo,用來包含多個插件:

1. 查看 GOPATH 路徑
go env | grep GOPATH
2. 在 GOPATH/src 目錄中創建
mkdir plugin-repocd plugin-repo
3. 執行項目初始化
go mod init
4. 創建協議插件目錄名稱,假設叫做

boltmkdir -p bolt/main 執行完成後,目錄結構如下:

因爲在開始編寫插件時,需要依賴 wasm sdk,需要在插件根目錄,執行以下命令,拉取依賴:

go get github.com/zonghaishang/proxy-wasm-sdk-gogo 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

在協議擴展場景,我們主要提供編解碼(codec)、編解碼對象(command)、協議層支持心跳 / 響應(protocol)和註冊協議(main)。

 編解碼實現

在處理請求和響應流程中,開發者需要實現 Codec 接口, 方法處理邏輯如下:

Decode:需要開發者將 data 中的字節數據解碼成請求或者響應

Encode:需要開發者將請求或者響應編碼成字節 buffer

type Codec interface { Decode(ctx context.Context, data Buffer) (Command, error) Encode(ctx context.Context, cmd Command) (Buffer, error)}

注意:在 Decode 流程中,完成解碼需要調用 data.Drain(frameLen), frameLen 代表完整請求或者報文總長度。

開發者在編寫編解碼時,建議採用協議名 +Codec 命名,比如 bolt 編解碼,命名爲 boltCodec。

目前提供了示例編解碼實現,請參考 boltCodec

(https://github.com/zonghaishang/plugin-repo/blob/master/bolt/codec.go)。

 編解碼對象

編解碼主要在二進制字節流和請求 / 響應對象互轉,開發者在定義請求 / 響應對象,應該遵守 command 接口。目前 command 主要分 2 類,請求和響應。

請求對象除了表達 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 的接口定義如下:

// Command base request or response commandtype 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)。

 協議層

因爲心跳需要協議層理解,如果開發者擴展的協議支持心跳能力,應當提供擴展 

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}type Hijacker interface { // Hijack allows sidecar to hijack requests Hijack(request Request, code uint32) Response}

接口中方法描述:

目前提供了示例協議實現,請參考 protocol

(https://github.com/zonghaishang/plugin-repo/blob/master/bolt/protocol.go)。

 註冊協議

在完成協議擴展後,需要將我們編寫的插件進行註冊,在 wasm 擴展中,我們一切是以 Context 爲核心來轉的,比如 host 側觸發解碼,在沙箱內會調用開發者 protocol context 的回調來解碼。

因此註冊協議我們需要提供一個 ProtocolContext 接口實現,和 protocol 接口極其類似:

// L7 layer extensiontype 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. 如果協議不支持心跳,這裏允許返回 nilfunc (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}

目前提供了示例協議註冊實現,請參考 main。

 調試 & 打包

開發者在編寫完插件後,允許在本地 idea 直接開始調試測試,並且不依賴 MOSN 啓動。目前推薦在協議開發完後,提供 main_test.go 實現,在裏面寫集成測試。目前 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 中直接調試集成測試, 需要執行以下操作:

目前提供了示例集成測試,請參考 main test 

(https://github.com/zonghaishang/plugin-repo/blob/master/bolt/main/main_test.go)。

目前打包插件,可以本地開發環境編譯打包,也支持鏡像方式編譯插件,目前通用 makefile

(https://github.com/zonghaishang/plugin-repo/blob/master/Makefile)已經提供,可以 copy 到插件項目根目錄中使用。

基於 makefile,2 種打包命令分別如下(編譯成功會在插件中創建 build 文件夾,並且輸出 bolt-go.wasm):

1. 本地編譯,bolt 替換成開發者插件名
make name=bolt
2. 基於鏡像編譯
make build-image name=bolt

目前提供了示例打包文件,請參考 bolt-go

(https://github.com/zonghaishang/plugin-repo/tree/master/bolt/build)。

啓動 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

提示:

其中, mosnd 可執行文件可以通過編譯 MOSN 獲取,執行以下命令:

下載 mosn 代碼到本地 GOPATH, 可以通過本地 shell 執行:go env | grep GOPATH 查看
step 1:
mkdir -p $GOPATH/src/mosn.iocd $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/mainProgram arguments: start -c /path/to/mosn_rpc_config_wasm.json

提示:

啓動應用服務

開發完成後,可以先啓動 MOSN,然後啓動應用的服務端和客戶端,以 SOFABoot 應用爲例展示。目前 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!

當前擴展特性已經合併進開源社區,感興趣同學可以查看實現原理:

(https://github.com/mosn/mosn/pull/1597)

(https://github.com/mosn/api/pull/31)

(https://github.com/zonghaishang/proxy-wasm-sdk-go)

實現原理文章,參考:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/l9xBeZokR5qZbnYTtzZ50A