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-route
match:
- headers:
user:
exact: en
route:
- destination:
host: A|B|C-svc
subset: v1
- route:
- destination:
host: A|B|C-svc
subset: 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這個更高級的配置了。這是我們一開始很不希望的結論。原因有兩個:
- EnvoyFilter的配置太過複雜,一般用戶很難在服務網格中快速學習和使用,即便我們提供示例,一旦需求稍有變化,示例對修改EnvoyFilter的參考價值甚微。
- 就算使用EnvoyFilter,目前Envoy內置的filter也沒有直接支持這個功能的,需要藉助Lua或者WebAssembly(WASM)進行開發。
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.wasm
Envoy支持動態配置,本地測試採用靜態配置:
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
...
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
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-v1
2.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_service
timeout:
seconds: 60
sha256: "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的分發,敬請期待。
類似地,業界其他友商也在推進這種方式,特別是http://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-svc
http:
- name: hello2-v2-route
match:
- headers:
route-v:
exact: hello2v2
route:
- destination:
host: hello2-svc
subset: hello2v2
- route:
- destination:
host: hello2-svc
subset: hello2v1
----
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: hello2-dr
spec:
host: hello2-svc
subsets:
- name: hello2v1
labels:
version: v1
- name: hello2v2
labels:
version: v2
Envoyfilter示意如下:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: hello1v2-propaganda-filter
spec:
workloadSelector:
labels:
app: hello1-deploy-v2
version: v2
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_OUTBOUND
proxy:
proxyVersion: "^1\\.8\\.*"
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
subFilter:
name: envoy.filters.http.router
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: propaganda_filter
root_id: propaganda_filter_root
configuration:
'@type': type.googleapis.com/google.protobuf.StringValue
value: |
{
"head_tag_name": "route-v",
"head_tag_value": "hello2v2"
}
vm_config:
runtime: envoy.wasm.runtime.v8
vm_id: propaganda_filter_vm
code:
local:
filename: /var/local/lib/wasm-filters/propaganda-header-filter.wasm
allow_precompiled: true
2 驗證方法
攜帶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}; do
curl -s -H "route-v:v2" "http://$ingressGatewayIp:$PORT/hello/eric" >>result
echo >>result
done
check=$(grep -o "Bonjour eric" result | wc -l)
if [[ "$check" -eq "15" ]]; then
echo "pass"
else
echo "fail"
exit 1
fi
result:
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_BEFORE
value:
name: envoy.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inlineCode: |
function envoy_on_request(handle)
handle:logInfo("[propagate header] route-v:hello3v2")
handle:headers():add("route-v", "hello3v2")
end
3.2 壓測方法
1 部署
- 分別在3個namespace上部署相同的Deployment/Service/VirtualService/DestinationRule
- 在
hello-abtest-lua
中部署基於Lua的EnvoyFilter - 在
hello-abtest-wasm
中部署基於WASM的EnvoyFilter
hello-abtest 基準環境
hello-abtest-lua 增加EnvoyFilter+LUA的環境
hello-abtest-wasm 增加EnvoyFilter+WASM的環境
2 工具
本例使用hey作爲壓測工具。hey前身是boom,用來代替ab(Apache Bench)。使用相同的壓測參數分別對三個環境進行壓測。示意如下:
# 併發work數量
export NUM=2000
# 每秒請求數量
export QPS=2000
# 壓測執行時常
export Duration=10s
hey -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個關鍵指標,如下圖所示:
3.4 結論
- 相對於基準版本,增加EnvoyFilter的兩個版本,平均延遲多出幾十個到幾百個毫秒,增加耗時比爲
- 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
- WASM版本的性能明顯優於LUA版本
注:相比LUA版本,WASM的實現是一套代碼多份配置。因此WASM的執行過程還比LUA多出一個獲取配置變量的過程。
4 展望
4.1 如何使用
本文從技術實現角度,講述瞭如何實現並驗證一個透傳用戶自定義Header的WASM,從而支持無侵入式全鏈路A/B Test這個需求。
但是,作爲服務網格的使用者,如果按照本文一步步去實現,是非常繁瑣且容易出錯的。
阿里雲服務網格ASM團隊正在推出一種ASM插件目錄的機制,用戶只需在插件目錄中選擇插件,併爲插件提供自定義的Header等極少數量的kv配置,即可自動生成和部署相關的EnvoyFilter+WASM+VirtualService+DestinationRule。
4.2 如何擴展
本例只展示了基於Header的匹配路由功能,如果我們希望根據Query Params進行匹配和路由該如何擴展呢?
這是ASM插件目錄正在密切關注的話題,最終插件目錄將提供最佳實踐。
以上。
本文爲阿里雲原創內容,未經允許不得轉載。