简介:咱们都晓得,服务网格 (ServiceMesh) 能够为运行其上的微服务提供无侵入式的流量治理能力。通过配置 VirtualService 和 DestinationRule,即可实现流量治理、超时重试、流量复制、限流、熔断等性能,而无需批改微服务代码。本文所述的实际是依据申请 Header 实现全链路 A / B 测试。
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/AliyunCont…
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/isti…
- https://github.com/proxy-wasm…
- https://www.elvinefendi.com/2…
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/que…
- https://envoyproxy.slack.com/…
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-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 个要害指标,如下图所示:
基准
WASM
LUA
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 论断
- 绝对于基准版本,减少 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 插件目录 正在亲密关注的话题,最终插件目录将提供最佳实际。
以上。
作者:六翁
原文链接
本文为阿里云原创内容,未经容许不得转载