Istio1.5 Envoy 数据面 WASM 实践
Istio 1.5 回歸單體架構,并拋卻原有的 out-of-process 的數據面擴展方式,轉而擁抱基于 WASM 的 in-proxy 擴展,以期獲得更好的性能。本文基于網易杭州研究院輕舟云原生團隊的調研與探索,介紹 WASM 的社區發展與實踐。
超簡單版解釋:
--> Envoy 內置 Google V8 引擎,支持WASM字節碼運行,并開放相關接口用于和 WASM 虛擬機交互數據;
--> 使用各種語言開發相關擴展并編譯為 .WASM 文件;
--> 將擴展文件掛載或者打包進入 Envoy 容器鏡像,通過xDS動態下發文件路徑及相關配置由虛擬機執行。
WebAssembly 簡述
Istio 最新發布的 1.5 版本,架構發生了巨大調整,從原有的分布式結構回歸為單體,同時拋卻了原有的 out-of-process 的 Envoy 擴展方式,轉而擁抱基于 WASM 的 in-proxy 擴展,以期獲得更好的性能,同時減小部署和使用的復雜性。所有的 WASM 插件都在 Envoy 的沙箱中運行,相比于原生 C++ Envoy 插件,WASM 插件具有以下的優點:
接近原生插件性能(存疑,待驗證,社區未給出可信測試結果,但是 WASM 字節碼和機器碼比較接近,它的性能極限確實值得期待);
沙箱運行,更安全,單個 filter 故障不會影響到 Envoy 主體執行,且 filter 通過特定接口和 Envoy 交互數據,Envoy 可以對暴露的數據進行限制(沙箱安全性對于 Envoy 整體穩定性保障具有很重要的意義);
可動態分發和載入運行(單個插件可以編譯為 .WASM 文件進行分發共享,動態掛載,動態載入,且沒有平臺限制);
無開發語言限制,開發效率更高(WASM 本身支持語言眾多,但是限定到 Envoy 插件開發,必然依賴一些封裝好的 SDK 用于和 Envoy 進行交互,目前只有 C++ 語言本身、Rust 以及 AssemblysScript 有一定的支持)。
WASM 的誕生源自前端,是一種為了解決日益復雜的前端 web 應用以及有限的 JavaScript 性能而誕生的技術。它本身并不是一種語言,而是一種字節碼標準,一個“編譯目標”。WASM 字節碼和機器碼非常接近,因此可以非常快速的裝載運行。任何一種語言,都可以被編譯成 WASM 字節碼,然后在 WASM 虛擬機中執行(本身是為 web 設計,必然天然跨平臺,同時為了沙箱運行保障安全,所以直接編譯成機器碼并不是最佳選擇)。理論上,所有語言,包括 JavaScript、C、C++、Rust、Go、Java 等都可以編譯成 WASM 字節碼并在 WASM 虛擬機中執行。
社區發展及現狀
Envoy & WASM
Envoy 提供了一個特殊的 Http 七層 filter,名為 wasm,用于載入和執行 WASM 字節碼。該七層 filter 同樣也負責 WASM 虛擬機的創建和管理,使用的是 Google 內部的 v8 引擎(支持 JS 和 WASM)。當前 filter 未進入 Envoy 主干,而是在單獨的一個工程中。該工程會周期性從主干合并代碼。從機制看,WASM 擴展和 Lua 擴展機制非常相似,只是 Lua 載入的是原始腳本,而 WASM 載入的是編譯后的 WASM 字節碼。Envoy 暴露相關的接口如獲取請求頭、請求體,修改請求頭,請求體,改變插件鏈執行流程等等,用于 WASM 插件和 Envoy 主體進行數據交互。
對于每一個 WASM 擴展插件都可以被編譯為一個 *.WASM 文件,而 Envoy 七層提供的 wasm Filter 可以通過動態下發相關配置(指定文件路徑)使其載入對應的文件并執行:前提是對應的文件已經在鏡像中或者掛載進入了對應的路徑。當然,WASM Filter 也支持從遠程獲取對應的 *.WASM 文件(和目前網易輕舟 API 網關對 Lua 腳本擴展的支持非常相似)。
Istio & WASM
現有的 Istio 提供了名為 Mixer 插件模型用于擴展 Envoy 數據面功能,具體來說,在 Envoy 內部,Istio 開發了一個原生 C++ 插件用于收集和獲取運行時請求信息并通過 gRPC 將信息上報給 Mixer,外部 Mixer 則調用各個 Mixer Adapter 用于監控、授權控制、限流等等操作,相關處理結果如有必要再返回給 Envoy 中 C++ 插件用于做相關控制。
Mixer 模型雖然提高了極高的靈活性,且對 Envoy 侵入性極低,但是引入了大量的額外的外部調用和數據交互,帶來了巨大的性能開銷(相關的測試結果很多,按照 istio 社區的數據:移除 Mixer 可以使整體 CPU 消耗減少 50%)。而且 Istio 插件擴展模型和 Envoy 插件模型整體是割裂的,Istio 插件在 out-of-process 中執行,通過 gRPC 進行插件與 Envoy 主體的數據交互,而 Envoy 原生插件則是 in-proxy 模式,在同一個進程中通過虛函數接口進行調用和執行。
因此在 Istio 1.5 中,Istio 提供了全新的插件擴展模型:WASM in proxy。使用 Envoy 支持的WASM機制來擴展插件:兼顧性能、多語言支持、動態下發動態載入、以及安全性。唯一的缺點就是現有的支持還不夠完善。
為了提升性能,Istio 社區在 1.5 發布中,已經將幾個擴展使用 in-proxy 模型(基于 WASM API 而非原生 Envoy C++ HTTP 插件 API)進行實現。但是目前考慮到 WASM 還不夠穩定,所以相關擴展默認不會執行在 WSAM 沙箱之中(在所謂 NullVM 中執行)。雖然 istio 也支持將相關擴展編譯為 WASM 模塊,并在沙箱中執行,但是不是默認選項。
所謂 Mixer V2 其最終目標就是將現有的 out-of-process 的插件模型最終用基于 WASM 的 in-proxy 擴展模型來替代。但是目前舉例目標仍舊有較長一段路要走,畢竟即使 Istio 社區本身的插件,也未能完全在 WASM 沙箱中落地。但從 Istio 1.5 開始,Istio 社區應該會快速推動 WASM 的發展。
solo.io & WASM
solo.io 推出了 WebAssembly Hub,用于構建、發布以及共享 Envoy WASM 擴展。WebAssembly Hub 包括一套用于簡化擴展開發的 SDK(目前 solo.io 提供了AssemblysScript SDK,而 Istio/Envoy 社區提供了 Rust/C++ SDK),相關的構建、發布命令,一個用于共享和復用的擴展倉庫。具體的內容可以參考 solo.io 提供的教程。
WASM 實踐
下面簡單實現一個 WASM 擴展作為演示 DEMO,可以幫助大家對 WASM 有進一步了解。此處直接使用了 solo.io 提供的構建工具,避免環境搭建等各個方面的一些冗余工作。該擴展名為 path_rewrite,可以根據路由原始的 path 值匹配,來將請求 path 重寫為不同值。
執行以下命令安裝 wasme:
curl?-sL?https://run.solo.io/wasme/install?|?sh export?PATH=$HOME/.wasme/bin:$PATHwasme 是 solo.io 提供的一個命令行工具,一個簡單的類比就是:docker cli 之于容器鏡像,wasme 之于 WASM 擴展。
ping@ping-OptiPlex-3040:~/Desktop/wasm_example$?wasme?init?./path_rewrite Use?the?arrow?keys?to?navigate:?↓?↑?→?← ??What?language?do?you?wish?to?use?for?the?filter:??cppassemblyscript執行 wasme 初始化命令,會讓用戶選擇使用何種語言開發 WASM 擴展,目前 wasme 工具僅支持 C++ 和 AssemblyScript,當前仍舊選擇 cpp 進行開發(AssemblyScript 沒有開發經驗,后續有機會可以學習一下)。執行命令之后,會自動創建一個 bazel 工程,目錄結構如下:其中關鍵的幾個文件已經添加了注釋。從目錄結構看,solo.io 沒有在 wasme 中添加任何黑科技,生成的模板非常的干凈,完整而簡潔。
. ├──?bazel │???└──?external │???????├──?BUILD │???????├──?emscripten-toolchain.BUILD │???????└──?envoy-wasm-api.BUILD??????#?說明如何編譯envoy?api依賴 ├──?BUILD?????????????????????????????#?說明如何編譯插件本身代碼 ├──?filter.cc?????????????????????????#?插件具體代碼 ├──?filter.proto??????????????????????#?擴展數據面接口 ├──?README.md ├──?runtime-config.json ├──?toolchain │???├──?BUILD │???├──?cc_toolchain_config.bzl │???├──?common.sh │???├──?emar.sh │???└──?emcc.sh └──?WORKSPACE?????????????????????????#?工程描述文件包含對envoy?api依賴filter.cc 中已經填充了樣板代碼,包括所有的插件需要實現的接口。開發者只需要按需修改某個接口的具體實現即可(此處列出了整個插件的全部代碼,以供參考。雖然該代碼沒有實現什么特許功能,但是已經包含了一個 WASM 擴展(C++ 語言版)應當具備的所有結構,無論多么復雜的插件,都只是在該結構的基礎上填充相關的邏輯代碼而已:
//?NOLINT(namespace-envoy) #include?<string> #include?<unordered_map>#include?"google/protobuf/util/json_util.h" #include?"proxy_wasm_intrinsics.h" #include?"filter.pb.h"class?AddHeaderRootContext?:?public?RootContext?{ public:explicit?AddHeaderRootContext(uint32_t?id,?StringView?root_id)?:?RootContext(id,?root_id)?{}bool?onConfigure(size_t?/*?configuration_size?*/)?override;bool?onStart(size_t)?override;std::string?header_name_;std::string?header_value_; };class?AddHeaderContext?:?public?Context?{ public:explicit?AddHeaderContext(uint32_t?id,?RootContext*?root)?:?Context(id,?root),?root_(static_cast<AddHeaderRootContext*>(static_cast<void*>(root)))?{}void?onCreate()?override;FilterHeadersStatus?onRequestHeaders(uint32_t?headers)?override;FilterDataStatus?onRequestBody(size_t?body_buffer_length,?bool?end_of_stream)?override;FilterHeadersStatus?onResponseHeaders(uint32_t?headers)?override;void?onDone()?override;void?onLog()?override;void?onDelete()?override; private:AddHeaderRootContext*?root_; }; static?RegisterContextFactory?register_AddHeaderContext(CONTEXT_FACTORY(AddHeaderContext),ROOT_FACTORY(AddHeaderRootContext),"add_header_root_id");bool?AddHeaderRootContext::onConfigure(size_t)?{?auto?conf?=?getConfiguration();Config?config;google::protobuf::util::JsonParseOptions?options;options.case_insensitive_enum_parsing?=?true;options.ignore_unknown_fields?=?false;google::protobuf::util::JsonStringToMessage(conf->toString(),?&config,?options);LOG_DEBUG("onConfigure?name?"?+?config.name());LOG_DEBUG("onConfigure?"?+?config.value());header_name_?=?config.name();header_value_?=?config.value();return?true;? }bool?AddHeaderRootContext::onStart(size_t)?{?LOG_DEBUG("onStart");?return?true;}void?AddHeaderContext::onCreate()?{?LOG_DEBUG(std::string("onCreate?"?+?std::to_string(id())));?}FilterHeadersStatus?AddHeaderContext::onRequestHeaders(uint32_t)?{LOG_DEBUG(std::string("onRequestHeaders?")?+?std::to_string(id()));return?FilterHeadersStatus::Continue; }FilterHeadersStatus?AddHeaderContext::onResponseHeaders(uint32_t)?{LOG_DEBUG(std::string("onResponseHeaders?")?+?std::to_string(id()));addResponseHeader(root_->header_name_,?root_->header_value_);replaceResponseHeader("location",?"envoy-wasm");return?FilterHeadersStatus::Continue; }FilterDataStatus?AddHeaderContext::onRequestBody(size_t?body_buffer_length,?bool?end_of_stream)?{return?FilterDataStatus::Continue; }void?AddHeaderContext::onDone()?{?LOG_DEBUG(std::string("onDone?"?+?std::to_string(id())));?}void?AddHeaderContext::onLog()?{?LOG_DEBUG(std::string("onLog?"?+?std::to_string(id())));?}void?AddHeaderContext::onDelete()?{?LOG_DEBUG(std::string("onDelete?"?+?std::to_string(id())));?}注意到生成的樣板代碼類型名稱仍舊以 AddHeader 為前綴,而沒有根據提供的路徑名稱生成,此處是 wasme 可以優化的一個地方。此外,自動生成的樣板代碼中已經包含了 AddHeader 的一些代碼,邏輯簡單,但是配置解析、API 訪問,請求頭修改等過程都具備,麻雀雖小,五臟俱全,正好可以幫助初次的開發者可以依葫蘆畫瓢熟悉 WASM 插件的開發過程。對于入門是非常友好的。
針對 path_rewrite 具體的開發步驟如下:
STEP ONE 首先修改模板代碼中 filter.proto 文件,因為 path rewrite 肯定不能簡單的只能替換固定值,修改后 proto 文件如下所示:
syntax?=?"proto3";message?PathRewriteConfig?{message?Rewrite?{string?regex_match?=?1;??????#?path正則匹配時替換string?custom_path?=?2;??????#?待替換值}repeated?Rewrite?rewrites?=?1; }STEP TWO 修改配置解析接口,具體方法名為 onConfigure。修改后解析接口如下:
bool?AddHeaderRootContext::onConfigure(size_t)?{auto?conf?=?getConfiguration();PathRewriteConfig?config;?//?message?type?in?filter.protoif?(!conf.get())?{return?true;}google::protobuf::util::JsonParseOptions?options;options.case_insensitive_enum_parsing?=?true;options.ignore_unknown_fields?=?false;//?解析字符串配置并轉換為PathRewriteConfig類型:配置反序列化google::protobuf::util::JsonStringToMessage(conf->toString(),?&config,options);//?配置階段編譯regex避免請求時重復編譯,提高性能for?(auto?&rewrite?:?config.rewrites())?{rewrites_.push_back({std::regex(rewrite.regex_match()),?rewrite.custom_path()});}return?true; }STEP THREE 修改請求頭接口,具體方法名為 onRequestHeaders,修改后接口代碼如下:
FilterHeadersStatus?AddHeaderContext::onRequestHeaders(uint32_t)?{LOG_DEBUG(std::string("onRequestHeaders?")?+?std::to_string(id()));//?Envoy中path同樣存儲在header中,key為:pathauto?path?=?getRequestHeader(":path");if?(!path.get())?{return?FilterHeadersStatus::Continue;}std::string?path_string?=?path->toString();for?(auto?&rewrite?:?root_->rewrites_)?{if?(std::regex_match(path_string,?rewrite.first)?&&!rewrite.second.empty())?{replaceRequestHeader(":path",?rewrite.second);replaceRequestHeader("location",?"envoy-wasm");return?FilterHeadersStatus::Continue;}}return?FilterHeadersStatus::Continue; }從上述過程不難看出,整個擴展的開發體驗相當簡單,按需實現對應接口即可,擴展本身內容非常輕,內部具體的功能邏輯才是決定擴展開發復雜性的關鍵。而且借助 wasme 工具,自動生成代碼后,效率可以更高(和目前在內部使用的 filter_creator.py 有部分相似,樣板代碼自動生成)。
至此,插件已經開發完成,可以打包編譯了。wasm 同樣提供了打包編譯的功能,甚至可以類似于容器鏡像將編譯后結構推送到遠端倉庫之中,用于分享或者存儲。不過有一個提示,在開發之前,先直接執行 bazel 命令編譯,編譯過程中,一些基礎依賴會被自動拉取并緩存到本地,借助 IDE 可以獲得更好的代碼提示和開發體驗。
bazel?build?:filter.wasm接下來是 wasme 命令編譯:
wasme?build?cpp?-t?webassemblyhub.io/wbpcode/path_rewrite:v0.1?.該命令會使用固定鏡像作為編譯環境,但是本質和直接使用 bazel 編譯并無不同。具體的編譯日志可以看出,實際上,該命令也是使用的bazel build :filter.wasm。
Status:?Downloaded?newer?image?for?quay.io/solo-io/ee-builder:0.0.19 Building?with?bazel...running?bazel?build?:filter.wasm Extracting?Bazel?installation... Starting?local?Bazel?server?and?connecting?to?it...注意,上述命令中 wbpcode 為用戶名,具體實踐時提議替換為自身用戶名,如果注冊了 webassemblyhub.io 賬號,甚至可以進行 push 和 pull 操作。此次就不做相關操作了,直接本地啟動帶 WASM 的 envoy。命令如下:
#?--config參數用于指定wasm擴展配置 wasme?deploy?envoy?webassemblyhub.io/wbpcode/path_rewrite:v0.1?--config?"{\"rewrites\":?[?{\"regex_match\":\"...\",?\"custom_path\":?\"/anything\"}?]}"?--envoy-run-args?"-l?trace"從 envoy 執行日志可以看到:最終 envoy 會執行七層 Filter:envoy.filters.http.wasm,相關配置為:wasm 文件位置(docker 執行時掛載進入容器內部)、 wasm 文件對應插件配置、runtime 等等。通過在 http_filters 中重復添加多個envoy.filters.http.wasm,即可實現多個 WASM 擴展的執行。從下面的日志也可以看出,即使不使用 solo.io 的工具,只需要為 Envoy 指定編譯好的 wasm 文件,其執行結果是完全相同的。
[2020-03-31?08:41:24.831][1][debug][config]?[external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:388]???????name:?envoy.filters.http.wasm [2020-03-31?08:41:24.831][1][debug][config]?[external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:390]?????config:?{"config":?{"rootId":?"add_header_root_id","vmConfig":?{"code":?{"local":?{"filename":?"/home/ping/.wasme/store/e58ddd90347b671ad314f1c969771cea/filter.wasm"}},"runtime":?"envoy.wasm.runtime.v8"},"configuration":?"{\"rewrites\":?[?{\"regex_match\":\"...\",?\"custom_path\":?\"/anything\"}?]}","name":?"add_header_root_id"} }之后使用對應 path 調用接口:可發現 WASM 插件已經生效:
':authority',?'localhost:8080' ':path',?'/ab'?#?原始請求path匹配"..." ':method',?'GET' 'user-agent',?'curl/7.58.0' 'accept',?'*/*' ':authority',?'localhost:8080' ':path',?'/anything' ':method',?'GET' ':scheme',?'https' 'user-agent',?'curl/7.58.0' 'accept',?'*/*' 'x-forwarded-proto',?'http' 'x-request-id',?'1009236e-ab57-4ded-a8ff-3d1b17c6787b' 'location',?'envoy-wasm' 'x-envoy-expected-rq-timeout-ms',?'15000'WASM 總結
WASM 擴展仍在快速發展當中,但是 Isito 使用 WASM API 實現了相關的插件,說明已經做好了遷移的準備。前景美好,值得期待,但有待進一步確定 WASM 沙箱本身穩定性和性能。
從開發體驗來說:
借助 solo.io 工具,簡單插件的開發幾乎沒有任何的難度,只是目前支持的語言只有 C++/AssemblyScript(Envoy 社區開發了 Rust 語言 SDK,但是正在開發當中而且使用 Rust 開發 WASM 擴展的價值存疑:Rust 相比于 C++ 最大的優勢是通過嚴格的編譯檢查來保證內存安全,但是也使得上手難度又提升了一個臺階,在有 WASM 沙箱為內存安全兜底的情況下,使用 Rust 而不使用 JS、Go 等上手更簡易的語言來開發擴展,實無必要)。
對于相對復雜的插件,如果使用 WASM 的話,測試相比于原生插件會更困難一些,WASM 擴展配置的輸入只能依賴手寫 JSON 字符串,希望未來能夠改善。
缺少路由粒度的配置,所有配置都是全局生效,依賴插件內部判斷,但是這一部分如果確實有需要,支持起來應該很快,不存在技術上的阻礙,倒是不用擔心。
作者簡介
王佰平,網易杭州研究院輕舟云原生團隊工程師,負責輕舟 Envoy 網關與輕舟 Service Mesh 數據面開發、功能增強、性能優化等工作,對 Envoy 數據面開發、增強、落地具有較為豐富的經驗。
點擊?閱讀原文?查看更多
總結
以上是生活随笔為你收集整理的Istio1.5 Envoy 数据面 WASM 实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谈谈登录密码传输这件小事
- 下一篇: 使用c# .net core开发国标gb