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