如何使用 WebAssembly 擴展後端應用

1.WebAssembly 簡介

爲了解決這些問題,Mozilla 的工程師 Alon Zakai 在 2012 年提出了 Asm.js。之後,經過幾年的發展,最終在 2015 年演變成了 WebAssembly。

WebAssembly(簡寫爲 Wasm)是一種用於堆棧式虛擬機的二進制指令格式。它的設計目的,是成爲其它編程語言的一個可移植的編譯目標,以便在 Web 上佈署客戶端和服務端應用。

這是 WebAssembly 官網上的定義,從這個定義中我們可以知道,WebAssembly 是一種二進制指令格式。但在日常的討論中,我們也常常把 WebAssembly Text Format 稱爲 WebAssembly,而這種文本格式實際上是一種編程語言。

正式發佈後,WebAssembly 迎來了迅猛的發展,到 2017 年 11 月,Mozilla 宣佈包括 Chrome、Firefox、Safari 等在內的所有主流瀏覽器都已經支持 WebAssembly,而根據 2021 年 7 月的數據,用戶正在使用的瀏覽器中,已經有 94% 支持了 WebAssembly。

在得到了瀏覽器的廣泛支持之後,一些重量級的應用也被逐漸被移植到了 Web 端,其中包括:

這些案例也說明 WebAssembly 達到了自己的設計目標——在 Web 上部署桌面上的原生應用。而 WebAssembly 能夠獲得如此快速的發展,得益於它的幾個特點:

2.WebAssembly 在後端的應用

在 WebAssembly 的官方定義中,“用於堆棧式虛擬機的”這個定語也非常值得關注,因爲它導致 WebAssembly 這項最初以 Web 端爲應用場景,以至於名字中都包含 “Web” 這個詞的技術,慢慢進入了後端應用的領域。

這是因爲,從早期的 VMWare WorkStation、VirtualBox,到今天的 Docker,虛擬化技術一直是雲計算的重要基礎。所以,WebAssembly 作爲一種具有很多特點的虛擬機代碼格式,進入後端應用領域是必然趨勢。Docker 的創始人 Solomon Hykes 在 2019 年說 “如果 2008 年就有 WASM 和 WASI,我們就不用發明 Docker 了”,對其在後端應用的前景之看好,可見一斑。

當然,Solon Hykes 後來也說他的意思不是 “WebAssembly 會取代 Docker”,這也是當今業界普遍的觀點:WebAssembly 和 Docker 各有優勢,互爲補充。具體來說:

下面,通過實際的示例介紹下如何在後端應用中使用 WebAssembly。

2.1.  在應用中嵌入 WebAssembly

如下圖所示,不管是 Web 應用還是非 Web 應用,要使用 WebAssembly,都需要在宿主程序中嵌入 WebAssembly 運行時引擎,區別只在於,在 Web 應用中,這個宿主程序是瀏覽器,而在非 Web 應用中,宿主程序是我們自己開發的,具體到本文關注的後端應用,宿主程序則是我們的後端服務程序。

目前可選的 WebAssembly 運行時引擎有 WasmtimeWasmEdgeWAVMWasmer 等很多種,各有自己的優勢和缺陷。本文將以 Wasmtime 爲例,介紹如何在以 Go 語言開發的宿主程序中嵌入 WebAssembly。

嵌入 WebAssembly 運行時引擎並實例化 WebAssembly 模塊本身非常簡單,在省略錯誤處理的情況下,只要下面幾行代碼即可:

其中涉及了實際開發中需要了解的幾個重要概念,簡單介紹如下:

雖然上面的代碼創建了 WebAssembly 模塊的實例,並且根據 WebAssembly 的規範,模塊實例化時就可以執行 WebAssembly 代碼,但由於安全性的限制,其執行結果無法對外輸出,所以這種 “執行” 毫無意義。因此,我們需要實現宿主程序和 WebAssembly 程序的互操作,爲 WebAssembly 程序提供輸入 / 輸出接口。

2.2.  宿主調用 WebAssembly

假設我們的 WebAssembly 程序中有一個名爲 sum 的函數,接收兩個整形變量作爲參數,返回它們的和,則宿主程序可以使用下面的代碼來調用這個函數:

雖然不同的宿主開發語言和 WebAssembly 運行時引擎具體的調用方式有區別,但運行時引擎的文檔一般都有相關說明,所以這一步照着文檔做就好,沒有難度。

這裏的難點在於,如何才能在 WebAssembly 程序中暴露出這個函數,以便宿主程序能找到並調用它。前面說過,只要有相應的編譯器,各種語言都可以編譯成 WebAssembly,但大多數語言設計時並沒有考慮 WebAssembly 的需要,也就沒有提供暴露函數的方法。所以這個問題只能通過特定編譯器的非標準擴展來解決。也就是說,找到這個非標準擴展是解決問題最關鍵的一步。但也正由於 “非標準”,所以相關的資料有時並不容易找到。

作爲示例,下面給出的是使用 C/C++(編譯器是 emscripten)和 AssemblyScript 時對外暴露函數的方法:

2.3.  WebAssembly 調用宿主

與宿主調用 WebAssembly 類似,運行時引擎的文檔中一般會介紹宿主端如何暴露一個函數給 WebAssembly 程序。

問題的難度同樣在於 WebAssembly 程序中如何使用語言的非標準擴展來導入這個函數,下面是 C/C++ 和 AssemblyScript 中的具體方法:

2.4.  複雜參數的傳遞

宿主和 WebAssembly 程序相互調用對方的函數時,也需要傳遞參數和返回值,如果是整數等簡單數據類型,直接傳遞即可。但當數據類型是字符串等複雜類型時,就會遇到新的問題,具體有兩點:

因爲宿主程序可以訪問 WebAssembly 的內存,所以第二個問題的解決方法是 WebAssembly 程序暴露內存管理的相關函數,讓宿主來操作 WebAssembly 的內存,例如:

之後,我們可以藉助這些內存管理函數,並通過序列化 / 反序列化相關數據類型來傳遞它們,比如在下面 WebAssembly 調用宿主函數的代碼中,參數和返回值本來各是一個字符串,但通過序列化 / 反序列化之後,只要傳遞它們在 WebAssembly 內存中的地址(整數)即可:

2.5.  SDK

看了上面宿主程序和 WebAssembly 程序互操作的過程,相信大家已經發現它和 RPC 調用的過程非常像。不同的是,在 RPC 調用中,一系列非常繁瑣的序列化 / 反序列化操作,都由工具自動生成代碼實現,使用者根本無需關心。

而在 WebAssembly 應用的開發中,用戶也不希望每次都處理那麼多的細節。所以,作爲宿主程序的開發者,我們需要爲用戶提供相關 SDK,屏蔽掉底層細節,讓用戶能夠專注於業務邏輯的開發。

由於用戶可以使用多種語言開發 WebAssembly 應用,所以我們需要提供針對不同語言的 SDK,或者至少也需要覆蓋目標用戶使用的主流語言。

2.6.  錯誤處理

像普通程序一樣,WebAssembly 程序也會有各種錯誤,雖然作爲宿主程序的開發者,我們無法預知具體的錯誤,但我們必須把這些錯誤的影響範圍限制在 WebAssembly 虛擬機內部,不能讓它們影響宿主程序的功能。

宿主程序要防範的第一類錯誤是 WebAssembly 程序中的死循環。實際場景中,其實宿主程序並沒有辦法判斷是否真的出現了死循環,所以,折衷的解決辦法是給 WebAssembly 程序設置一個最大運行時間,一旦超過了這個時間還沒有結束,就認爲裏面出現了死循環,並終止它的執行。終止執行的示例代碼如下:

要防範的第二類錯誤是 WebAssembly 程序中的非法內存訪問等導致的崩潰,WebAssembly 運行時引擎一般會將這類錯誤轉換爲宿主程序中的異常(在 Go 語言中是一個 panic),我們只要處理這個異常即可。由於宿主程序並不知道具體的錯誤原因,也不知道 WebAssembly 程序出現錯誤後的具體狀態,所以宿主程序能進行的處理一般只能是釋放出問題的 WebAssembly 虛擬機佔用的資源並重新創建一個虛擬機來替代它。

  1. Easegress 中的 WebAssembly ===========================

EasegressMegaEase 開發的下一代流量型網關,具有云原生、高可用、可觀測、可擴展等特點。在 Easegress 之前,市場上已經有包括 nginx 在內的多個成熟網關產品。但 MegaEase 認爲,流量調度型網關並不僅僅是一個反向代理,還需要能夠動態的進行流量編排和調度。此外,在具體應用場景中,還涉及各種各樣的業務邏輯侵入,因此,必須高度可擴展。

基於以上觀點,Easegress 從誕生之日就將可擴展性放到了重要位置,並在多個層面進行了針對性的設計。

首先,在開發語言的選擇上,我們有 C/C++/Java/Rust/Go 這些主流的靜態語言的選項

第二,通過外部調用的方式擴展,如 FaaS。通過雲原生的 FaaS 方式的好處是不限制用戶的開發語言,而且可以讓整個架構具有很好的可伸縮性,但其缺點是需要引用 Kubernetes 等的比較重的外部依賴,導致整個運維非常複雜(注:Easegress 目前已經支持了 Knative)

第三,通過嵌入其它語言的解釋器進行擴展。這方面,Lua 本身就是爲嵌入其它程序而設計的,具有相應的優勢。但我們認爲 Lua 也有兩個缺點,一是本身表現力不夠,不適合寫複雜的業務邏輯;二是太小衆,不是主流語言,有相關經驗的程序員太少。所以,經過權衡,Easegress 最終選擇了嵌入 WebAssembly,主要基於兩點考慮:一是接近於原生代碼的高性能;二是不限制用戶的開發語言,用戶可以使用自己喜歡或熟悉的語言開發業務邏輯。

作爲使用 WebAssembly 擴展業務邏輯的樣例,我們之前已經發布了《使用 Easegress + WebAssembly 做秒殺》,歡迎大家閱讀並向我們反饋更多的實際案例。

當然,選擇了 WebAssembly 意味着我們需要開發多種語言的 SDK,目前已經完成了 AssemblyScript SDK 的開發,相信在 MegaEase 和整個開源社區的共同努力下,我們支持的語言會越來越多。歡迎大家關注我們的開源社區:https://github.com/megaease/

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