基于WASM的无侵入式全链路A/B Test实践
1 背景介紹
我們都知道,服務網格(ServiceMesh)可以為運行其上的微服務提供無侵入式的流量治理能力。通過配置VirtualService和DestinationRule,即可實現流量管理、超時重試、流量復制、限流、熔斷等功能,而無需修改微服務代碼。
流量管理的前提是一個服務存在多個版本,我們可以按部署多版本的目的進行分類,簡述如下,以方便理解余文。
- traffic routing:根據請求信息(Header/Cookie/Query Params),將請求流量路由到指定服務(Service)的指定版本(Deployment)的端點上(Pod[])。就是我們所說的A/B測試(A/B Testing)。
- traffic shifting:通過灰度/金絲雀(Canary)發布,將請求流量無差別地按比例路由到指定服務(Service)的各個版本(Deployment[])的端點上(Pod[])。
- traffic switching/mirroring:通過藍綠(Blue/Green)發布,根據請求信息按比例進行流量切換,以及進行流量復制。
本文所述的實踐是根據請求Header實現全鏈路A/B測試。
1.1 功能簡述
從Istio社區的文檔,我們很容易找到關于如何根據請求Header將流量路由到一個服務的特定版本的文檔和示例。但是這個示例只能在全鏈路的第一個服務上生效。
舉例來說,一個請求要訪問A-B-C三個服務,這三個服務都有en版本和fr版本。我們期待:
- header值為user:en的請求,全鏈路路由為A1-B1-C1
- header值為user:fr的請求,全鏈路路由為A2-B2-C2
相應的VirtualService配置如下所示:
http: - name: A|B|C-routematch:- headers:user:exact: enroute:- destination:host: A|B|C-svcsubset: v1 - route:- destination:host: A|B|C-svcsubset: v2我們通過實測可以發現,只有A這個服務的路由是符合我們預期的。B和C無法做到根據Header值路由到指定版本。
這是為什么呢?對于服務網格其上的微服務來說,這個header是憑空出現的,也就是微服務代碼無感知。因此,當A服務請求B服務時,不會透傳這個header;也就是說,當A請求B時,這個header已經丟失了。這時,這個匹配header進行路由的VirtualService配置已經毫無意義。
要解決這個問題,從微服務方的業務角度看,只能修改代碼(枚舉業務關注的全部header并透傳)。但這是一種侵入式的修改,而且無法靈活地支持新出現的header。
從服務網格的基礎設施角度看,任何header都是沒有業務意義且要被透傳的kv pair。只有做到這點,服務網格才能實現無差別地透傳用戶自定義的header,從而支持無侵入式全鏈路A/B Test功能。
那么該怎樣實現呢?
1.2 社區現狀
前面已經說明,在header無法透傳的情況下,單純地配置VirtualService的header匹配是無法實現這個功能的。
但是,在VirtualService中是否存在其他配置,可以實現header透傳呢?如果存在,那么單純使用VirtualService,代價是最小的。
經過各種嘗試(包括精心配置header相關的set/add),我發現無法實現。原因是VirtualService對header的干預發生在inbound階段,而透傳是需要在outbound階段干預header的。而微服務workload沒有能力對憑空出現的header值進行透傳,因此在路由到下一個服務時,這個header就會丟失。
因此,我們可以得出一個結論:無法單純使用VirtualService實現無侵入式全鏈路A/B Test,進一步地說,社區提供的現有配置都無法做到直接使用就能支持這個功能。
那么,就只剩下EnvoyFilter這個更高級的配置了。這是我們一開始很不希望的結論。原因有兩個:
1.3 實現方案
接下來進入技術選型。我用一句話來概括:
- Lua的優點是小巧,缺點是性能不理想
- WASM的優點是性能好,缺點是開發和分發相比Lua要困難。
- WASM的實現主流是C++和Rust,其他語言的實現尚不成熟或者存在性能問題。本文使用的是Rust。
我們使用Rust開發一個WASM,在outbound階段,獲取用戶在EnvoyFilter中定義的header并向后傳遞。
WASM包的分發使用Kubernetes的configmap存儲,Pod通過annotation中的定義獲取WASM配置并加載。(為什么使用這種分發形式,后面會講。)
2 技術實現
本節所述的相關代碼:https://github.com/AliyunContainerService/rust-wasm-4-envoy/tree/master/propagate-headers-filter
2.1 使用RUST實現WASM
1 定義依賴
WASM工程的核心依賴crates只有一個,就是proxy-wasm,這是使用Rust開發WASM的基礎包。此外,還有用于反序列化的包serde_json和用于打印日志的包log。Cargo.toml定義如下:
[dependencies] proxy-wasm = "0.1.3" serde_json = "1.0.62" log = "0.4.14"2 定義構建
WASM的最終構建形式是兼容c的動態鏈接庫,Cargo.toml定義如下:
[lib] name = "propaganda_filter" path = "src/propagate_headers.rs" crate-type = ["cdylib"]3 Header透傳功能
首先定義結構體如下,head_tag_name是用戶自定義的header鍵的名稱,head_tag_value是對應值的名稱。
struct PropagandaHeaderFilter {config: FilterConfig, }struct FilterConfig {head_tag_name: String,head_tag_value: String, }{proxy-wasm}/src/traits.rs中的trait HttpContext定義了on_http_request_headers方法。我們通過實現這個方法來完成Header透傳的功能。
impl HttpContext for PropagandaHeaderFilter {fn on_http_request_headers(&mut self, _: usize) -> Action {let head_tag_key = self.config.head_tag_name.as_str();info!("::::head_tag_key={}", head_tag_key);if !head_tag_key.is_empty() {self.set_http_request_header(head_tag_key, Some(self.config.head_tag_value.as_str()));self.clear_http_route_cache();}for (name, value) in &self.get_http_request_headers() {info!("::::H[{}] -> {}: {}", self.context_id, name, value);}Action::Continue} }第3-6行是獲取配置文件中用戶自定義的header鍵值對,如果存在就調用set_http_request_header方法,將鍵值對寫入當前header。
第7行是對當前proxy-wasm實現的一個workaround,如果你對此感興趣可以閱讀如下參考:
- https://github.com/istio/istio/issues/30545#issuecomment-783518257
- https://github.com/proxy-wasm/spec/issues/16
- https://www.elvinefendi.com/2020/12/09/dynamic-routing-envoy-wasm.html
2.2 本地驗證(基于Envoy)
1 WASM構建
使用如下命令構建WASM工程。需要強調的是wasm32-unknown-unknown這個target目前只存在于nightly中,因此在構建之前需要臨時切換構建環境。
rustup override set nightly cargo build --target=wasm32-unknown-unknown --release構建完成后,我們在本地使用docker compose啟動Envoy,對WASM功能進行驗證。
2 Envoy配置
本例需要為Envoy啟動提供兩個文件,一個是構建好的propaganda_filter.wasm,一個是Envoy配置文件envoy-local-wasm.yaml。示意如下:
volumes:- ./config/envoy/envoy-local-wasm.yaml:/etc/envoy-local-wasm.yaml- ./target/wasm32-unknown-unknown/release/propaganda_filter.wasm:/etc/propaganda_filter.wasmEnvoy支持動態配置,本地測試采用靜態配置:
static_resources:listeners:- address:socket_address:address: 0.0.0.0port_value: 80filter_chains:- filters:- name: envoy.filters.network.http_connection_manager ...http_filters:- name: envoy.filters.http.wasmtyped_config:"@type": type.googleapis.com/udpa.type.v1.TypedStructtype_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasmvalue:config:name: "header_filter"root_id: "propaganda_filter"configuration:"@type": "type.googleapis.com/google.protobuf.StringValue"value: |{"head_tag_name": "custom-version","head_tag_value": "hello1-v1"}vm_config:runtime: "envoy.wasm.runtime.v8"vm_id: "header_filter_vm"code:local:filename: "/etc/propaganda_filter.wasm"allow_precompiled: true ...Envoy的配置重點關注如下3點:
- 15行 我們在http_filters中定義了一個名稱為header_filter的type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
- 32行 本地文件路徑為/etc/propaganda_filter.wasm
- 20-26行 相關配置的類型是type.googleapis.com/google.protobuf.StringValue,值的內容是{"head_tag_name": "custom-version","head_tag_value": "hello1-v1"}。這里自定義的Header鍵名為custom-version,值為hello1-v1。
3 本地驗證
執行如下命令啟動docker compose:
docker-compose up --build請求本地服務:
curl -H "version-tag":"v1" "localhost:18000"此時Envoy的日志應有如下輸出:
proxy_1 | [2021-02-25 06:30:09.217][33][info][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1152] wasm log: ::::create_http_context head_tag_name=custom-version,head_tag_value=hello1-v1 proxy_1 | [2021-02-25 06:30:09.217][33][info][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1152] wasm log: ::::head_tag_key=custom-version ... proxy_1 | [2021-02-25 06:30:09.217][33][info][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1152] wasm log: ::::H[2] -> custom-version: hello1-v12.3 WASM的分發方式
WASM的分發是指將WASM包存儲于一個分布式倉庫中,供指定的Pod拉取的過程。
1 Configmap + Envoy的Local方式
雖然這種方式不是WASM分發的終態,但是因為它較為容易理解且適合簡單的場景,本例最終選擇了這個方案作為示例講解。雖然configmap的本職工作不是存WASM的,但是configmap和Envoy的local模式都很成熟,兩者結合恰能滿足當前需求。
阿里云服務網格ASM產品已經提供了這種類似的方式,具體可以參考 為Envoy編寫WASM Filter并部署到ASM中。
要把WASM包塞到配置中,首要考慮的是包的尺寸。我們使用wasm-gc進行包裁剪,示意如下:
ls -hl target/wasm32-unknown-unknown/release/propaganda_filter.wasm wasm-gc ./target/wasm32-unknown-unknown/release/propaganda_filter.wasm ./target/wasm32-unknown-unknown/release/propaganda-header-filter.wasm ls -hl target/wasm32-unknown-unknown/release/propaganda-header-filter.wasm執行結果如下,可以看到裁剪前后,包的尺寸對比:
-rwxr-xr-x 2 han staff 1.7M Feb 25 15:38 target/wasm32-unknown-unknown/release/propaganda_filter.wasm -rw-r--r-- 1 han staff 136K Feb 25 15:38 target/wasm32-unknown-unknown/release/propaganda-header-filter.wasm創建configmap:
wasm_image=target/wasm32-unknown-unknown/release/propaganda-header-filter.wasm kubectl -n $NS create configmap -n $NS propaganda-header --from-file=$wasm_image為指定Deployment打Patch:
patch_annotations=$(cat config/annotations/patch-annotations.yaml) kubectl -n $NS patch deployment "hello$i-deploy-v$j" -p "$patch_annotations"patch-annotations.yaml如下:
spec:template:metadata:annotations:sidecar.istio.io/userVolume: '[{"name":"wasmfilters-dir","configMap": {"name":"propaganda-header"}}]'sidecar.istio.io/userVolumeMount: '[{"mountPath":"/var/local/lib/wasm-filters","name":"wasmfilters-dir"}]'2 Envoy的Remote方式
Envoy同時支持local和remote形式的資源定義。對比如下:
vm_config:runtime: "envoy.wasm.runtime.v8"vm_id: "header_filter_vm"code:local:filename: "/etc/propaganda_filter.wasm" vm_config:runtime: "envoy.wasm.runtime.v8"code:remote:http_uri:uri: "http://*.*.*.216:8000/propaganda_filter.wasm"cluster: web_servicetimeout:seconds: 60sha256: "da2e22*"remote方式是最接近原始Enovy的,因此這種方式本來是本例的首選。但是實測過程中發現在包的hash校驗上存在問題,詳見下方參考。并且,Envoy社區的大牛周禮贊反饋我說remote不是Envoy支持WASM分發的未來方向。因此,本例最終放棄這種方式。
- https://stackoverflow.com/questions/65871312/how-to-set-the-sha256-hex-in-envoy-wasm-remote-config
- https://envoyproxy.slack.com/archives/C78M4KW76/p1611496672017500
3 ORAS + Local方式
ORAS是OCI Artifacts項目的參考實現,可顯著簡化OCI注冊表中任意內容的存儲。
使用ORAS客戶端或者API/SDK的方式將具有允許的媒體類型的Wasm模塊推送到注冊庫(一個OCI兼容的注冊庫)中,然后通過控制器將Wasm Filter部署到指定工作負載對應的Pod中,以Local的方式進行掛載。
阿里云服務網格ASM產品中提供了對WebAssembly(WASM)技術的支持,服務網格使用人員可以把擴展的WASM Filter通過ASM部署到數據面集群中相應的Envoy代理中。通過ASMFilterDeployment Controller組件, 可以支持動態加載插件、簡單易用、以及支持熱更新等能力。具體來說,ASM產品提供了一個新的CRD ASMFilterDeployment以及相關的controller組件。這個controller組件會監聽ASMFilterDeployment資源對象的情況,會做2個方面的事情:
- 創建出用于控制面的Istio EnvoyFilter Custom Resource,并推送到對應的asm控制面istiod中
- 從OCI注冊庫中拉取對應的wasm filter鏡像,并掛載到對應的workload pod中
具體可以參考:基于Wasm和ORAS簡化擴展服務網格功能。
后續的實踐分享將會使用這種方式進行WASM的分發,敬請期待。
類似地,業界其他友商也在推進這種方式,特別是Solo.io提供了一整套WASM的開發框架wasme,基于該框架可以開發-構建-分發WASM包(OCI image)并部署到Webassembly Hub。這個方案的優點很明顯,完整地支持了WASM的開發到上線的生命周期。但這個方案的缺點也非常明顯,wasme的自包含導致了很難將其拆分,并擴展到solo體系之外。
阿里云服務網格ASM團隊正在與包括solo在內的業界相關團隊交流如何共同推進Wasm filter的OCI規范以及相應的生命周期管理,以幫助客戶可以輕松擴展Envoy的功能并將其在服務網格中的應用推向了新的高度。
2.4 集群驗證(基于Istio)
1 實驗示例
WASM分發到Kubernetes的configmap后,我們可以進行集群驗證了。示例(源代碼)包含3個Service:hello1-hello2-hello3,每個服務包含2個版本:v1/en和v2/fr。
每個Service配置了VirtualService和DestinationRule用來定義匹配Header并路由到指定版本。
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata:name: hello2-vs spec:hosts:- hello2-svchttp:- name: hello2-v2-routematch:- headers:route-v:exact: hello2v2route:- destination:host: hello2-svcsubset: hello2v2- route:- destination:host: hello2-svcsubset: hello2v1 ---- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata:name: hello2-dr spec:host: hello2-svcsubsets:- name: hello2v1labels:version: v1- name: hello2v2labels:version: v2Envoyfilter示意如下:
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata:name: hello1v2-propaganda-filter spec:workloadSelector:labels:app: hello1-deploy-v2version: v2configPatches:- applyTo: HTTP_FILTERmatch:context: SIDECAR_OUTBOUNDproxy:proxyVersion: "^1\\.8\\.*"listener:filterChain:filter:name: envoy.filters.network.http_connection_managersubFilter:name: envoy.filters.http.routerpatch:operation: INSERT_BEFOREvalue:name: envoy.filters.http.wasmtyped_config:"@type": type.googleapis.com/udpa.type.v1.TypedStructtype_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasmvalue:config:name: propaganda_filterroot_id: propaganda_filter_rootconfiguration:'@type': type.googleapis.com/google.protobuf.StringValuevalue: |{"head_tag_name": "route-v","head_tag_value": "hello2v2"}vm_config:runtime: envoy.wasm.runtime.v8vm_id: propaganda_filter_vmcode:local:filename: /var/local/lib/wasm-filters/propaganda-header-filter.wasmallow_precompiled: true2 驗證方法
攜帶header的請求curl -H "version:v1" "http://$ingressGatewayIp:8001/hello/xxx"通過istio-ingressgateway進入,全鏈路按header值,進入服務的指定版本。這里,由于header中指定了version為v2,那么全鏈路將
為hello1 v2-hello2 v2-hello3 v2。效果如下圖所示。
驗證過程和結果示意如下。
for i in {1..5}; docurl -s -H "route-v:v2" "http://$ingressGatewayIp:$PORT/hello/eric" >>resultecho >>result done check=$(grep -o "Bonjour eric" result | wc -l) if [[ "$check" -eq "15" ]]; thenecho "pass" elseecho "fail"exit 1 firesult:
Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182 Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182 Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182 Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182 Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182我們看到,輸出信息Bonjour eric來自各個服務的fr版本,說明功能驗證通過。
3 性能分析
新增EnvoyFilter+WASM后,功能驗證通過,但這會帶來多少延遲開銷呢?這是服務網格的提供者和使用者都非常關心的問題。本節將對如下兩個關注點進行驗證。
- 增加EnvoyFilter+WASM后的增量延遲開銷情況
- WASM版本和Lua版本的開銷對比
3.1 Lua實現
Lua的實現可以直接寫到EnvoyFilter中,無需獨立的工程。示例如下:
patch:operation: INSERT_BEFOREvalue:name: envoy.luatyped_config:"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuainlineCode: |function envoy_on_request(handle)handle:logInfo("[propagate header] route-v:hello3v2")handle:headers():add("route-v", "hello3v2")end3.2 壓測方法
1 部署
- 分別在3個namespace上部署相同的Deployment/Service/VirtualService/DestinationRule
- 在hello-abtest-lua中部署基于Lua的EnvoyFilter
- 在hello-abtest-wasm中部署基于WASM的EnvoyFilter
2 工具
本例使用hey作為壓測工具。hey前身是boom,用來代替ab(Apache Bench)。使用相同的壓測參數分別對三個環境進行壓測。示意如下:
# 并發work數量 export NUM=2000 # 每秒請求數量 export QPS=2000 # 壓測執行時常 export Duration=10shey -c $NUM -q $QPS -z $Duration -H "route-v:v2" http://$ingressGatewayIp:$PORT/hello/eric > $SIDECAR_WASM_RESULT請關注hey壓測結果文件,結果最后不能出現socket: too many open files,否則影響結果。可以使用ulimit -n $MAX_OPENFILE_NUM命令配置,然后再調整壓測參數,以確保結果的準確性。
3.3 報告
我們從三份結果報告中選取4個關鍵指標,如下圖所示:
| 1000并發1000QPS持續10秒鐘 | |||
| 平均延遲 | 0.6317 secs | 0.6395 secs | 0.7012 secs |
| 延遲99%分布 | 0.9167 secs | 0.9352 secs | 1.1355 secs |
| QPS | 1541 | 1519 | 1390 |
| Total | 16281 | 16109 | 1390 |
| 2000并發2000QPS持續10秒鐘 | |||
| 平均延遲 | 1.2078 secs | 1.3290 secs | 1.4593 secs |
| 延遲99%分布 | 1.8621 secs | 1.8354 secs | 2.2116 secs |
| QPS | 1564 | 1421 | 1290 |
| Total | 17622 | 16009 | 14662 |
3.4 結論
- wasm 1.2% (0.6395-0.6317)/0.6317和1% (1.3290-1.2078)/1.2078
- lua 11%(0.7012-0.6317)/0.6317和20% (1.4593-1.2078)/1.2078
4 展望
4.1 如何使用
本文從技術實現角度,講述了如何實現并驗證一個透傳用戶自定義Header的WASM,從而支持無侵入式全鏈路A/B Test這個需求。
但是,作為服務網格的使用者,如果按照本文一步步去實現,是非常繁瑣且容易出錯的。
阿里云服務網格ASM團隊正在推出一種ASM插件目錄的機制,用戶只需在插件目錄中選擇插件,并為插件提供自定義的Header等極少數量的kv配置,即可自動生成和部署相關的EnvoyFilter+WASM+VirtualService+DestinationRule。
4.2 如何擴展
本例只展示了基于Header的匹配路由功能,如果我們希望根據Query Params進行匹配和路由該如何擴展呢?
這是ASM插件目錄正在密切關注的話題,最終插件目錄將提供最佳實踐。
以上。
原文鏈接:https://developer.aliyun.com/article/782181?
版權聲明:本文內容由阿里云實名注冊用戶自發貢獻,版權歸原作者所有,阿里云開發者社區不擁有其著作權,亦不承擔相應法律責任。具體規則請查看《阿里云開發者社區用戶服務協議》和《阿里云開發者社區知識產權保護指引》。如果您發現本社區中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社區將立刻刪除涉嫌侵權內容。總結
以上是生活随笔為你收集整理的基于WASM的无侵入式全链路A/B Test实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Knative 多容器支持介绍
- 下一篇: 【开启报名】云原生变革与效能技术沙龙 ·