WebAssembly 在 MOSN 中的實踐 - 基礎框架篇

本文將介紹 WebAssembly 技術在 MOSN 中的實踐,首先介紹了當前 MOSN 在擴展隔離方面所面臨的痛點,並對 Wasm 技術的相關背景知識進行介紹。隨後描述了Wasm 擴展框架的整體架構,並介紹了我們在 Proxy-Wasm 社區規範中所做的貢獻,最後描述了框架在性能、異常調試等方面的實踐內容。

作爲金融級服務網格中的流量代理組件,MOSN 在承載螞蟻數十萬服務容器之間流量的同時,也承載着諸多例如限流、鑑權、路由等中間件基礎能力。這些能力以不同的擴展形式與 MOSN 運行於同一進程內。非隔離的運行方式在保障性能的同時,卻也給 MOSN 帶來了不可預知的安全風險。
針對上述問題,我們採用 WebAssembly(Wasm) 技術,給 MOSN 實現了一個安全隔離的沙箱環境,讓擴展程序能夠運行在隔離沙箱之中,並對其資源、能力進行嚴格限制,使程序故障止步於沙箱,從而實現安全隔離的目標。本文將着重敘述 MOSN 中的 Wasm 擴展框架,並介紹我們在 Proxy-Wasm 這一開源規範上的 貢獻

總體設計


上圖爲 MOSN Wasm 擴展框架的整體示意圖。如圖所示,對於 MOSN 的任意擴展點(Codec、NetworkFilter、StreamFilter 等),用戶均能夠通過 Wasm 擴展框架,以隔離沙箱的形式運行自定義的擴展代碼。而 MOSN 與 Wasm 擴展代碼之間的交互,則是通過 Proxy-Wasm 標準 ABI 來完成的。

隔離沙箱

當我們在討論 Wasm 時,都明白 Wasm 能夠提供一個安全隔離的沙箱環境,但並不是每個人都瞭解 Wasm 實現隔離沙箱的技術原理。這時又要搬出計算機科學中的至理名言: “計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決”。Wasm 實際上也是通過引用一個“中間層”來實現的安全隔離。簡單來說,Wasm 通過一個運行時 (Runtime) 來運行 Wasm 沙箱擴展,每個 Wasm 沙箱都有其獨立的線性內存空間和一組導入/導出模塊。

一方面,每個 Wasm 沙箱都有其獨立的線性內存空間,其內存模型如上圖所示。Wasm 代碼只能通過簡單的 load/store 等指令訪問線性內存空間的有限部分,並通過符號(下標)的方式來間接訪問函數、全局變量等。上述限制杜絕了類似 C 語言中訪問任意內存地址的騷操作。同時,用於間接調用函數的符號表對於 Wasm 代碼而言是隻讀的,保證了 Wasm 代碼的執行是受控的。此外,Wasm 沙箱的整個線性內存空間由宿主機 (Wasm Runtime) 分配及管理,通過嚴格的內存管理保證沙箱的隔離性。

另一方面,Wasm 也規定了代碼中任何可能產生外部影響的操作只能通過導入/導出模塊來實現。當我們在編寫 C 語言源碼時,可以直接通過系統調用來訪問系統的環境變量、文件、網絡等資源。而在 Wasm 的世界中,並不存在系統調用相關的指令,任何對外部資源的訪問必須通過導入模塊來間接實現。以文件讀寫爲例,在 Wasm 中要想進行文件讀寫,需要宿主機提供實現文件讀寫功能的導入函數,Wasm 代碼調用該導入函數,由宿主機間接進行文件讀寫,再將操作結果返回給 Wasm 擴展。在上述過程中,實際的文件讀寫操作由宿主機完成,宿主機對這一過程有絕對的控制權,包括但不限於只允許讀寫指定文件、限制讀寫內容、完全禁止讀寫等。

擴展框架

MOSN 以 插件(Plugin) 的形式對 Wasm 擴展進行統一管理,插件是指一組 Wasm 沙箱實例及其配置的集合。用戶通過配置來加載、更新以及卸載 Wasm 插件,並通過配置來描述沙箱實例的運行規格(使用的執行引擎、Wasm 文件路徑、實例數量等)。下面展示了一個典型的 Wasm 插件配置:

當 MOSN 加載上述插件配置時,會按照以下流程生成插件對應的 Wasm 沙箱實例:

在後續運行的過程中,用戶通過 Wasm 擴展框架獲取指定插件的沙箱實例, 然後通過沙箱實例暴露的 API 與擴展程序進行交互。本文的下一小節將對此交互過程進行詳細描述。在 MOSN 中,Wasm 擴展框架與具體用途無關,在 MOSN 已有的任何一處擴展點,均可以直接使用 Wasm 框架來獲取安全隔離的插件執行能力。

    如下圖所示,Wasm 擴展框架主要分爲 Manager、VM 和 ABI 三個子模塊。其中

  • Manager 模塊負責對 Wasm 插件的配置進行統一管理,提供插件的增刪查改功能,並負責將用戶提供的插件配置渲染成一組的 Wasm 沙箱實例

  • VM 模塊提供對 Wasm Runtime(虛擬機) 的統一封裝,負責 .wasm 文件的編譯、執行,以及 Wasm 沙箱實例的資源管理

  • ABI 模塊則提供對外的使用接口,可以看作是 MOSN 與 Wasm 擴展代碼之間交互的膠水層

本文不再對框架的具體實現細節進行介紹,感興趣的讀者可以閱讀開源 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。

規範的實現需要宿主側和 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

本小節主要演示如何在 MOSN 中進行配置並運行 Wasm 擴展插件流程。演示所需的源文件參考 example。

在演示中,我們通過配置讓 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 源碼實現進行演示。

進入 example/wasm/httpCall 目錄,執行命令:
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。

執行以下命令將啓動上述 HTTP 服務器:
go run server.go

4. 請求驗證

上述操作準備就緒後,便可通過 Curl 來進行請求 驗證了。
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

總結

對於螞蟻而言,安全可信永遠是我們追求的目標,而面對越來越多的擴展場景,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 技術的出現仍然爲我們提供了一種啓發和希望,促使我們進一步思考如何在雲原生時代更好地踐行安全可信這一信條。


 延伸閱讀

本文分享自微信公衆號 - 金融級分佈式架構(Antfin_SOFA)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章