Shopify 致力于讓大多數(shù)商家都需要的功能變得簡單易用,并通過接口在 Shopify 平臺上執(zhí)行查詢、擴展和更改,進而為商家提供更多可能。借助這些接口,我們豐富的合作伙伴生態(tài)系統(tǒng)可以解決諸多問題。這一生態(tài)系統(tǒng)主要借助“App”(一個獨立托管的 Web 服務)來運作。該 App 通過網(wǎng)絡與 Shopify 進行通信。盡管這種模式很強大,但會帶來一系列技術問題。我們的合作伙伴需要打造能夠隨 Shopify 規(guī)模擴展的 Web 服務,這讓一些本就資源有限的合作伙伴越發(fā)捉襟見肘。即便合作伙伴有無限的資源,在與 Shopify 通信時產(chǎn)生的網(wǎng)絡延遲也足以讓我們的 App 在對時效性要求很高的用例中敗下陣來。
我們希望我們的合作伙伴能夠專注于利用他們的專長來解決問題,而不用花費時間管理可擴展的 Web 服務。為實現(xiàn)這一目標,我們保留了不受信任的合作伙伴代碼的靈活性,并將其在我們的基礎設施上運行。為了確保這些代碼的性能、安全性與靈活性,我們選擇了 WebAssembly 這種通用格式。
WebAssembly
什么是 WebAssembly?WebAssembly.org 給出了如下定義:
”WebAssembly(縮寫為 Wasm)是一種基于堆棧虛擬機的二進制指令格式。Wasm 是為編程語言設計的可移植編譯目標,使客戶端和服務器應用程序能夠在 Web 上部署?!?/p>
如需詳細了解 WebAssembly 及其歷史,可以瀏覽由 Mozilla 的 Lin Clark 撰寫的這篇圖文并茂的文章。本文在此不做詳述。
Wasm 通常都是與 JavaScript 一起在瀏覽器內運行,但 Shopify 卻另辟蹊徑,在瀏覽器之外運行 Wasm,并且不用到 JavaScirpt。作為一款高性能語言,Wasm 絕非 JavaScript 的單純替代品:它面向 Web 和非 Web 的嵌入而設計,解決了廣泛存在于瀏覽器和代碼執(zhí)行引擎中的一個難題,即如何在不受信任的環(huán)境中高效執(zhí)行程序。Wasm 滿足了我們的三大主要技術需求:安全性、性能和靈活性。
安全性
運行不受信任的代碼具有極大的風險。從本質上來講,這些代碼不僅難以預測,并且還很有可能對整個 Shopify 平臺造成損害。盡管市面上并沒有百分之百安全可靠的應用,但我們還是要防范安全漏洞,并且在出現(xiàn)問題后采取措施來減輕其影響。
Wasm 將代碼執(zhí)行放到了一個基于堆棧的沙箱環(huán)境中,依靠顯式導入來與主機進行通信。因此,我們無法在 Wasm 中寫入任何惡意代碼,只能使用提供的輸入端口操作虛擬環(huán)境。在這一點上 Wasm 與字節(jié)碼有所不同,字節(jié)碼在語法中直接引用了它們希望在其中運行的計算機或操作系統(tǒng)。
Wasm 還有很多不同的功能,可讓用戶免受錯誤代碼的影響,包括受保護地調用堆棧和運行時類型檢查。WebAssembly.org 上提供了更多關于 Wasm 安全模型的詳細資料。
性能
在電商領域,較快的運行速度才是商家推動業(yè)績增長時必備的利器。如果 Shopify 提供的功能無法兼顧加載時間和定制價值,那么這種功能壓根就沒有任何價值可言。
Wasm 本身的設計充分利用了常見的硬件功能,并在各種平臺上發(fā)揮出最接近原生的性能。它面向追求最高性能、優(yōu)化瀏覽器執(zhí)行的開發(fā)者社區(qū)。因此,無論是現(xiàn)在還是未來,Wasm 和它的周邊工具在設計上都會以性能優(yōu)化為中心。
靈活性
能幫助開發(fā)者提高開發(fā)效率的代碼執(zhí)行服務才是真正有用的服務。Wasm 作為一款字節(jié)碼格式,與多種編譯器相兼容,為代碼開發(fā)者提供了支持多種編程語言的一流開發(fā)體驗。這也讓我們能夠在不改變底層執(zhí)行模型的情況下,提供多語言支持。
基于社區(qū)
Shopyify 在發(fā)展目標和設計方面基本保持一致,這為我們選擇 Wasm 提供了技術上的理由,但事實并不僅限于此:我們對 Wasm 的選擇不僅關乎于技術,更關乎于人。如果 Wasm 生態(tài)系統(tǒng)無人問津,或者它僅在生死線上垂死掙扎,那么我們不會選擇它。WebAssembly 的社區(qū)是個充滿活力的社區(qū),不斷創(chuàng)新,它的潛力是無窮的。自從加入這個充滿熱情的社區(qū),Shopify 就獲益匪淺。
同樣,我們也在為社區(qū)貢獻出我們的力量。通過收集用戶反饋,探討功能缺陷,以及為我們使用的開源工具提交代碼貢獻。我們認為,這為我們與 WebAssembly 社區(qū)之間建立良好的互惠合作關系打下了堅實基礎,我們也期望著在未來能夠繼續(xù)為這個鮮活的社區(qū)獻出我們的力量。
代碼執(zhí)行服務的架構
在簡單介紹過 WebAssembly 以及我們選擇它的原因后,下一步就來深入探討我們的運行方案。
我們使用的是最初由 Fastly 開發(fā)的開源工具 Lucet。Fastly 這家公司為大批量壽命不長且不受信任的模塊提供了一個可編程的邊緣云平臺,讓它們可以在盡可能接近發(fā)起請求的地方執(zhí)行請求。這與我們的合作方提供的代碼所面臨的問題相同,因此,我們自然而然就選擇了 Lucet。
Lucet
Lucet 是 Wasm 的運行時和編譯器。Wasm 中的模塊確保了系統(tǒng)的安全性,由于我們無法在 Wasm 中寫入惡意代碼,因此 Lucet 利用 Wasm 模塊的驗證進行安全檢查。在驗證之后,模塊會被編譯為一個可執(zhí)行的文件,其性能可以達到原生狀態(tài)。另外,Wasm 還支持提前編譯,可避免執(zhí)行運行時編譯帶來的延遲。Lucet 容器在啟動時無需執(zhí)行任何操作,這讓它擁有了令人驚嘆的 35μs 啟動時間。如果您對 Lucet 及其工作原理感興趣,可以去看看 Fastly 的 CTO Tyler McMullan 的演講視頻。
Shopify 中 Wasm 引擎的工作原理流程圖
我們將 Lucet 包裝在一個管理 I/O 和模塊存儲的 Rust Web 服務里,并將其稱作是 Wasm 引擎。在運行時,Shopify 通過 Web 請求調用 Wasm 引擎以處理部分功能。引擎之后再調用站點的上下文中應用輸出,這里的上下文可能會涉及到創(chuàng)建折扣、執(zhí)行約束,或者是任何商家想要在平臺中私人定制的同步服務。
運行性能
下圖中是我們在最近一次的性能測試中提取到的一些指標。我們選擇了一個很小的功能及逆行測試:讓模塊對用戶購物車中添加的物品數(shù)量進行限制。在測試期間,每分鐘執(zhí)行十萬個模塊,持續(xù)時間約 5 分鐘。
模塊執(zhí)行所需時間
該圖表展示了執(zhí)行一個模塊所需時間的詳細情況,其中包括容器的 I/O 和模塊的執(zhí)行。y 軸代表時間(單位:ms),x 軸代表測試運行的具體時間。
圖中的淺紫色圖例代表 Lucet 中執(zhí)行模塊需要的時間,其寬度大約在 100μs 左右徘徊。其余圖標則是 I/O 的處理和引擎的具體情況,可以看出執(zhí)行的全部時間大約在 4ms 左右。所有的時間顯示都是第 99 位百分比(99p)。為了能更好地理解圖中時間的含義,下面讓我們將用 Shopify 中性能卓越的在線商店渲染服務:Storefront Renderer 的測試請求時間做比較。
Storefront Renderer 響應時間
這張圖表中展示了 Storefront Renderer 在一段時間內的請求時間。y 軸代表請求時間(單位:秒),x 軸代表返回數(shù)值時的具體時間。淺藍色線條代表在 700 毫秒左右的第 99 百分比。
如果將模塊處理時間大致估算在 5 毫秒內,那么可以說 Lucet 執(zhí)行時間帶來的性能影響幾乎可以忽略不計。
生成 WebAssembly
為了讓我們性能卓越的執(zhí)行引擎發(fā)揮作用,我們還需要授權開發(fā)者創(chuàng)建兼容的 Wasm 模塊。Wasm 的作用并不是讓用戶親自編寫(想寫當然是可以寫的)代碼,而是作為一個編譯目標存在。這就會讓我們思考以下問題:我們支持哪些編程語言,具體又要支持到什么程度。
理論上來說,任何有 Wasm 支持的開發(fā)語言都是可以的。但是,我們更希望開發(fā)者可以將精力集中在為商家解決問題上,而不是研究要如何符合我們的 API。這也是我們選擇單一語言 Ruby 支持,并為開發(fā)者提供快速啟動工具的原因。然而,由于 Ruby 動態(tài)語言的特性,我們并不能將其直接編譯為 Wasm,而涉及編譯解釋器的解決方案會有嚴苛的性能懲罰。正因如此,我們最終決定采用靜態(tài)編譯的語言,并將動態(tài)語言編譯的可能性留待未來。
通過我們的調研發(fā)現(xiàn),Shopify 生態(tài)系統(tǒng)中的開發(fā)者大多能對 JavaScript 熟練應用??上У氖?,由于 JavaScript 與 Ruby 一樣是動態(tài)語言,只得被排除在外。最終,我們選擇了一種語法類似于 TypeScript 的開發(fā)語言:AssemblyScript。
使用 AssemblyScript
雖然 WebAssembly 支持大量開發(fā)語言,但其中有兩大類編譯器是我們無法使用的:
生成環(huán)境或開發(fā)語言特定產(chǎn)物的編譯器,即節(jié)點或瀏覽器。(例如 Asterius、Blazor)
只適用于特定運行時的編譯器。這些編譯器生成的模塊依賴于特定語言的特定導入,通常是為了支持某些特定語言的標準庫,讓他們能夠在系統(tǒng)調用或運行時功能可用而存在的。因為我們并不想被鎖死在某一特定語言上,所以這類編譯器就不在我們的考慮范圍內了。(例如 Lumen)
這些功能強大的編譯器在其他情況下或許能夠發(fā)揮奇效,但可惜無法為我們所用。我們需要能夠生成 WebAssembly 的工具,而不是由 WebAssembly 支持的工具。AssemblyScript 便是被我們選中的工具。
與 WebAssembly 中的其他工具一樣,AssemblyScript 還在開發(fā)過程中。它缺乏一些諸如閉包支持等關鍵功能,在邊緣情況下仍會報錯。這時候就顯現(xiàn)出了社區(qū)的重要性。
開發(fā)語言 AssemblyScript 和它的周邊工具擁有一個用戶活躍的愛好者和維護者社區(qū),自從 2019 年 Shopify 首次使用 AssemblyScript 以來,他們就一直在支持著我們。而我們也通過 OpenCollective 長期貢獻代碼以支持社區(qū)。我們編寫完成了一個語言服務器,在實現(xiàn)閉包方面也取得里一些進展,也為編譯器和周邊工具提供了錯誤修復。
我們還將 AssemblyScript 融入了我們早期的工具之中。在 Shopify CLI 中,我們通過集成 AssemblyScript,允許開發(fā)者通過命令行創(chuàng)建、測試和部署模塊。為了提高開發(fā)效率,我們提供了可以處理 Shopify 定義對象(例如“Money”)底層實現(xiàn)的 SDK。除了這些工具,我們還搭建了一個允許合作伙伴監(jiān)控模塊的系統(tǒng),方便他們在模塊出現(xiàn)故障時收到警報。我們的最終目標是讓合作伙伴們能夠在不失去代碼在原生平臺上靈活性和可觀察性的前提下,將他們的代碼遷移到我們的平臺之上。