美文网首页
基于WASM的无侵入式全链路A/B Test实践

基于WASM的无侵入式全链路A/B Test实践

作者: 六爷1_1 | 来源:发表于2021-02-25 18:25 被阅读0次

    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值路由到指定版本。

    image.png

    这是为什么呢?对于服务网格其上的微服务来说,这个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就会丢失。

    image.png

    因此,我们可以得出一个结论:无法单纯使用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配置并加载。(为什么使用这种分发形式,后面会讲。)

    image.png

    2 技术实现

    本节所述的相关代码:https://github.com/feuyeux/rust-wasm-4-envoy/tree/propaganda/propagate-headers-filter

    2.1 使用RUST实现WASM

    1 定义依赖

    WASM工程的核心依赖crates只有一个,就是proxy-wasm,这是使用Rust开发WASM的基础包。此外,还有用于反序列化的包serde_json和用于打印日志的包logCargo.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,如果你对此感兴趣可以阅读如下参考:

    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_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 conpomse:

    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 solo的wasme框架

    solo提供了一整套WASM的开发框架wasme,基于该框架可以开发-构建-分发WASM包(OCI image)并部署到

    Webassembly Hub。同时,该框架以CRD的形式对EnvoyFilter进行了封装,且可以让相关Pod从Webassembly Hub拉取WASM包。

    image.png

    这个方案的优点很明显,完整地支持了WASM的开发到上线的生命周期。但这个方案的缺点也非常明显,wasme的自包含导致了很难将其拆分,并扩展到solo体系之外。

    wasme的方向是正确的,比如通过CRD来封装EnvoyFilter以及Webassembly Hub这种WASM中央仓库的思路。我的观点是,各大厂商顺着这个思路发展下去,提供成熟的定制和插拔能力后,WASM分发的终态就会浮出水面。

    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分发的未来方向。因此,本例最终放弃这种方式。

    3 Configmap+Envoy的local方式

    虽然这种方式不是WASM分发的终态,但是本例最终选择了这个方案。虽然configmap的本职工作不是存WASM的,但是configmap和Envoy的local模式都很成熟,两者结合恰能满足当前需求。

    要把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.4 集群验证(基于Istio)

    1 实验示例

    WASM分发到Kubernetes的configmap后,我们可以进行集群验证了。示例(源代码)包含3个Service:hello1-hello2-hello3,每个服务包含2个版本:v1/env2/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中指定了versionv2,那么全链路将
    hello1 v2-hello2 v2-hello3 v2。效果如下图所示。

    image.png

    验证过程和结果示意如下。

    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
    
    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个关键指标,如下图所示:

    image.png
    基准 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 结论

    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这个需求。

    但是,作为服务网格的使用者,如果按照本文一步步去实现,是非常繁琐且容易出错的。

    服务网格的提供者应当提供一种类似solo wasme的方式,将功能所需的配置进行封装,并发布到插件目录。用户只需在插件目录中选择插件,并为插件提供自定义的Header等极少数量的kv配置,即可自动生成和部署相关的EnvoyFilter+WASM+VirtualService+DestinationRule。

    4.2 如何扩展

    本例只展示了基于Header的匹配路由功能,如果我们希望根据Query Params进行匹配和路由该如何扩展呢?

    一种方式是开发并替换本例中的WASM,一种方式是由服务网格的提供者发布更多的插件目录。前者更灵活,但非托管无法保证稳定性和兼容性;后者更健壮,但会有滞后。

    以上。

    相关文章

      网友评论

          本文标题:基于WASM的无侵入式全链路A/B Test实践

          本文链接:https://www.haomeiwen.com/subject/mmvufltx.html