本文將介紹 WebAssembly 技術(shù)在 MOSN 中的實踐,首先介紹了當前 MOSN 在擴展隔離方面所面臨的痛點,并對 Wasm 技術(shù)的相關(guān)背景知識進行介紹。隨后描述了 Wasm 擴展框架的整體架構(gòu),并介紹了我們在 Proxy-Wasm 社區(qū)規(guī)范中所做的貢獻,最后描述了框架在性能、異常調(diào)試等方面的實踐內(nèi)容。 作為金融級服務網(wǎng)格中的流量代理組件,MOSN 在承載螞蟻數(shù)十萬服務容器之間流量的同時,也承載著諸多例如限流、鑒權(quán)、路由等中間件基礎能力。這些能力以不同的擴展形式與 MOSN 運行于同一進程內(nèi)。非隔離的運行方式在保障性能的同時,卻也給 MOSN 帶來了不可預知的安全風險。針對上述問題,我們采用 WebAssembly(Wasm) 技術(shù),給 MOSN 實現(xiàn)了一個安全隔離的沙箱環(huán)境,讓擴展程序能夠運行在隔離沙箱之中,并對其資源、能力進行嚴格限制,使程序故障止步于沙箱,從而實現(xiàn)安全隔離的目標。本文將著重敘述 MOSN 中的 Wasm 擴展框架,并介紹我們在 Proxy-Wasm 這一開源規(guī)范上的貢獻。 總體設計 上圖為 MOSN Wasm 擴展框架的整體示意圖。如圖所示,對于 MOSN 的任意擴展點 (Codec、NetworkFilter、StreamFilter 等),用戶均能夠通過 Wasm 擴展框架,以隔離沙箱的形式運行自定義的擴展代碼。而 MOSN 與 Wasm 擴展代碼之間的交互,則是通過 Proxy-Wasm 標準 ABI 來完成的。 隔離沙箱 當我們在討論 Wasm 時,都明白 Wasm 能夠提供一個安全隔離的沙箱環(huán)境,但并不是每個人都了解 Wasm 實現(xiàn)隔離沙箱的技術(shù)原理。這時又要搬出計算機科學中的至理名言: “計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決”。Wasm 實際上也是通過引用一個“中間層”來實現(xiàn)的安全隔離。簡單來說,Wasm 通過一個運行時 (Runtime) 來運行 Wasm 沙箱擴展,每個 Wasm 沙箱都有其獨立的線性內(nèi)存空間和一組導入 / 導出模塊。 一方面,每個 Wasm 沙箱都有其獨立的線性內(nèi)存空間,其內(nèi)存模型如上圖所示。Wasm 代碼只能通過簡單的 load/store 等指令訪問線性內(nèi)存空間的有限部分,并通過符號 (下標) 的方式來間接訪問函數(shù)、全局變量等。上述限制杜絕了類似 C 語言中訪問任意內(nèi)存地址的騷操作。同時,用于間接調(diào)用函數(shù)的符號表對于 Wasm 代碼而言是只讀的,保證了 Wasm 代碼的執(zhí)行是受控的。此外,Wasm 沙箱的整個線性內(nèi)存空間由宿主機 (Wasm Runtime) 分配及管理,通過嚴格的內(nèi)存管理保證沙箱的隔離性。 另一方面,Wasm 也規(guī)定了代碼中任何可能產(chǎn)生外部影響的操作只能通過導入 / 導出模塊來實現(xiàn)。當我們在編寫 C 語言源碼時,可以直接通過系統(tǒng)調(diào)用來訪問系統(tǒng)的環(huán)境變量、文件、網(wǎng)絡等資源。而在 Wasm 的世界中,并不存在系統(tǒng)調(diào)用相關(guān)的指令,任何對外部資源的訪問必須通過導入模塊來間接實現(xiàn)。以文件讀寫為例,在 Wasm 中要想進行文件讀寫,需要宿主機提供實現(xiàn)文件讀寫功能的導入函數(shù),Wasm 代碼調(diào)用該導入函數(shù),由宿主機間接進行文件讀寫,再將操作結(jié)果返回給 Wasm 擴展。在上述過程中,實際的文件讀寫操作由宿主機完成,宿主機對這一過程有絕對的控制權(quán),包括但不限于只允許讀寫指定文件、限制讀寫內(nèi)容、完全禁止讀寫等。 擴展框架 MOSN 以 插件 (Plugin) 的形式對 Wasm 擴展進行統(tǒng)一管理,插件是指一組 Wasm 沙箱實例及其配置的集合。用戶通過配置來加載、更新以及卸載 Wasm 插件,并通過配置來描述沙箱實例的運行規(guī)格 (使用的執(zhí)行引擎、Wasm 文件路徑、實例數(shù)量等)。下面展示了一個典型的 Wasm 插件配置: 當 MOSN 加載上述插件配置時,會按照以下流程生成插件對應的 Wasm 沙箱實例: 在后續(xù)運行的過程中,用戶通過 Wasm 擴展框架獲取指定插件的沙箱實例, 然后通過沙箱實例暴露的 API 與擴展程序進行交互。本文的下一小節(jié)將對此交互過程進行詳細描述。在 MOSN 中,Wasm 擴展框架與具體用途無關(guān),在 MOSN 已有的任何一處擴展點,均可以直接使用 Wasm 框架來獲取安全隔離的插件執(zhí)行能力。 如下圖所示,Wasm 擴展框架主要分為 Manager、VM 和 ABI 三個子模塊。其中
本文不再對框架的具體實現(xiàn)細節(jié)進行介紹,感興趣的讀者可以閱讀開源 PR 文檔了解細節(jié)。 由于當前市面上幾乎不存在使用 Go 語言直接編寫的 Wasm Runtime,因此 MOSN 只能通過 CGO 調(diào)用的方式來間接地調(diào)用由 C++/Rust 編寫的 Wasm 執(zhí)行引擎。我們從 SDK 完善程度、性能、項目活躍度等角度綜合考慮,經(jīng)過一系列橫向?qū)Ρ戎?,選擇了 Wasmer 作為 MOSN 默認的執(zhí)行引擎。 Proxy-Wasm ABI 規(guī)范 本小節(jié)將介紹 MOSN 具體是如何跟 Wasm 擴展程序進行交互的。先說結(jié)論: MOSN 跟 Wasm 擴展代碼之間的交互采用的是社區(qū)規(guī)范: Proxy-Wasm。 Proxy-Wasm 是開源社區(qū)針對「網(wǎng)絡代理場景」設計的一套 ABI 規(guī)范,屬于當前的事實規(guī)范。當前支持該規(guī)范的網(wǎng)絡代理軟件包括 Envoy、MOSN 和 ATS(Apache Traffic Server),支持該規(guī)范的 Wasm 擴展 SDK 包括 C++、Rust 和 Go。采用該規(guī)范的好處在于能讓 MOSN 復用社區(qū)既有的 Wasm 擴展 (包括 Go 實現(xiàn)以及 C++/Rust 實現(xiàn)),也能讓原本專門為 MOSN 開發(fā)的 Wasm 擴展運行在 Envoy 等網(wǎng)絡代理產(chǎn)品上。Proxy-Wasm 規(guī)范定義了宿主機與 Wasm 擴展程序之間的交互細節(jié),包括 API 列表、函數(shù)調(diào)用規(guī)范以及數(shù)據(jù)傳輸規(guī)范這幾個方面。其中,API 列表包含了 L4/L7、property、metrics、日志等方面的擴展點,涵蓋了網(wǎng)絡代理場景下所需的大部分交互點,且可以劃分為宿主側(cè)擴展和 Wasm 側(cè)擴展。這里簡單展示規(guī)范中的部分內(nèi)容,完整內(nèi)容請參考 spec。 規(guī)范的實現(xiàn)需要宿主側(cè)和 Wasm 側(cè)兩邊配合才能正常工作。對于 Wasm 側(cè),社區(qū)已經(jīng)有 C++、Rust 和 Go 三種語言實現(xiàn)的 SDK,用戶可以直接使用這些 SDK 來編寫與宿主無關(guān)的 Wasm 擴展程序。而對于宿主側(cè),社區(qū)只提供了 C++ 和 Rust 的宿主側(cè)實現(xiàn)。為此,我們在項目中使用 Go 語言對 Proxy-Wasm 規(guī)范的宿主側(cè)進行了實現(xiàn),并將其貢獻給開源社區(qū),使之成為社區(qū)推薦的 Go-Host 實現(xiàn) (如下圖所示)。需要強調(diào)的是,宿主側(cè)實現(xiàn)并不依賴具體的網(wǎng)絡代理程序,理論上任何直接通過 Host 程序與 Wasm 擴展進行交互。 我們以 HTTP 場景為例,介紹在 MOSN 中是如何通過 Proxy-Wasm 規(guī)范來與 Wasm 擴展程序進行交互,處理 HTTP 請求的。
上述示例中,我們并不限制 Wasm 側(cè)的語言實現(xiàn),用戶可以使用 C++/Rust/Go 幾種語言來編寫自定義的擴展代碼。與之相對的,只需要用相應語言的 Proxy-Wasm-SDK 一起編譯成 .wasm 文件,即可運行在 MOSN 之上。 工程實踐 Quick Start 本小節(jié)主要演示如何在 MOSN 中進行配置并運行 Wasm 擴展插件流程。演示所需的源文件參考 example。在演示中,我們通過配置讓 Wasm 擴展插件來處理 MOSN 接收的 HTTP 請求,MOSN 的監(jiān)聽端口為 2045。在 Wasm 處理請求的源碼中,我們通過 Proxy-Wasm 規(guī)范中的 proxy_dispatch_http_call 接口向外部 HTTP 服務器發(fā)起請求,Wasm 源碼內(nèi)指定外部 HTTP 服務器的監(jiān)聽端口為 2046。演示場景的流程如下圖所示: 該演示流程主要分為以下步驟:
我們在示例工程中提供了 C 和 Go 兩種語言實現(xiàn)的 Wasm 擴展源碼,對 Proxy-Wasm 規(guī)范的采用使得我們能夠利用多種語言 (C++/Rust/Go) 來編寫 Wasm 擴展代碼。出于編譯的便利性,這里使用 Go 源碼實現(xiàn)進行演示。 進入 example/wasm/httpCall 目錄,執(zhí)行命令: 上述操作會將目錄下的 filter-go.go 源碼文件編譯成 filter-go.wasm 文件
示例工程提供了一份加載 filter-go.wasm 擴展文件的配置,通過以下命令即可啟動: 上述命令中使用的 MOSN 可執(zhí)行程序可以通過以下命令由源碼構(gòu)建:
該示例工程中,Wasm 擴展源碼會通過 MOSN 向外部 HTTP 服務器發(fā)起請求,請求的 URL 為: http://127.0.0.1:2046/ 為此,示例工程也提供了一段 HTTP 服務器代碼,當其收到 HTTP 請求時,均會返回響應頭: from: external http server,返回響應體: response body from external http server。 執(zhí)行以下命令將啟動上述 HTTP 服務器:
執(zhí)行上述命令后,MOSN 終端將能夠觀察到以下日志: 性能測試 測試環(huán)境:
測試場景:
測試數(shù)據(jù): 「native」表示添加 Header 的操作使用 MOSN 原生的 Stream Filter 完成; 「wasm」表示添加 Header 的操作使用 Wasm 擴展完成
壓測命令: sofaload --h1 -c 100 -t 4 --qps=2000 -D 30 http://127.0.0.1:2045/圖片
異常調(diào)試 對于實際的工程項目而言,光能運行是不夠的,必須具備一定的問題排查和定位能力,才能在遇到程序故障時,解析異常源碼的調(diào)用堆棧,快速定位第一現(xiàn)場,從而提高開發(fā)及調(diào)試的效率。由于 Wasm 本身的定位是與編程語言無關(guān)的字節(jié)碼規(guī)范,不同語言的源代碼 (C++/Go/Java 等) 均能夠編譯為統(tǒng)一的 Wasm 字節(jié)碼,因此如何屏蔽具體編程語言的細節(jié)模型,制定語言無關(guān)的調(diào)試信息規(guī)范,是社區(qū)需要解決的難題之一。 針對這一問題,在當前的工程實踐中,Java 語言采用的是 Source Map 格式,而 C++、Rust 和 Go 語言采用的是 Dwarf 格式的調(diào)試信息。對具體調(diào)試信息格式的介紹并不在本文的范圍之內(nèi),讀者可自行參考外部文章。這里需要強調(diào)的是,對于 Wasm 而言,還需要對調(diào)試信息的格式進行一定的擴展,才能滿足實際的應用需要。與其他編程語言不同的是,.wasm 文件是能夠被轉(zhuǎn)換成 .wat 格式,并手動編輯內(nèi)容的,編譯好的 .wasm 文件仍然有修改段內(nèi)容的可能。為了適應這種場景,Wasm 調(diào)試規(guī)范對 Dwarf 格式中的位置信息編碼進行了調(diào)整,指令的偏移值被設置成基于 Code 段的偏移: With WebAssembly, the .debug_line section maps Code section-relative instruction offsets to source locations. 為此,我們在解析指令偏移時,需要偏移數(shù)值進行調(diào)整,減去 Code 段的偏移量,才能得到 Wasm 指令的實際偏移值,進而利用 .debug_line 段定位到準確的源碼行。下圖展示了利用 MOSN 輸出的錯誤日志定位 Wasm 故障源碼行的示例。
總 結(jié) 對于螞蟻而言,安全可信永遠是我們追求的目標,而面對越來越多的擴展場景,MOSN 需要一個安全可靠的隔離環(huán)境,以避免擴展代碼給 MOSN 運行造成的安全風險。為此,我們采用 WebAssembly 技術(shù),為 MOSN 實現(xiàn)了一個基于 Wasm 隔離沙箱的插件擴展框架。MOSN 采用網(wǎng)絡代理社區(qū)中的 Proxy-Wasm 規(guī)范,實現(xiàn)了語言無關(guān)、宿主無關(guān)的網(wǎng)絡代理擴展能力。同時,我們也向開源社區(qū)貢獻了 Proxy-Wasm-Go-Host 實現(xiàn),積極融入開源社區(qū)。 需要注意的是,當前 WebAssembly 技術(shù)仍處于發(fā)展階段,Go 語言自身對 WebAssenbly 生態(tài)的支持仍有巨大的提升空間。我們在實踐的過程中,也總是面臨 Go 語言在 Wasm 生態(tài)中不夠給力的情況。由于 Go 官方編譯器還不支持將 Go 源碼程序編譯成 WASI 系統(tǒng)接口 (GOOS=wasi) 的 .wasm 文件,我們不得不借助 TinyGo 來完成 Go 擴展程序的編譯,而這也導致我們需要面對 TinyGo 在語言特性支持程度、性能、穩(wěn)定性等方面不足的痛點。與之相比,C++/Rust 對 Wasm 生態(tài)的支持程度就要完善得多。 總而言之,WebAssembly 技術(shù)的出現(xiàn)仍然為我們提供了一種啟發(fā)和希望,促使我們進一步思考如何在云原生時代更好地踐行安全可信這一信條。 活動推薦: 云原生架構(gòu)使得創(chuàng)新越來越簡單,業(yè)務聚焦在業(yè)務核心價值的創(chuàng)新使得創(chuàng)新迭代效率得到大幅提升。而業(yè)務迭代的極致簡單也帶來系統(tǒng)架構(gòu)的極致復雜。4 月 25-26 日在全球架構(gòu)師上海站,「云原生時代的智能系統(tǒng)架構(gòu)」專題將邀請信也科技、PingCAP、阿里巴巴的技術(shù)專家,結(jié)合各家業(yè)務實踐以及個人實際工作經(jīng)驗,探討技術(shù)負責人構(gòu)建智能架構(gòu)時可能面對的各種權(quán)衡和選擇,以及遇到的挑戰(zhàn)和教訓。 |
|