简介:咱们都晓得,服务网格(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这个更高级的配置了。这是咱们一开始很不心愿的论断。起因有两个:

  1. EnvoyFilter的配置太过简单,个别用户很难在服务网格中疾速学习和应用,即使咱们提供示例,一旦需要稍有变动,示例对批改EnvoyFilter的参考价值甚微。
  2. 就算应用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 nightlycargo 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_filtertype.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-v1proxy_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.wasmwasm-gc ./target/wasm32-unknown-unknown/release/propaganda_filter.wasm ./target/wasm32-unknown-unknown/release/propaganda-header-filter.wasmls -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.wasmkubectl -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同时反对localremote模式的资源定义。比照如下:

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的散发,敬请期待。

相似地,业界其余友商也在推动这种形式,特地是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/env2/fr

每个Service配置了VirtualService和DestinationRule用来定义匹配Header并路由到指定版本。

apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata:  name: hello2-vsspec:  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/v1alpha3kind: DestinationRulemetadata:  name: hello2-drspec:  host: hello2-svc  subsets:    - name: hello2v1      labels:        version: v1    - name: hello2v2      labels:        version: v2

Envoyfilter示意如下:

apiVersion: networking.istio.io/v1alpha3kind: EnvoyFiltermetadata:  name: hello1v2-propaganda-filterspec:  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中指定了versionv2,那么全链路将
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 >>resultdonecheck=$(grep -o "Bonjour eric" result | wc -l)if [[ "$check" -eq "15" ]]; then    echo "pass"else    echo "fail"    exit 1fi

result:

Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182Bonjour eric@hello1:172.17.68.205<Bonjour eric@hello2:172.17.68.206<Bonjour eric@hello3:172.17.68.182Bonjour 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=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个要害指标,如下图所示:

基准WASMLUA
1000并发1000QPS继续10秒钟
均匀提早0.6317 secs0.6395 secs0.7012 secs
提早99%散布0.9167 secs0.9352 secs1.1355 secs
QPS154115191390
Total16281161091390
2000并发2000QPS继续10秒钟
均匀提早1.2078 secs1.3290 secs1.4593 secs
提早99%散布1.8621 secs1.8354 secs2.2116 secs
QPS156414211290
Total176221600914662

3.4 论断

  1. 绝对于基准版本,减少EnvoyFilter的两个版本,均匀提早多出几十个到几百个毫秒,减少耗时比为
  • wasm 1.2% (0.6395-0.6317)/0.63171% (1.3290-1.2078)/1.2078
  • lua 11%(0.7012-0.6317)/0.631720% (1.4593-1.2078)/1.2078
  1. 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插件目录正在亲密关注的话题,最终插件目录将提供最佳实际。

以上。

版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。