本文將介紹 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 技術的出現仍然爲我們提供了一種啓發和希望,促使我們進一步思考如何在雲原生時代更好地踐行安全可信這一信條。
延伸閱讀
本文分享自微信公衆號 - 金融級分佈式架構(Antfin_SOFA)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。