做者 | 易立 來源 | 阿里巴巴雲原生公衆號git
雲計算已經成爲了支撐數字經濟發展的關鍵基礎設施。雲計算基礎設施也在持續進化,從 IaaS,到容器即服務(CaaS),再到 Serverless 容器和函數 PaaS (fPaaS 或者 FaaS),新的計算形態相繼出現。以容器和 Serverless 爲表明的雲原生技術正在重塑整個應用生命週期。github
在 Gartner 分析報告中,雲計算基礎設施的發展路徑,也是雲原生特質逐漸加強的過程。其具體表如今:正則表達式
-
模塊化愈來愈高- 更加細粒度的計算單元,如容器和 Serverless 函數,更加適於微服務架構的應用交付,能夠更加充分利用雲的能力,提高架構敏捷性。redis
-
可編程性愈來愈高- 能夠經過聲明式 API 和策略進行實現自動化管理與運維,能夠經過 Immutable Infrastructure (不可變基礎設施)進一步提高分佈式應用運維的肯定性。docker
-
彈性效率愈來愈高- VM 能夠實現分鐘級擴容;容器與 Serverless 容器能夠實現秒級擴容;藉助調度優化,函數能夠作到毫秒級擴容。數據庫
-
韌性愈來愈高- Kubernetes 提供了強大自動化編排能力,提高應用系統自愈性。而 Serverless 進一步將穩定性、可伸縮性和安全等系統級別複雜性下沉到基礎設施,開發者只需關注自身業務應用邏輯,進一步釋放了生產力,提高系統的可恢復能力。編程
分佈式雲則是雲計算髮展的另一個重要趨勢,公有云的服務能夠拓展到不一樣的物理位置,讓計算進一步貼近客戶。分佈式雲讓客戶享受雲計算的便利的同時,也能夠知足對計算實時性和安全合規的訴求。這也推進了企業應用架構的變化 - 應用要可以在不一樣的環境進行部署、遷移,以最優化的方式提供服務。json
進一步隨着移動互聯網,AI 與 IoT 等新技術的涌現,無處不在的計算已經成爲現實。與此同時,這也在催生算力的多樣性,X86 架構一統天下的時代已通過去,ARM/RISC-V 等芯片新勢力不但稱雄移動通訊和嵌入式設備領域,也在向邊緣計算和數據中心市場發起進攻。開發者甚至須要讓應用支持不一樣的 CPU 體系架構,好比咱們能夠將一個圖像識別應用部署在邊緣或者 IoT 等不一樣環境、不一樣體系架構的設備之上運行。後端
在分佈式雲、邊緣計算、雲端一體等新的雲計算場景下,下一代雲原生應用運行時將具有什麼樣的特色?設計模式
下一代雲原生應用運行時
1. 無處不在的計算催生下一代可移植、高性能、輕量化的安全沙箱
容器應用採用自包含的打包方式 -- 容器鏡像,它包含了應用代碼和依賴的系統組件,能夠實現應用與基礎設施解耦,讓應用能夠在公共雲、專有云等不一樣的運行環境以一致的方式進行部署、運維,簡化了彈性和遷移。此外 Docker 鏡像規範支持多架構(Multi-Arch)鏡像,能夠簡化不一樣 CPU 體系架構(如 x86, ARM 等)的應用鏡像的構建與分發。
函數應用只包含用於事件響應的代碼包,這將應用交付格式從原生二進制文件提高到了高級語言層面。這也給應用的可移植性帶來了更大的想象空間,理論上甚至能夠屏蔽執行環境 CPU 體系架構的差別。好比對於不依賴本地代碼的 Python/NodeJS 等腳本或者 Java 應用,無需修改就能夠在 x86 或者 ARM 等不一樣 CPU 架構上運行。
然而理想很豐滿,現實很骨感,可移植性和廠商鎖定是函數 PaaS 發展的攔路虎。
-
不少腳本代碼依然須要經過調用原生代碼來實現數據處理和調用中間件(如數據庫驅動),可是編譯原生代碼須要構建環境與目標執行環境一致才能保障兼容性。好比 AWS Lambda / 阿里雲函數計算都要求二進制原生代碼依賴指定的內核和 libc 版本。所以,愈來愈多的函數 PaaS 服務支持容器鏡像做爲載體,來簡化函數應用打包和依賴管理。
-
函數應用一般依賴後端服務(BaaS, Backend as a Service)實現數據訪問與計算處理等能力,因爲 BaaS 不存在任何標準,這樣很難將在 AWS Lambda 上開發的函數應用移植到阿里雲的函數計算服務。
在 Serverless 計算中,現有的主流技術是利用沙箱容器技術,如 AWS Firecraker 或者阿里雲沙箱容器,來實現強隔離的安全執行環境,可是也帶來更大的資源消耗。雖然如今阿里雲沙箱容器通過優化能夠實現 300ms 的冷啓動速度,接近 Docker 這樣的 OS 容器啓動速度,可是還沒法知足函數 PaaS 毫秒級的啓動要求,目前須要經過的調度策略,預留必定的 standby 實例才能夠知足,可是這樣也引入了更多的資源消耗。
WebAssembly(WASM) 是一個新的 W3C 規範,是一個通用、開放、高效、安全的底層虛擬機抽象。它的設計初衷是爲了解決JavaScript的性能問題,使得 Web 應用有接近本機原生應用的性能。能夠將現有編程語言應用,如 C/C++, Rust 等,編譯成爲 WASM 的字節碼,運行在瀏覽器中的一個沙箱環境中。
WASM 讓應用開發技術與運行時環境解耦,極大促進了代碼複用。Mozilla 更在 2019 年推出了 WebAssembly System Interface(WASI),它提供相似 POSIX 這樣的標準 API 來標準化 WebAssembly 與系統資源的交互抽象,好比文件系統訪問,內存管理等。WASI 的出現拓展了 WASM 的應用場景,可讓其做爲一個虛擬機運行各類類型的服務端應用。WASM/WASI 爲應用的可移植性帶來全新的但願,爲了進一步推進 WebAssembly 生態發展,Mozilla、Fastly、英特爾和紅帽公司攜手成立了字節碼聯盟(Bytecode Alliance),共同領導 WASI 標準、 WebAssembly 運行時、工具等工做。
WebAssembly 所具有的的安全、可移植、高效率,輕量化的特色,爲應用沙箱的發展帶來了全新的思路。WASM 能夠輕鬆實現毫秒級冷啓動時間和極低的資源消耗。同時 WASM 字節碼比原生機器碼有更高的安全級別。此外,WASI 實現了細粒度基於能力的安全模型,遵循最小權限原則。在執行過程當中,WASI 應用只能訪問由依賴注入指明的確切資源集,這種方式與傳統粗粒度的操做系統級隔離相比,進一步收斂了安全攻擊面。
正因如此,WASM/WASI 獲得了 Serverless、IoT/邊緣計算等社區的普遍關注。Fastly、Cloudflare 等廠商相繼發佈了基於 WebAssembly 技術實現了更加輕量化的 Serverless 服務。
然而 WebAssembly 在服務器端的應用之路依然佈滿荊棘。首先 WASI 的能力還在很是早期的狀態,一些關鍵能力依然缺失,首當其衝的就是缺少標準化的網絡訪問能力:https://github.com/WebAssembly/WASI/issues/315。
目前 WASI 應用僅能作一些計算類任務,基本沒法實現分佈式應用,也沒法調用多樣性的後端服務和 Redis、MySQL、Kafka 等應用中間件。這大大限制了 WASI 的應用場景。
當理想撞上現實,頭破血流仍是絕處逢生?
2. 下一代可移植應用運行時加速編程界面上移,應用基礎設施能力下沉
Dapr 是微軟開源的面向雲原生應用的分佈式應用運行時,目標使全部開發人員可以使用任何語言和任何框架輕鬆地構建彈性的、事件驅動的、可移植的微服務應用。
Dapr 實現了一系列構建高性能、可伸縮、高可用的分佈式應用的設計模式,好比提供了服務發現和服務調用能力,也實現了一個簡單、一致的編程模型來支持事件驅動應用架構。
此外 Dapr 經過基礎設施屏蔽了應用訪問後端服務的技術細節,如資源綁定、安全管理,可觀測性等等。這個對 Serverless 應用很是重要,一方面將開發和部署進行了解耦,讓開發者和運維團隊能夠經過關注點分離簡化系統複雜性;一方面,能夠將短生命週期、無狀態的 Serverless 應用邏輯,與數據庫鏈接池管理這樣的長期運行,有狀態的中間件訪問能力進行解耦,提高了 Serverless 應用的可伸縮性和運行效率。
「Any language, any framework, anywhere」 是 Dapr 的重要設計目標。Dapr 經過在應用和後端服務之間,經過 Sidecar 方式提供一個抽象層,並經過標準化的 HTTP/gRPC API 實現了應用的可移植性,和後端服務的可替換性。
走向詩和遠方
咱們能夠將 WebAssembly 和 Dapr 相結合,來實現可移植、強隔離、輕量化的微服務應用架構。Dapr sidecar 與 WASM 虛擬機部署在一塊兒。WASI 應用經過 HTTP/gRPC 訪問本地的 Dapr 服務端點,由 Dapr 代理鏈接各類後端服務或者實現服務間通訊。
這樣的架構設計讓 WASI 應用的安全邊界很是清晰,符合 WASI 安全模型,WASI 應用只能經過 Dapr sidecar 實現外部服務訪問。同時在這個架構中,只有 WASM 虛擬機和 Dapr 做爲可信的環境依賴以原生機器碼運行。而應用是可移植的 WASM 字節碼,大大提高了架構的可移植性和安全性。
來自微軟 Deis Labs 的 Radu Matei,最近提供了一個實驗性項目能夠爲 WASI 添加 HTTP 支持。詳見:_https://deislabs.io/posts/wasi-experimental-http/ _。
在此基礎上,咱們來構建一個最小原型,驗證 WebAssembly 與 Dapr 相結合的技術可行性。
1. Dapr 環境準備
咱們首先按照 https://docs.dapr.io/getting-started/ 的流程:
$ dapr init ⌛ Making the jump to hyperspace... ✅ Downloading binaries and setting up components... ✅ Downloaded binaries and completed components set up. ℹ️ daprd binary has been installed to /Users/yili/.dapr/bin. ℹ️ dapr_placement container is running. ℹ️ dapr_redis container is running. ℹ️ dapr_zipkin container is running. ℹ️ Use `docker ps` to check running containers. ✅ Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started $ dapr run --app-id myapp --dapr-http-port 3500 WARNING: no application command found. ℹ️ Starting Dapr with id myapp. HTTP Port: 3500. gRPC Port: 63734 ℹ️ Checking if Dapr sidecar is listening on HTTP port 3500 ... ℹ️ Checking if Dapr sidecar is listening on GRPC port 63734 ℹ️ Dapr sidecar is up and running. ✅ You're up and running! Dapr logs will appear here.
2. 利用 Redis 做爲 WASI 應用的狀態存儲
咱們下面利用 Dapr 的 Get Started 的例子,利用 Redis 做爲 WASI 應用的狀態存儲。具體邏輯以下圖。
注:下面的應用須要 Rust 和 AssemblyScript 環境配置,請你們自行完成。
咱們在 Radu 項目的基礎上 fork 了一個版本,首先來下載代碼,並進行構建。
$ git clone https://github.com/denverdino/wasi-experimental-http $ cd wasi-experimental-http $ cargo build ... Finished dev [unoptimized + debuginfo] target(s) in 3m 02s
咱們利用 AssemblyScript 來實現了這個測試應用,測試代碼以下:
$ cat tests/dapr/index.ts // @ts-ignore import { Console } from "as-wasi"; import { DaprClient, StateItem } from "./dapr"; import { JSON } from "assemblyscript-json"; Console.log("Testing Dapr API ....") let dapr = new DaprClient() dapr.saveState("statestore", "weapon", JSON.Value.String("Death Star")) let o = JSON.Value.Object() o.set("name", "Tatooine") o.set("test", 123) let item = new StateItem("planets", o) let items: StateItem[] = [item] dapr.saveBulkState("statestore", items) let testObj = dapr.getState("statestore", "planets") let testStr = dapr.getState("statestore", "weapon") if (testStr.toString() == "Death Star" && testObj.isObj && (<JSON.Integer>(<JSON.Obj>testObj).getInteger("test")).valueOf() == 123) { Console.log("Test successfully!") } else { Console.log("Test failed!") }
代碼邏輯很是簡單,就是建立一個 Dapr 客戶端,而後經過 REST API,進行 Dapr 的狀態管理。咱們能夠快速驗證一下。
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.19s Running `target/debug/wasi-experimental-http-wasmtime-sample` Testing Dapr API .... POST http://127.0.0.1:3500/v1.0/state/statestore with [{"key":"weapon","value":"Death Star"}] POST http://127.0.0.1:3500/v1.0/state/statestore with [{"key":"planets","value":{"name":"Tatooine","test":123}}] GET http://127.0.0.1:3500/v1.0/state/statestore/planets GET http://127.0.0.1:3500/v1.0/state/statestore/weapon Test successfully! module instantiation time: 333.16637ms
3. 關鍵要點分析
wasi-experimental-http 項目在 Wasmtime (來自 Bytecode Alliance 的一個 WASM 實現)虛擬機上實現了擴展,支持在 WASI 應用中,訪問 HTTP 服務。它還提供了一個 AssemblyScript 的 HTTP Client 實現。
wasi-experimental-http 項目:https://github.com/deislabs/wasi-experimental-http/。
在此之上,咱們爲 AssemblyScript 提供一個 Dapr 的封裝,能夠參見:https://github.com/denverdino/wasi-experimental-http/blob/main/tests/dapr/dapr.ts。
// @ts-ignore import { Console } from "as-wasi"; import { Method, RequestBuilder, Response } from "../../crates/as"; import { JSONEncoder, JSON } from "assemblyscript-json"; export class StateItem { key: string value: JSON.Value etag: string | null metadata: Map<string, string> | null constructor(key: string, value: JSON.Value) { this.key = key this.value = value this.etag = null this.metadata = null } } ... export class DaprClient { port: i32 address: string constructor() { this.address = "127.0.0.1" this.port = 3500 } stateURL(storeName: string): string { return "http://" + this.address + ":" + this.port.toString() + "/v1.0/state/" + storeName } saveState(storeName: string, key: string, value: JSON.Value): boolean { let item = new StateItem(key, value) let items: StateItem[] = [item] return this.saveBulkState(storeName, items) } saveBulkState(storeName: string, items: StateItem[]): boolean { // Handle field let encoder = new JSONEncoder(); // Construct necessary object encoder.pushArray(null); for (let i = 0, len = items.length; i < len; i++) { let item = items[i] encoder.pushObject(null); encoder.setString("key", item.key) encodeValue(encoder, "value", item.value) if (item.etag != null) { encoder.setString("etag", <string>item.etag) } encoder.popObject() }; encoder.popArray(); // Or get serialized data as string let jsonString = encoder.toString(); let url = this.stateURL(storeName); Console.log("POST " + url + " with " + jsonString); let res = new RequestBuilder(url) .method(Method.POST) .header("Content-Type", "application/json") .body(String.UTF8.encode(jsonString)) .send(); let ok = res.status.toString() == "200" res.close(); return ok } getState(storeName: string, key: string): JSON.Value { let url = this.stateURL(storeName) + "/" + key; Console.log("GET " + url); let res = new RequestBuilder(url) .method(Method.GET) .send(); let ok = res.status.toString() == "200" let result = <JSON.Value> new JSON.Null() if (ok) { let body = res.bodyReadAll(); result = <JSON.Value>JSON.parse(body) } res.close(); return result } };
測試應用的 main 函數,會建立一個 Wasmtime 運行時環境,併爲其添加爲 HTTP 擴展,並加載執行測試應用的 WASM 字節碼:https://github.com/denverdino/wasi-experimental-http/blob/main/src/main.rs。
fn main() { let allowed_domains = Some(vec![ "http://127.0.0.1:3500".to_string(), ]); let module = "tests/dapr/build/optimized.wasm"; create_instance(module.to_string(), allowed_domains.clone()).unwrap(); } /// Create a Wasmtime::Instance from a compiled module and /// link the WASI imports. fn create_instance( filename: String, allowed_domains: Option<Vec<String>>, ) -> Result<Instance, Error> { let start = Instant::now(); let store = Store::default(); let mut linker = Linker::new(&store); let ctx = WasiCtxBuilder::new() .inherit_stdin() .inherit_stdout() .inherit_stderr() .build()?; let wasi = Wasi::new(&store, ctx); wasi.add_to_linker(&mut linker)?; // Link `wasi_experimental_http` let http = HttpCtx::new(allowed_domains, None)?; http.add_to_linker(&mut linker)?; let module = wasmtime::Module::from_file(store.engine(), filename)?; let instance = linker.instantiate(&module)?; let duration = start.elapsed(); println!("module instantiation time: {:#?}", duration); Ok(instance) }
道阻且長,行則將至
WASM/WASI 爲輕量化、可移植、缺省安全的應用運行時提供了良好的基礎,在區塊鏈等領域 WebAssembly 已經獲得了普遍的應用。然而,對於通用性的服務器端應用,WASM/WASI 的差距還很是明顯。因爲 berkeley socket 這樣標準化的網絡編程接口的缺失,只能經過擴展 WASM 虛擬機的方式來進行補齊。此外 WASM 的多線程能力尚未被標準化,目前的 HTTP 調用採用阻塞式同步調用,還沒法實現高效和穩定的網絡通訊。
此外,另外 WASM/WASI 的一個短板就是開發效率和生態建設。目前而言,雖然衆多的編程語言已經逐漸開始提供 WebAssembly 的支持,可是對於普通開發者而言,AssemblyScript 這樣的腳本語言是更加合適的選擇。AssemblyScript 複用了 TypeScript 的語法,與 Rust/C++ 相比,大大下降了學習曲線,也提供了很是好的 IDE 工具體驗,如 VS Code 等。可是與 TypeScripty 經過翻譯成爲 JavaScript 執行不一樣,AssemblyScript 應用會被編譯成 WASM 字節碼執行。AssemblyScript 本質上是一個靜態類型的編譯型語言,本質上與 JS/TS 這樣的動態類型的解釋型語言很是不一樣。兩者在語法上也有一些不一樣,好比目前 AssemblyScript 缺乏對閉包 (closure) 和正則表達式 (Regex) 等經常使用功能支持,這讓開發 WASM 應用仍是有必定的技術門檻。
另外與 NPM 強大的生態相比,AssemblyScript 社區也很年輕。不少功能都須要從頭構建,好比對 JSON 的序列化與反序列化,咱們選擇了 https://github.com/nearprotocol/assemblyscript-json ,可是其易用性和性能與成熟的 JSON 類庫還有必定差距。固然咱們也看到 AssemblyScript 的快速成長,以及愈來愈多的開發者開始貢獻 AssemblyScript 代碼庫,好比 regex 支持等等。
Dapr 的出現爲 WASM/WASI 開發通用的分佈式應用,尤爲是爲可移植的、Serverless 化的應用帶來另一縷曙光。然而 Dapr 也並不是完美:API 標準化在提高對後端服務可移植性的同時也阻礙了對差別化能力的支持。Sidecar 架構在提高靈活性的同時增長了部署和管理複雜性。
做爲一個理性樂觀派,任何技術都有其青澀的時代,期待社區的共同努力讓計算無處不在、創新觸手可及的理想成爲現實。