使用 WebAssembly 对 Istio 进行扩展

2023年 12月 12日 46.1k 0

WebAssembly(简称为 Wasm)的诞生源自前端,是一种为了解决日益复杂的 Web 前端应用以及有限的 JavaScript 性能而诞生的技术。它本身并不是一种语言,而是一种字节码标准。WASM 字节码和机器码非常接近,因此可以非常快速的装载运行。任何一种语言,都可以被编译成 WASM 字节码,然后在 WASM 虚拟机中执行,理论上,所有语言,包括 JavaScript、C、C++、Rust、Go、Java 等都可以编译成 WASM 字节码并在 WASM 虚拟机中执行。当然不仅可以嵌入浏览器增强 Web 应用,也可以应用于其他的场景。

WebAssembly

WebAssembly 是为下列目标而生的:

  • 快速、高效、可移植 —— 通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
  • 可读、可调试 —— WebAssembly 是一门低阶语言,但是它也有一种人类可读的文本格式,这允许通过手工来写代码,看代码以及调试代码。
  • 保持安全 —— WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
  • 不破坏网络 —— WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。

Istio WASM

对于 Istio 来说,WebAssembly 也使得 Istio 的扩展能力得到了极大的提升,Isstio 从 1.12 版本开始引入 WASM 扩展 Envoy,当你需要添加 Envoy 或 Istio 不支持的自定义功能时,那么我们就可以使用 Wasm 插件,比如使用 Wasm 插件来添加自定义验证、认证、日志或管理配额等等。

Envoy 架构

首先我们再回顾下 Envoy 的过滤机制,Envoy 通过过滤器来实现各种功能,比如路由、负载均衡、TLS、认证、日志、监控等等。Envoy 提供了进程外架构、支持 L3/L4 filter、HTTP L7 filter,过滤器包括侦听器过滤器(Listener Filters)、网络过滤器(Network Filters)、HTTP 过滤器(HTTP Filters)三种类型。

侦听器过滤器

侦听器过滤器在初始连接阶段访问原始数据并操作 L4 连接的元数据。例如,TLS 检查器过滤器标识连接是否经过 TLS 加密,并解析与该连接关联的 TLS 元数据;HTTP Inspector Filter 检测应用协议是否是 HTTP,如果是的话,再进一步检测 HTTP 协议类型 (HTTP/1.x or HTTP/2) ,这两种过滤器解析到的元数据都可以和 FilterChainMatch 结合使用。

网络过滤器

网络过滤器访问和操作 L4 连接上的原始数据,即 TCP 数据包。例如,TCP 代理过滤器将客户端连接数据路由到上游主机,它还可以生成连接统计数据。此外,MySQL proxy、Redis proxy、Dubbo proxy、Thrift proxy 等都属于网络过滤器。

HTTP 过滤器

HTTP 过滤器在 L7 上运行,由网络过滤器(即 HTTP 连接管理器,HTTP Connection Manager)创建。这些过滤器用于访问、操作 HTTP 请求和响应,例如,gRPC-JSON 转码器过滤器可以为 gRPC 后端提供一个 REST API,并将请求和响应转换为相应的格式。此外,还包括 JWT、Router、RBAC 等多种过滤器。

WASM 插件

有很多编程语言都支持编写 WASM 插件,比如 C、C++、Rust、Go、Java 等等,这里我们以 Go 语言为例来编写一个简单的 WASM 插件,编写 WASM 的工具有 Solo.io 团队的 wasme、tinygo等,目前应用比较多是 tinygo,tinygo 支持的包可以查看 https://tinygo.org/docs/reference/lang-support/stdlib/ 进行了解。

TinyGo 是 Go 编程语言规范的一个编译器实现,为什么不使用官方的 Go 编译器?目前官方编译器无法生成可以在浏览器外部运行的 WASM 二进制文件,因此也无法生成与 Proxy-Wasm 兼容的二进制文件。

Proxy-Wasm

Proxy-Wasm是开源社区针对「网络代理场景」设计的一套 ABI 规范,定义了网络代理和运行在网络代理内部的 Wasm 虚拟机之间的接口,属于当前的事实规范。当前支持该规范的网络代理软件包括 Envoy、MOSN 和 ATS(Apache Traffic Server),支持该规范的 Wasm 扩展 SDK 包括 C++、Rust 和 Go。采用该规范的好处在于能让 Wasm 扩展程序在不同的网络代理产品上运行,比如 MOSN 的 Wasm 扩展程序可以运行在 Envoy 上,而 Envoy 的 Wasm 扩展程序也可以运行在 MOSN 上。

Proxy-Wasm 规范定义了宿主机与 Wasm 扩展程序之间的交互细节,包括 API 列表、函数调用规范以及数据传输规范这几个方面。其中,API 列表包含了 L4/L7、property、metrics、日志等方面的扩展点,涵盖了网络代理场景下所需的大部分交互点。目前实现该规范的 Wasm 扩展 SDK 包括 AssemblyScript、C++、Rust 和 Go:

  • AssemblyScript SDK
  • C++ SDK
  • Go (TinyGo) SDK
  • Rust SDK

为了方便,我们也直接选择已有的 proxy-wasm-go-sdk 这个 SDK 进行开发。这个 Proxy-Wasm Go SDK 是用于使用 Go 编程语言在 Proxy-Wasm ABI 规范之上扩展网络代理(例如 Envoyproxy)的 SDK,有了这个 SDK,每个人都可以轻松地生成与 Proxy-Wasm 规范兼容的 Wasm 二进制文件,而无需了解低级且对于没有专业知识的人来说难以理解的 Proxy-Wasm ABI 规范。

环境准备

首先安装 tinygo 工具,前往 https://github.com/tinygo-org/tinygo/releases/tag/v0.30.0 下载对应的版本,比如我们这里是 Linux 系统,可以使用下面的命令进行安装:

# linux
$ wget https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo0.30.0.linux-amd64.tar.gz
$ tar -xvf tinygo0.30.0.linux-amd64.tar.gz
$ export PATH=$PATH:~/tinygo/bin
$ tinygo version
tinygo version 0.30.0 linux/amd64 (using go version go1.17 and LLVM version 16.0.1)

当然我们也可以直接使用 docker 镜像来进行编译。

编写插件

接下来我们就可以来编写一个 WASM 插件,这里我们将包含一个 Envoy 过滤器,将来自 http://service/banana/X 的请求重定向到 http://service/status/X,这里我们使用 Go 语言来编写插件,首先初始化项目:

$ mkdir wasm-go-demo && cd wasm-go-demo
$ go mod init github.com/cnych/wasm-go-demo
$ go get github.com/tetratelabs/proxy-wasm-go-sdk

然后创建 main.go 文件,内容如下:

package main

import (
 "regexp"

 "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
 "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

type vmContext struct {
 // 嵌入默认的 VM 上下文,这样我们就不需要重新实现所有方法
 types.DefaultVMContext
}

func (ctx *vmContext) NewPluginContext(contextID uint32) types.PluginContext {
 return &pluginContext{}
}

type pluginContext struct {
 // 嵌入默认的插件上下文,这样我们就不需要重新实现所有方法
 types.DefaultPluginContext

 pattern     string
 replaceWith string
 configData  string // 保存插件的一些配置信息
}

// 注入额外的 Header
var additionalHeaders = map[string]string{
 "who-am-i":    "go-wasm-demo",
 "injected-by": "istio-api!",
 "site":        "youdianzhishi.com",
 "author":      "阳明",
 // 定义自定义的header,每个返回中都添加以上header
}

// NewHttpContext 为每个 HTTP 请求创建一个新的上下文。
func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
 return &httpRegex{
  contextID:     contextID,
  pluginContext: ctx,
 }
}

// OnPluginStart 在插件被加载时调用。
func (ctx *pluginContext) OnPluginStart(pluginCfgSize int) types.OnPluginStartStatus {
 proxywasm.LogWarnf("regex/main.go OnPluginStart()")
 // 获取插件配置
 data, err := proxywasm.GetPluginConfiguration()
 if data == nil {
  return types.OnPluginStartStatusOK
 }
 if err != nil {
  proxywasm.LogWarnf("failed read plug-in config: %v", err)
  return types.OnPluginStartStatusFailed
 }

 proxywasm.LogWarnf("read plug-in config: %sn", string(data))

 // 插件启动的时候读取配置
 ctx.configData = string(data)
 ctx.pattern = "banana/([0-9]*)"
 ctx.replaceWith = "status/$1"

 return types.OnPluginStartStatusOK
}

// OnPluginDone 在插件被卸载时调用。
func (ctx *pluginContext) OnPluginDone() bool {
 proxywasm.LogWarnf("regex/main.go OnPluginDone()")
 return true
}

type httpRegex struct {
 // 嵌入默认的 HTTP 上下文,这样我们就不需要重新实现所有方法
 types.DefaultHttpContext
 // contextID 是插件上下文的 ID,它是唯一的。
 contextID     uint32
 pluginContext *pluginContext
}

// OnHttpResponseHeaders 在收到 HTTP 响应头时调用。
func (ctx *httpRegex) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
 proxywasm.LogWarnf("%d httpRegex.OnHttpResponseHeaders(%d, %t)", ctx.contextID, numHeaders, endOfStream)

 // 添加 Header
 for k, v := range additionalHeaders {
  if err := proxywasm.AddHttpResponseHeader(k, v); err != nil {
   proxywasm.LogWarnf("failed to add response header %s: %v", k, err)
  }
 }

 //为了便于演示观察,将配置信息也加到返回头里
 proxywasm.AddHttpResponseHeader("configData", ctx.pluginContext.configData)
 return types.ActionContinue
}

// OnHttpRequestHeaders 在收到 HTTP 请求头时调用。
func (ctx *httpRegex) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
 proxywasm.LogWarnf("%d httpRegex.OnHttpRequestHeaders(%d, %t)", ctx.contextID, numHeaders, endOfStream)

 re := regexp.MustCompile(ctx.pluginContext.pattern)
 replaceWith := ctx.pluginContext.replaceWith

 s, err := proxywasm.GetHttpRequestHeader(":path")
 if err != nil {
  proxywasm.LogWarnf("Could not get request header: %v", err)
 } else {
  result := re.ReplaceAllString(s, replaceWith)
  proxywasm.LogWarnf("path: %s, result: %s", s, result)

  err = proxywasm.ReplaceHttpRequestHeader(":path", result)
  if err != nil {
   proxywasm.LogWarnf("Could not set request header to %q: %v", result, err)
  }
 }

 return types.ActionContinue
}

func (ctx *httpRegex) OnHttpStreamDone() {
 proxywasm.LogWarnf("%d OnHttpStreamDone", ctx.contextID)
}

func main() {
 proxywasm.LogWarnf("regex/main.go main() REACHED")
 // 设置 VM 上下文,这样我们就可以在插件启动时读取配置。
 proxywasm.SetVMContext(&vmContext{})
}

在上面的代码中,我们主要关注 pluginContext 和 httpRegex 这两个结构体,其中 pluginContext 结构体主要用于插件的初始化,而 httpRegex 结构体主要用于处理 HTTP 请求,这里我们主要关注 OnHttpRequestHeaders 和 OnHttpResponseHeaders 这两个方法,这两个方法分别用于处理 HTTP 请求头和响应头,我们在这两个方法中添加了一些自定义的 Header,然后在 Istio 中就可以看到这些 Header 了。

部署插件

代码编写完成后,我们就可以使用 tinygo 来编译了,执行以下命令:

tinygo build -o main.wasm -scheduler=none -target=wasi main.go

上面的命令会生成一个 main.wasm 文件,这个文件就是我们的 WASM 插件,接下来我们就可以将这个插件部署到 Istio 中了。

部署 WASM

我们可以将这个 main.wasm 文件放到一个 ConfigMap 中,然后挂载到 Envoy 中,这样就可以在 Envoy 中使用了,比如我们可以使用下面的命令来创建一个 ConfigMap:

kubectl create configmap new-filter --from-file=new-filter.wasm=main.wasm

然后接下来我们以 httpbin 为例来测试下,这里我们需要修改下 httpbin 的部署文件,将 ConfigMap 挂载到 Envoy 中。

部署 WASM

修改后的部署文件如下所示:

# httpbin.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: httpbin
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
  labels:
    app: httpbin
    service: httpbin
spec:
  ports:
    - name: http
      port: 8000
      targetPort: 80
  selector:
    app: httpbin
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
      annotations:
        # 不能在容器上使用 volume 挂载,因为它来自 injector。
        # NOTE: 我们这个示例始终挂在 "new-filter" ConfigMap 到 /var/local/wasm/new-filter.wasm
        sidecar.istio.io/userVolume: '[{"name":"new-filter","configMap":{"name":"new-filter"}}]'
        sidecar.istio.io/userVolumeMount: '[{"mountPath":"/var/local/wasm","name":"new-filter"}]'
    spec:
      serviceAccountName: httpbin
      containers:
        - image: docker.io/kennethreitz/httpbin
          imagePullPolicy: IfNotPresent
          name: httpbin
          ports:
            - containerPort: 80

直接应用上面的资源对象即可:

kubectl apply -f httpbin.yaml
# 当然要需要创建 VirtualService,在 Istio 根目录下面
kubectl apply -f samples/httpbin/httpbin-gateway.yaml

部署完成后接下来我们先访问下 httpbin 服务,看下是否正常:

$ export GATEWAY_URL=$(kubectl get po -l istio=ingressgateway -n istio-system -o 'jsnotallow={.items[0].status.hostIP}'):$(kubectl get svc istio-ingressgateway -n istio-system -o 'jsnotallow={.spec.ports[?(@.name=="http2")].nodePort}')
$ curl -v http://$GATEWAY_URL/status/418
> GET /status/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.0.20:31896
> Accept: */*
>
< HTTP/1.1 418 Unknown
< server: istio-envoy
< date: Fri, 08 Dec 2023 07:28:57 GMT
< x-more-info: http://tools.ietf.org/html/rfc2324
< access-control-allow-origin: *
< access-control-allow-credentials: true
< content-length: 135
< x-envoy-upstream-service-time: 2
<

    -=[ teapot ]=-

       _...._
     .'  _ _ `.
    | ."` ^ `". _,
    _;`"---"`|//
      |       ;/
      _     _/
        `"""`

上面我们编写的插件逻辑是当我们访问 http://service/banana/X 时,会将请求重定向到 http://service/status/X,所以我们可以使用下面的命令来测试下:

$ curl -v http://$GATEWAY_URL/banana/418
> GET /banana/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.0.20:31896
> Accept: */*
>
< HTTP/1.1 404 Not Found
< server: istio-envoy
< date: Fri, 08 Dec 2023 07:30:19 GMT
< content-type: text/html
< content-length: 233
< access-control-allow-origin: *
< access-control-allow-credentials: true
< x-envoy-upstream-service-time: 9
<

404 Not Found
Not Found

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

从结果可以看到,我们定义的插件并没有生效,其实也能预料到,现在我们只是将编译后的 WASM 文件挂载到了 Envoy 中,但是 Envoy 并不知道这个文件是用来做什么的,或者说 Envoy 并不知道要将这个文件当成 WASM 插件来使用。

$ kubectl get pods -l app=httpbin
NAME                       READY   STATUS    RESTARTS   AGE
httpbin-55db5999b4-qtlcg   2/2     Running   0          8m23s
$ kubectl exec -it httpbin-55db5999b4-qtlcg -c istio-proxy -- ls /var/local/wasm
new-filter.wasm

wasm 插件

这个时候我们还需要使用到一个名为 EnvoyFilter 的 CRD 资源对象,EnvoyFilter 提供了一种机制,可以自定义 Istio Pilot 生成的 Envoy 配置,使用 EnvoyFilter 可以修改某些字段的值、添加特定的过滤器,甚至添加全新的监听器、集群等。需要注意的是这个功能必须小心使用,因为不正确的配置可能会导致整个网格不稳定。与其他 Istio 网络对象不同,EnvoyFilters 是附加应用的。对于特定命名空间中给定工作负载来说,可以存在任意数量的 EnvoyFilters,这些 EnvoyFilters 的应用顺序如下:首先是配置根命名空间中所有的 EnvoyFilters,然后是工作负载所在命名空间中匹配到的所有 EnvoyFilters。

比如我们可以使用下面的配置来将我们的 WASM 插件挂载到 Envoy 中:

# httpbin-wasm-filter.yaml
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: httpbin-wasm-filter
spec:
  workloadSelector:
    labels:
      app: httpbin
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: SIDECAR_INBOUND # 仅对入站流量进行过滤
        listener:
          filterChain:
            filter:
              name: envoy.filters.network.http_connection_manager
              subFilter:
                name: envoy.filters.http.router
      patch:
        operation: INSERT_BEFORE # 在 router 之前插入
        value:
          name: mydummy
          typed_config:
            "@type": type.googleapis.com/udpa.type.v1.TypedStruct
            type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
            value:
              config:
                configuration:
                  "@type": type.googleapis.com/google.protobuf.StringValue
                  value: dummy
                root_id: "regex_replace"
                vm_config:
                  code:
                    local:
                      filename: /var/local/wasm/new-filter.wasm
                  runtime: envoy.wasm.runtime.v8
                  vm_id: myvmdummy

上面的 EnvoyFilter 对象中,首先我们通过 workloadSelector 来指定要对哪些 Pod 进行过滤,这里我们指定了 app: httpbin,表示只对 httpbin 这个 Pod 进行过滤,然后主要关注 configPatches 字段,这个字段用于配置 Envoy 的过滤器,其中的 match 字段用于匹配 Envoy 的过滤器,这里我们匹配的是 envoy.filters.network.http_connection_manager,然后 patch 字段用于指定要挂载的 WASM 插件,在 value.config 中指定了插件的配置信息以及 WASM 插件的路径。

然后直接应用上面的这个资源对象即可:

kubectl apply -f httpbin-wasm-filter.yaml

部署完成后我们可以先查看 httpbin 应用的 sidecar 日志:

$ kubectl logs -f httpbin-55db5999b4-qtlcg -c istio-proxy
# ......
2023-12-08T07:48:03.605802Z     warning envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1151 wasm log: regex/main.go main() REACHED  thread=25
2023-12-08T07:48:03.606902Z     warning envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1151 wasm log regex_replace myvmdummy: read plug-in config: dummy
        thread=25
2023-12-08T07:48:03.729847Z     info    Readiness succeeded in 1.025422807s
2023-12-08T07:48:03.730148Z     info    Envoy proxy is ready

正常现在就可以看到上面我们在插件中添加的一些日志了,然后我们再来测试下 httpbin 服务,访问 http://service/banana/X,看下是否能够正常重定向到 http://service/status/X:

$ curl -v http://$GATEWAY_URL/banana/418
> GET /banana/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.0.20:31896
> Accept: */*
>
< HTTP/1.1 418 Unknown
< server: istio-envoy
< date: Fri, 08 Dec 2023 08:11:17 GMT
< x-more-info: http://tools.ietf.org/html/rfc2324
< access-control-allow-origin: *
< access-control-allow-credentials: true
< content-length: 135
< x-envoy-upstream-service-time: 10
< who-am-i: go-wasm-demo
< injected-by: istio-api!
< site: youdianzhishi.com
< author: 阳明
< configdata: dummy
<

    -=[ teapot ]=-

       _...._
     .'  _ _ `.
    | ."` ^ `". _,
    _;`"---"`|//
      |       ;/
      _     _/

从上面的结果可以看到,我们的插件已经生效了,当我们访问 http://service/banana/X 时,会将请求重定向到 http://service/status/X,并且在响应头中添加了我们定义的一些 Header。到这里我们就实现了一个简单的 WASM 插件,当然这个插件只是一个简单的示例,实际上我们可以实现更加复杂的逻辑,比如可以实现自定义认证、自定义日志、自定义路由等等。

WasmPlugin API

需要注意的是这里我们的部署方式是创建一个包含已编译的 Wasm 插件的 ConfigMap,将 ConfigMap 挂载到 Pod 的 Envoy Sidecar 中去,然后通过 EnvoyFilter 配置 Envoy,从本地文件加载 Wasm 插件。这种方法的确可以实现我们的需求,但是配置 EnvoyFilter 对象有点复杂,功能丰富的 Wasm 插件可能超出 ConfigMap 1MB 的大小限制。为了解决这个问题,Istio 便引入了一个新的用于自定义 Wasm 插件对 Istio 代理功能进行扩展的新顶层 API - WasmPlugin CRD,不再需要使用 EnvoyFilter 资源向代理添加自定义 Wasm 模块,取而代之的是使用 WasmPlugin 资源:

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: your-filter
spec:
  selector:
    matchLabels:
      app: server
  phase: AUTHN
  priority: 10
  pluginConfig:
    someSetting: true
    someOtherSetting: false
    youNameIt:
      - first
      - second
  url: docker.io/your-org/your-filter:1.0.0

WasmPlugin 和 EnvoyFilter 之间有不少相似的地方,但也存在一些不同之处。比如在上面的示例中是将 Wasm 模块部署到与 selector 字段匹配的所有工作负载 —— 这与 EnvoyFilter 是完全相同的。

接下来的字段是 phase,该字段决定了 Wasm 模块将被注入到代理过滤器链中的哪个位置。我们为其定义了四个不同的阶段:

  • AUTHN:在所有 Istio 身份验证和授权过滤器之前。
  • AUTHZ:在 Istio 身份验证过滤器之后以及所有一级授权过滤器之前,即应用于 AuthorizationPolicy 资源之前。
  • STATS:在所有授权过滤器之后,以及 Istio 统计过滤器之前。
  • UNSPECIFIED_PHASE:让控制平面决定注入的位置,通常位于过滤器链的末端,也就是在路由之前。这也是该 phase 字段的默认值。

pluginConfig 字段用于 Wasm 插件的具体配置。在此字段中输入的任何内容都将通过 JSON 格式进行编码并传递到过滤器中,我们可以在 Proxy-Wasm SDK 的配置回调中访问它,比如在 Go SDK 中的 OnPluginStart 回调中可以获取这些配置信息。

url 字段指定了 Wasm 模块的拉取位置,这里的 url 是一个 docker URI,除了通过 HTTP、HTTPS 和本地文件系统 (使用 file://)方式加载 Wasm 模块之外,还可以使用 OCI 镜像格式作为分发 Wasm 插件,这也是推荐的方式。

接下来我们按照上面的要求在代码根目录中新建一个 Dockerfile,用来将我们的 Wasm 插件打包到 Docker 镜像中:

# Dockerfile for building "compat" variant of Wasm Image Specification.
# https://github.com/solo-io/wasm/blob/master/spec/spec-compat.md

FROM scratch

COPY main.wasm ./plugin.wasm

当然也可以将构建的动作放到 Dockerfile 中,进行多阶段构建:

FROM tinygo/tinygo as build
WORKDIR /src

COPY . .
RUN go env -w GOPROXY=https://goproxy.cn,direct
RUN tinygo build -o main.wasm -scheduler=none -target=wasi ./main.go

FROM scratch

COPY --from=build /src/main.wasm ./plugin.wasm

直接使用 docker build 命令来构建镜像即可,构建完成后推送到镜像仓库:

docker build -t cnych/wasm-go-demo:v0.1 .
docker.io/cnych/wasm-go-demo:v0.1

接下来就可以使用 WasmPlugin 资源对象来部署我们的 WASM 插件了,创建如下所示的 WasmPlugin 资源对象:

# httpbin-wasm-plugin.yaml
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: httpbin-wasm-plugin
  namespace: default
spec:
  selector:
    matchLabels:
      app: httpbin
  url: oci://docker.io/cnych/wasm-go-demo:v0.1
  pluginConfig:
    testConfig: abcd
    website: youdianzhishi.com
    listconfig:
      - abc
      - def

先删除之前的 EnvoyFilter 资源:

$ kubectl delete cm new-filter
configmap "new-filter" deleted
$ kubectl delete envoyfilter httpbin-wasm-filter
envoyfilter.networking.istio.io "httpbin-wasm-filter" deleted

记得将 httpbin.yaml 中的 sidecar.istio.io/userVolume 和 sidecar.istio.io/userVolumeMount 字段删除:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
      # annotations: # 去掉这里的注解
    spec:
      serviceAccountName: httpbin
      containers:
        - image: docker.io/kennethreitz/httpbin
          imagePullPolicy: IfNotPresent
          name: httpbin
          ports:
            - containerPort: 80

然后直接应用上面的 WasmPlugin 资源对象:

$ kubectl apply -f httpbin-wasm-plugin.yaml
$ kubectl get wasmplugins
NAME                  AGE
httpbin-wasm-plugin   5s

部署完成后我们可以先查看 httpbin 应用的 sidecar 日志:

$ kubectl logs -f httpbin-86869bccff-6wqdc -c istio-proxy
# ......
2023-12-08T08:43:11.360127Z     warning envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1151 wasm log: regex/main.go main() REACHED  thread=25
2023-12-08T08:43:11.360757Z     warning envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1151 wasm log: regex/main.go OnPluginStart() thread=25
2023-12-08T08:43:11.360895Z     warning envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1151 wasm log: read plug-in config: {"listconfig":["abc","def"],"testConfig":"abcd","website":"youdianzhishi.com"}
        thread=25
2023-12-08T08:43:12.692961Z     info    Readiness succeeded in 6.774251572s
2023-12-08T08:43:12.693263Z     info    Envoy proxy is ready

可以看到我们的插件已经生效了,然后我们再来测试下 httpbin 服务,访问 http://service/banana/X,看下是否能够正常重定向到 http://service/status/X:

$ curl -v http://$GATEWAY_URL/banana/418
> GET /banana/418 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: 192.168.0.20:31896
> Accept: */*
>
< HTTP/1.1 418 Unknown
< server: istio-envoy
< date: Fri, 08 Dec 2023 08:45:29 GMT
< x-more-info: http://tools.ietf.org/html/rfc2324
< access-control-allow-origin: *
< access-control-allow-credentials: true
< content-length: 135
< x-envoy-upstream-service-time: 8
< who-am-i: go-wasm-demo
< injected-by: istio-api!
< site: youdianzhishi.com
< author: 阳明
< configdata: {"listconfig":["abc","def"],"testConfig":"abcd","website":"youdianzhishi.com"}
<

    -=[ teapot ]=-

       _...._
     .'  _ _ `.
    | ."` ^ `". _,
    _;`"---"`|//
      |       ;/
      _     _/
        `"""`

从上面的结果可以看到结果是符合我们的预期的,证明我们的插件已经生效了。

性能数据

参考阿里云性能测试结果(仅供参考):

1000 并发 1000QPS 持续 10 秒钟

基准

WASM

LUA

平均延迟

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

相对于基准版本,增加 Wasm 插件的两个版本,平均延迟多出几十个到几百个毫秒,增加耗时比为:

  • 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 版本。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论