WebAssembly(WASM)是一种编程语言,ASM提供了对WASM技术的支持,可以把扩展的WASM Filter通过ASM部署到数据面集群中相应的Envoy代理中。通过这种过滤器扩展机制,可以轻松扩展Envoy的功能并将其在服务网格中的应用推向了新的高度。本文介绍了WASM Filter以及如何为Envoy编写WASM Filter并部署到ASM中。

背景信息

Envoy是一个高性能、可编程的L3、L4和L7代理,作为ASM数据面的代理使用。Envoy的连接和流量处理的核心是网络过滤器(Network Filter),该过滤器一旦融合进过滤器链(Filter Chain),就可以实现访问控制、数据或协议转换、数据增强、审计等高级功能。通过添加新的过滤器,可以用来扩展Envoy的已有功能集。当前有两种方法可以添加新的过滤器:
  • 静态预编译:将其他过滤器集成到Envoy的源代码中,并编译新的Envoy版本。这种方法的缺点是您需要维护Envoy版本,并不断使其与官方发行版保持同步。此外,由于Envoy是用C++实现的,因此新开发的过滤器也必须用C++实现。
  • 动态运行时加载:在运行时将新的过滤器动态加载到Envoy代理中。

显而易见,第二种方法极大地简化了扩展Envoy的过程。这种解决方案依赖于一种称之为WebAssembly(WASM)的新技术,它是一种有效的可移植二进制指令格式,提供了可嵌入和隔离的执行环境。

ASM提供了对WASM技术的支持,如下图所示。

Enovy.png

为什么要使用WASM Filter?

使用WASM实现过滤器的扩展,有如下优势:
  • 敏捷性:过滤器可以动态加载到正在运行的Envoy进程中,而无需停止或重新编译。
  • 可维护性:不必更改Envoy自身基础代码库即可扩展其功能。
  • 多样性:可以将流行的编程语言(例如C/C++和Rust)编译为WASM,因此开发人员可以选择实现过滤器的编程语言。
  • 可靠性和隔离性:过滤器会被部署到VM沙箱中,因此与Envoy进程本身是隔离的;即使当WASM Filter出现问题导致崩溃时,它也不会影响Envoy进程。
  • 安全性:过滤器通过预定义API与Envoy代理进行通信,因此它们可以访问并只能修改有限数量的连接或请求属性。
当前WASM实现过滤器的扩展,也需要考虑以下缺点是否可以容忍:
  • 性能约为C++编写的原生静态编译的Filter的70%。
  • 由于需要启动一个或多个WASM虚拟机,因此会消耗一定的内存使用量。

使用Proxy-WASM SDK构建过滤器

Envoy Proxy在基于堆栈的虚拟机中运行WASM过滤器,因此过滤器的内存与主机环境是隔离的。Envoy代理与WASM过滤器之间的所有交互都是通过Envoy Proxy-WASM SDK提供的功能实现的。Envoy Proxy-WASM SDK提供了多种编程语言的实现,包括C++、Rust、AssemblyScript以及处于实验中的Golang等。此外,社区也在推动相应的WASMfor Proxies (Proxy-Wasm)应用二进制接口ABI规范,详情请参见spec

  1. 构建WASM Filter的最简单方法是使用Docker,使用C++ Envoy Proxy-WASM SDK创建一个Docker镜像。详情请参见Docker
  2. 创建一个项目并使用上述Docker镜像进行构建。详情请参见Creating a project for use with the Docker build image
  3. 编辑开发此项目。详情请参见WebAssembly for Proxies (C++ SDK)
  4. 切换到项目根目录,执行如下命令构建WASM。
    docker run -v $PWD:/work -w /work  registry.cn-hangzhou.aliyuncs.com/acs/wasmsdk:v0.1 /build_wasm.sh

在ASM中部署启用WASM Filter

  1. 创建一个configmap,用于保存WASM过滤器的二进制文件内容。例如,在命名空间default下,创建一个名称为wasm-example-filter的configmap,并将WASM过滤器的二进制文件example-filter.wasm保存到该configmap中。
    kubectl create configmap -n default wasm-example-filter --from-file=example-filter.wasm
  2. 使用以下两个annotation将WASM过滤器的二进制文件注入到应用程序对应的Kubernetes服务中。
    sidecar.istio.io/userVolume: '[{"name":"wasmfilters-dir","configMap": {"name": "wasm-example-filter"}}]'
    sidecar.istio.io/userVolumeMount: '[{"mountPath":"/var/local/lib/wasm-filters","name":"wasmfilters-dir"}]'
  3. 执行以下命令更新productpage-v1。
    kubectl patch deployment productpage-v1 -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/userVolume":"[{\"name\":\"wasmfilters-dir\",\"configMap\": {\"name\": \"wasm-example-filter\"}}]","sidecar.istio.io/userVolumeMount":"[{\"mountPath\":\"/var/local/lib/wasm-filters\",\"name\":\"wasmfilters-dir\"}]"}}}}}'
  4. 执行以下命令更新details-v1。
    kubectl patch deployment details-v1 -p '{"spec":{"template":{"metadata":{"annotations":{"sidecar.istio.io/userVolume":"[{\"name\":\"wasmfilters-dir\",\"configMap\": {\"name\": \"wasm-example-filter\"}}]","sidecar.istio.io/userVolumeMount":"[{\"mountPath\":\"/var/local/lib/wasm-filters\",\"name\":\"wasmfilters-dir\"}]"}}}}}'
  5. 在istio-proxy容器中的路径/var/local/lib/wasm-filters下,找到WASM过滤器的二进制文件。
    kubectl exec -it deployment/productpage-v1 -c istio-proxy -- ls /var/local/lib/wasm-filters/
    kubectl exec -it deployment/details-v1 -c istio-proxy -- ls /var/local/lib/wasm-filters/
  6. 执行以下命令,使WASM过滤器在处理针对应用服务productpage的流量时,能够以DEBUG日志级别记录。
    kubectl port-forward deployment/productpage-v1 15000
    curl -XPOST "localhost:15000/logging?wasm=debug"
  7. 执行以下命令,使WASM过滤器在处理针对应用服务details-v1的流量时,能够以DEBUG日志级别记录。
    kubectl port-forward deployment/details-v1 15000
    curl -XPOST "localhost:15000/logging?wasm=debug"
  8. 执行以下命令,将WASM过滤器插入到应用服务productpage的HTTP级别过滤器链中。
    apiVersion: networking.istio.io/v1alpha3
    kind: EnvoyFilter
    metadata:
      name: productpage-v1-examplefilter
    spec:
      configPatches:
      - applyTo: HTTP_FILTER
        match:
          context: SIDECAR_INBOUND
          listener:
            filterChain:
              filter:
                name: envoy.http_connection_manager
                subFilter:
                  name: envoy.router
        patch:
          operation: INSERT_BEFORE
          value:
            config:
              config:
                name: example-filter
                rootId: my_root_id
                vmConfig:
                  code:
                    local:
                      filename: /var/local/lib/wasm-filters/example-filter.wasm
                  runtime: envoy.wasm.runtime.v8
                  vmId: example-filter
                  allow_precompiled: true
            name: envoy.filters.http.wasm
      workloadSelector:
        labels:
          app: productpage
          version: v1
  9. 执行以下命令,将WASM过滤器插入到应用服务details的HTTP级别过滤器链中。
    apiVersion: networking.istio.io/v1alpha3
    kind: EnvoyFilter
    metadata:
      name: details-v1-examplefilter
    spec:
      configPatches:
      - applyTo: HTTP_FILTER
        match:
          context: SIDECAR_INBOUND
          listener:
            filterChain:
              filter:
                name: envoy.http_connection_manager
                subFilter:
                  name: envoy.router
        patch:
          operation: INSERT_BEFORE
          value:
            config:
              config:
                name: example-filter
                rootId: my_root_id
                vmConfig:
                  code:
                    local:
                      filename: /var/local/lib/wasm-filters/example-filter.wasm
                  runtime: envoy.wasm.runtime.v8
                  vmId: example-filter
                  allow_precompiled: true
            name: envoy.filters.http.wasm
      workloadSelector:
        labels:
          app: details
          version: v1

验证结果

  1. 通过在浏览器中访问入口网关的地址,将一些流量发送到productpage服务上,在页面响应中,可以看到过滤器的头添加到响应头中,如下图所示。
    enovy2
  2. 执行以下命令,将一些流量发送到details服务上。在响应中,可以看到过滤器的头添加到响应头中。
    kubectl exec -ti  deploy/productpage-v1 -c istio-proxy -- curl -v http://details:9080/details/123
    *   Trying 172.31.13.58...
    * TCP_NODELAY set
    * Connected to details (172.31.13.58) port 9080 (#0)
    > GET /details/123 HTTP/1.1
    > Host: details:9080
    > User-Agent: curl/7.58.0
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    xxxxxxx
    < resp-header-demo: added by our filter
    xxxxx
    * Connection #0 to host details left intact
    xxxxx