背景信息
2019年前,Envoy是以静态编译的二进制文件方式来运行的,这意味着其所有扩展等均需要在构建阶段完成编译。因此其他工程(例如Istio)只能发布他们自己维护的自定义Envoy版本,一旦有更新或者Bug修复就不得不构建一个新的二进制版本、发布、重新部署到生产环境中。
上述问题虽然没有一个完美的解决方案,但是部分场景下可以通过C++的动态可加载性来实现,即在一种标准的二进制应用接口(ABI, Application Binary Interface)下编写、交付WebAssembly(WASM)模块,
WASM 本身是源自前端的技术,是为了解决日益复杂的前端 Web 应用以及有限的 JS 脚本解释性能而诞生的技术,通过该技术可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行。
随着WASM的发展,现在WASM不仅仅可以用于浏览器, 它已经被定义为一个可移植、体积小、加载快并且兼容 Web 的全新格式为一种可移植的二进制格式。
本文讨论的WASM用于以接近本机的速度在一个内存安全(memory-safe)的沙箱中执行多种语言编写的代码,在沙箱内有明确的资源限制和API来与内嵌的主机环境(例如Envoy)通信。
优点
-
敏捷性:WASM可以动态加载到正在运行的Envoy进程中,而无需停止或重新编译 -
可维护性:不必更改Envoy自身基础代码库即可扩展其功能 -
多样性:可以将流行的编程语言(例如C/C++和TinyGo)编译为WASM,因此开发人员可以选择实现过滤器的编程语言 -
可靠性和隔离性:过滤器会被部署到VM沙箱中,因此与Envoy进程本身是隔离的;即使当WasmFilter出现问题导致崩溃时,它也不会影响Envoy进程 -
安全性:过滤器通过预定义API与Envoy代理进行通信,因此它们可以访问并只能修改有限数量的连接或请求属性
缺点
Proxy-wasm
Proxy-Wasm是WASM扩展模块与L4/L7代理之间的二进制应用接口(ABI)规范与标准,其明确定义了主机环境与Wasm虚拟机之间、函数调用、内存管理等通信接口。
当前Proxy-wasm提供了AssemblyScript SDK、C++ SDK、Go (TinyGo) SDK、Rust SDK、Zig SDK SDK,支持Envoy、Istio Proxy (Istio基于Envoy的扩展)、MOSN等代理主机环境中运行。
整体架构
在每个Envoy工作线程上(事件驱动),内置的WASM运行时将创建一个Wasm虚拟机,通过Proxy-Wasm 规范来校验、实例化WASM模块(本地磁盘文件或控制面板XDS推送的方式)。
WASM模块通过扩展接口进行调用时,Proxy-Wasm 通过一个垫片进行转码、翻译在Wasm虚拟机上运行。
注: Envoy 使用单进程 - 多线程 的架构模型。一个 master 线程管理各种琐碎的任务,而一些 worker 线程则负责执行监听、过滤和转发。当监听器接收到一个连接请求时,该连接将其生命周期绑定到一个单独的 worker 线程。
运行时
Envoy内嵌了基于LLVM的WAVM 与V8 两个C/C++ Wasm 运行时,在WASM模块配置时可进行选择。
Proxy-wasm-go-sdk
Go (TinyGo) SDK是一种基于Tinygo语言的Proxy-Wasm 实现。
本文基于该项目标签v0.14.0 进行阐述。
TinyGo
TinyGo是一个Go编译器,旨在用于微控制器,WebAssembly(WASM)和命令行工具等小型场景。它重用了Go语言工具和LLVM一起使用的库,以提供编译用Go编程语言编写的程序的另一种方法。
官方的Go编译器无法产生Proxy-Wasm 所兼容的二进制文件,并且TinyGo相比较于Go另一个最主要的的差异是二进制尺寸。
根据TinyGo官方描述,最简单的"Hello world"程序,在strip 命令加持下(移除所有符号标志与调试信息),Go编译器产生837kb大小的二进制,而TinyGo则为10kb,接近于1%的尺寸缩减效率。
使用TinyGo也存在一些限制和约束:
虽然不支持创建协程,但是Proxy-Wasm 定义了OnTick 函数,类似于定时器触发函数,可用于处理一些异步调用任务。
术语
-
虚拟机(Wasm VM): Envoy中Wasm虚拟机是在每个工作线程中被创建的,并且相互之间隔离 -
插件(Plugin): Envoy中的过滤器类型(Http Filter, Network(Tcp) Filter, 和 Wasm Service)。每个扩展模块都可以进行配置,单个虚拟机中相同的Wasm模块经过不同的配置便形成了多个插件
-
Http Filter: 在工作线程虚拟机中处理HTTP协议,可操作Http请求的头、正文、尾部内容 -
Network Filter: 在工作线程虚拟机中处理Tcp协议,可操作Tcp数据帧,连接信息 -
Wasm Service: 运行于Envoy主线程的单例虚拟中,可用于并发处理一些额外的任务如集成metrics,日志等
Envoy配置
Envoy中Wasm过滤器配置如下:
vm_config:
vm_id: "foo"
runtime: "envoy.wasm.runtime.v8"
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: '{"my-vm-env": "dev"}'
code:
local:
filename: "example.wasm"
configuration:
"@type": type.googleapis.com/google.protobuf.StringValue
value: '{"my-plugin-config": "bar"}'
字段 | 描述 |
---|
vm_config | 配置Wasm虚拟机 | vm_config.vm_id | 虚拟机的id,可用于配置跨虚拟机通信 | vm_config.runtime | Wasm 运行时类型,如: envoy.wasm.runtime.v8 . | vm_config.configuration | 虚拟机配置,可在运行时动态读取以便配置不同的虚拟机上下文 | vm_config.code | Wasm二进制文件位置 | configuration | 插件配置,可在运行时动态读取以便配置不同的插件上下文 |
字段vm_config 中所有属性为相同值时,则多个插件共享一个Wasm虚拟机,这在资源使用及启动时延上会有一定影响。
Http Filter
处理HTTP事件,即http协议流量,插件引用为envoy.filter.http.wasm ,示例如下:
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
vm_config: { ... }
# ... plugin config follows
- name: envoy.filters.http.router
Network Filter
处理TCP事件,即所有tcp流量(包括http流量),插件引用为envoy.filter.network.wasm ,示例如下:
filter_chains:
- filters:
- name: envoy.filters.network.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
config:
vm_config: { ... }
# ... plugin config follows
- name: envoy.tcp_proxy
注: Http Filter 与Network Filter 之间的差别仅仅是通过不同的配置,分别作用于TCP流或HTTP流。
Wasm Service
工作于主线程,配置在bootstrap_extensions 中,插件引用为envoy.bootstrap.wasm ,示例如下:
bootstrap_extensions:
- name: envoy.bootstrap.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.wasm.v3.WasmService
singleton: true
config:
vm_config: { ... }
# ... plugin config follows
其中singleton 属性通常配置为true 代表重用主线程的虚拟机,此时主线程不会阻塞工作线程中插件的运行。
Go SDK API
环境上下文(Contexts)
Go SDK 中接口的集合,一共有四种类型的上下文: VMContext , PluginContext , TcpContext and HttpContext 。其关系表如下:
Wasm Virtual Machine
(.vm_config.code)
┌────────────────────────────────────────────────────────────────┐
│ Your program (.vm_config.code) TcpContext │
│ │ ╱ (Tcp stream) │
│ │ 1: 1 ╱ │
│ │ 1: N ╱ 1: N │
│ VMContext ────────── PluginContext │
│ (Plugin) ╲ 1: N │
│ ╲ │
│ ╲ HttpContext │
│ (Http stream) │
└────────────────────────────────────────────────────────────────┘
-
VMContext : 对应于配置中的.vm_config.code , 每个虚拟机中仅有一个VMContext ,作为PluginContexts的父级可以创建任意多个PluginContexts -
PluginContext : 对用于配置中的.configuration ,负责具体插件的实例化,作为TcpContex与HttpContext的父级可以创建多个TcpContex与HttpContext -
TcpContext : 处理Tcp数据流 -
HttpContext : 处理Http数据流
虚拟机上下文VMContext 源码定义如下:
// VMContext 相当于Wasm虚拟机的配置,是扩展网络代理的入口点。其生命周期与Wasm虚拟机相同
type VMContext interface {
// 当Wasm虚拟机创建时, OnVMStart 被调用,期间API GetVMConfiguration 可用来检索配置中的 vm_config.configuration 属性
// 这个函数主要用于Wasm虚拟机级别的初始化
OnVMStart(vmConfigurationSize int) OnVMStartStatus
?
// 根据插件配置来创建 PluginContext
NewPluginContext(contextID uint32) PluginContext
}
插件上下文PluginContext 源码定义如下:
// PluginContext 相当于每个不同的插件配置(config.configuration)
// 每个配置通常在一个监听器的 http/tcp 过滤器中创建,因此 PluginContext 相当于创建网络过滤器实例
type PluginContext interface {
// 在 OnVmStart调用发生之后, OnPluginStart 将被调用,期间API GetPluginConfiguration 可用来检索配置中的 config.configuration 属性
OnPluginStart(pluginConfigurationSize int) OnPluginStartStatus
?
// 插件在主机中结束运行时, onPluginDone 被调用
// 返回 false 代表着其出于 pending 状态,还有一些遗留工作需要完成
// 这种情况下,必须调用方法 PluginDone() 来告诉主机工作已完成可以清除上下文
OnPluginDone() bool
?
// 当插件调用API RegisterQueue 后,其他插件将数据入队列, 本插件的 OnQueueReady 被调用
OnQueueReady(queueID uint32)
?
// 当通过API SetTickPeriodMilliSeconds 设置了定时周期并且时间已到时, 本插件的 OnTick 被调用
// 本方法可以用于流处理期间并行的处理其他任务
OnTick()
?
// 开发者必须实现下面两者中的出于实际流数据的扩展入口点
//
// NewTcpContext 用来创建 TcpContext, 返回 nil 代表本插件不适用于 TcpContext
NewTcpContext(contextID uint32) TcpContext
// NewHttpContext 用来创建 HttpContext, 返回 nil 代表本插件不适用于 HttpContext.
NewHttpContext(contextID uint32) HttpContext
}
HttpContext 和 TcpContext 不再具体展开,可通过context.go查看详情。
主机调用API
主机调用API是Proxy-Wasm 提供一系列方法用于与网络插件交互,例如在HttpContext 中可以调用GetHttpRequestHeaders API来获取Http请求头数据, LogInfo API可以用来在日志中添加打印信息。
所有可用的API可通过hostcall.go查看详情。
入口点
当Envoy创建Wasm虚拟机时,在他创建VMContext 前它将调用程序中main 函数,因此必须在main 函数中实现自定义的VMContext 。
Proxywasm 包中的SetVMContext 正是创建VMContext 的入口点,借助于Proxywasm 包提供的DefaultVMContext ,main 函数一般如下:
func main() {
proxywasm.SetVMContext(&vmContext{})
}
?
type vmContext struct {
// 嵌入默认提供的虚拟机上下文,这样就不必实现VMContext接口中的所有方法
types.DefaultVMContext
}
?
// 覆盖DefaultVMContext中的NewPluginContext方法
func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
return &pluginContext{}
}
?
type pluginContext struct {
// 嵌入默认提供的插件上下文
types.DefaultPluginContext
}
// 覆盖DefaultPluginContext中的NewTcpContext方法
func (ctx *pluginContext) NewTcpContext(contextID uint32) types.TcpContext {
return &networkContext{}
}
?
type networkContext struct {
// 嵌入默认提供的Tcp上下文
types.DefaultTcpContext
}
?
// 覆盖DefaultTcpContext的方法OnNewConnection
func (ctx *networkContext) OnNewConnection() types.Action {
... ...
跨虚拟机通信
上文中提到,在每个Envoy工作线程上内置的WASM运行时将创建一个Wasm虚拟机,某些特定场景下我们可能需要在当前虚拟机中与其他虚拟机通信,例如集成状态信息、缓存数据等。
当前提供了两种方案来实现跨虚拟机通信。
共享数据
共享数据(Shared Data) 是一种基于键值对存储以跨虚拟机或跨线程共享数据的方案。
共享数据 适用于场景如:
一份共享存储区通过vm_config.vm_id 配置来创建,这意味着同一份wasm二进制文件(vm_config.code 指定)不是必要条件。
?
如上图所示,两个虚拟机虽然使用hello.wasm和bye.wasm两个二进制文件,由于其使用同一个vm_id (foo),但是他们却共享同一份数据存储区。
共享数据 可用的API如下:
// GetSharedData 用于检索指定的键
// 返回的 "cas" 是方法 SetSharedData 中设置的用于保证线程安全更新的值
func GetSharedData(key string) (value []byte, cas uint32, err error)
?
// SetSharedData 用于在共享存储区设置键值对
// 若 CAS 值未匹配上当前值,则返回 ErrorStatusCasMismatch, 这意味着有其他Wasm虚拟机已经在这个键上设置了一个值,因此当前 CAS值递增更新,因此在变成逻辑中添加重试逻辑是非常有必要的
// 设置 cas 为0时代表不进行CAS值比对,永远返回成功
func SetSharedData(key string, data []byte, cas uint32) error
API相对简单,其使用了Compare-And-Swap方案来确保线程安全。
共享队列
共享队列(Shared Queue) 是一种先进先出(FIFO, First-In-First-Out)的队列。
共享队列 适用于场景如:
一个共享队列 通过配置中的vm_config.vm_id 和一个队列名称(vm_id, name)来创建,通过这两个产生可产生一个队列ID(queue_id)用于出/入队列。
共享队列 可用的API如下:
// ResolveSharedQueue 通过 vm_id 与 queue name 来产生ququeID, 用于 Enqueue/DequeueSharedQueue方法
func ResolveSharedQueue(vmID, queueName string) (ququeID uint32, err error)
?
// 通过 queueID 入队列
func EnqueueSharedQueue(queueID uint32, data []byte) error
?
// 通过 queueID 出队列
func DequeueSharedQueue(queueID uint32) ([]byte, error)
?
// RegisterSharedQueue 用于在插件上下文注册一个共享队列
// 注册意味着当有数据入队列时,当前插件上下文的 OnQueueReady 方法被调用
func RegisterSharedQueue(name string) (ququeID uint32, err error)
通常情况下, RegisterSharedQueue 与DequeueSharedQueue 被"消费者"调用,ResolveSharedQueue 与EnqueueSharedQueue 为"生产者"使用:
因此两个方法都返回了队列ID。
在环境上下文一节中提到,PluginContext 中包含一个API OnQueueReady ,这正是当有数据入队列时用于通知"消费者"的机制,当其他插件将数据入队列, 本插件的 OnQueueReady 被调用。
建议在单例的Wasm Service中(如Envoy的主线程)创建共享队列,否则当 OnQueueReady 发生调用时,将阻塞当前工作线程Tcp/Http流处理。
?
如上图所示,主线程Wasm虚拟机(vm_id="foo", my-singleton.wasm)通过 RegisterQueue 创建、注册了两个共享队列 (分别命名为"http"与"tcp")。两个共享队列 的"生产者"分别在 各自工作线程的Wasm虚拟机中实例化了HttpContext 与TcpContext 用于处理Http与Tcp数据流。当他们往各自队列入队数据时,主线程中的PluginContext 自动调用OnQueueReady 方法用于获取队列数据。
样例测试
由于Wasm过滤器支持的功能较为丰富,本节仅进行简单的数据流打印测试,验证其在Tcp数据流中的作用。
部署样例服务
部署以下几个服务并且注入边车,验证服务正常运行:
[root@linux ~]# kubectl -nwasm get po
NAME READY STATUS RESTARTS AGE
goserver-7c5cc7cf6-lslcz 2/2 Running 2 4d2h
sleep-558cdddbdb-g4wwd 2/2 Running 2 3d6h
[root@linux ~]# kubectl -nwasm get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
goserver NodePort 10.107.169.228 <none> 9091:16814/TCP
sleep ClusterIP 10.96.185.32 <none> 80/TCP
[root@linux ~]# kubectl -nwasm exec -it goserver-7c5cc7cf6-lslcz -c goserver -- bash
bash-4.4# curl 127.0.0.1:8081/healthz
{"status":"healthy","hostName":"goserver-7c5cc7cf6-lslcz"}
?
[root@linux ~]# kubectl -ndubbo get po -owide
NAME READY STATUS IP
dubbo-sample-consumer-6958b44b75-lf7sr 2/2 Running 10.244.104.60
dubbo-sample-provider-v1-cfdcf7768-ptbld 2/2 Running 10.244.122.140
[root@linux ~]# kubectl -ndubbo get se dubbo-samples-demoservice -o yaml
apiVersion: networking.istio.io/v1beta1
kind: ServiceEntry
...
spec:
addresses:
- 240.240.0.5
endpoints:
- address: 10.244.122.140
ports:
tcp-dubbo: 20880
[root@linux ~]# kubectl -ndubbo logs -f deploy/dubbo-sample-consumer
...
Hello Aeraki, response from dubbo-sample-provider-v1-cfdcf7768-ptbld/10.244.122.140
构建Wasm过滤器
根据web-assembly-hub教程安装wasm 客户端工具,并且初始化工程
[root@linux ~]# wasm init --language tinygo --platform istio --platform-version 1.9.x tcp-stream-data
INFO[0000] extracting 1416 bytes to /path/tcp-stream-data
[root@linux ~]# tree .
.
├── go.mod
├── go.sum
├── main.go
└── runtime-config.json
?
0 directories, 4 files
修改go.mod ,使用最新版SDK:
go 1.16
?
require github.com/tetratelabs/proxy-wasm-go-sdk v0.14.0
修改其默认生成的main.go ,使得其符合新版SDK的用法(默认应用v0.1.0版本),添加自定义打印函数:
...
// 打印tcp连接属性
func (ctx *networkContext) PrintConnectionAttrs() error {
addr, err := proxywasm.GetProperty([]string{"source", "address"})
......
proxywasm.LogInfof("source address: %s", string(addr))
?
dest, err := proxywasm.GetProperty([]string{"destination", "address"})
......
proxywasm.LogInfof("destination address: %s", string(dest))
?
return nil
}
?
// 打印envoy上游属性
func (ctx *networkContext) PrintUpstreamAttrs() error {
addr, err := proxywasm.GetProperty([]string{"upstream", "address"})
......
proxywasm.LogInfof("upstream address: %s", string(addr))
return nil
}
分别在OnDownstreamData (客户端请求数据)、OnUpstreamData (服务端响应数据)中引用上述自定义函数进行打印:
func (ctx *networkContext) OnDownstreamData(dataSize int, endOfStream bool)types.Action{
......
_ = ctx.PrintConnectionAttrs()
_ = ctx.PrintUpstreamAttrs()
?
data, err := proxywasm.GetDownstreamData(0, dataSize)
......
proxywasm.LogInfof(">>>>>> downstream data received >>>>>>\n%s", string(data))
return types.ActionContinue
}
?
func (ctx *networkContext) OnUpstreamData(dataSize int, endOfStream bool) types.Action {
......
_ = ctx.PrintConnectionAttrs()
_ = ctx.PrintUpstreamAttrs()
?
data, err := proxywasm.GetUpstreamData(0, dataSize)
......
?
proxywasm.LogInfof("<<<<<< upstream data received <<<<<<\n%s", string(data))
return types.ActionContinue
}
其中,Envoy支持引用的环境上下文属性参见其官网。
将代码在linux环境下编译生成二进制文件:
# 根据需要配置网络代理
[root@linux ~]# export http_proxy=proxyIP:proxyPort
[root@linux ~]# export GOPROXY=https://goproxy.cn,https://goproxy.io,direct
# 编译 (在容器镜像quay.io/solo-io/ee-builder:0.0.33内完成编译,映射编译结果到本地磁盘)
[root@linux ~]# wasm build tinygo . -t tcp-steam-data:test --store ./build/
Building with tinygo...go: downloading github.com/tetratelabs/proxy-wasm-go-sdk v0.14.0
INFO[0007] adding image to cache... filter file=/tmp/wasme551366072/filter.wasm tag="tcp-steam-data:test"
INFO[0007] tagged image digest="sha256:fc1563eb463aeb31119104a923509d4e885063ad0bd64fcfd2f6dd4da79c2196" image="docker.io/library/tcp-steam-data:test"
[root@linux ~]# ls -l build/79ada3a6417713a07a6c89d400f62306/
-rw-r--r--. 1 root root 225 Aug 9 16:57 descriptor.json
-rw-r--r--. 1 root root 255K Aug 9 16:57 filter.wasm
-rw-r--r--. 1 root root 37 Aug 9 16:57 image_ref
-rw-r--r--. 1 root root 126 Aug 9 16:57 runtime-config.json
应用Wasm过滤器
为简单起见,此处以hostPath 方式挂载存储,使得sleep服务边车容器能访问得到本地生成的Wasm二进制文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: sleep
......
template:
metadata:
annotations:
sidecar.istio.io/userVolume: '[{"name":"host","host": {"path":"/host/path"}}]'
sidecar.istio.io/userVolumeMount: '[{"mountPath":"/mount/path","name":"host"}]'
修改客户端边车容器日志级别为Info ,默认为Warn ,而上文代码中使用了proxywasm.LogInfof 输入Info日志:
[root@linux ~]# kubectl -nwasm exec deploy/sleep -- curl -X POST http://localhost:15000/logging?level=info
active loggers:
admin: info
......
wasm: info
[root@linux ~]# kubectl -ndubbo exec deploy/dubbo-sample-consumer -- curl -X POST http://localhost:15000/logging?level=info
active loggers:
admin: info
......
wasm: info
由于本文将Wasm模块应用到SIDECAR_OUTBOUND 环境,因此需要将客户端边车日志级别做调整。
编写EnvoyFilter 资源,使得Wasm过滤器插入到最后一条过滤器envoy.filters.network.tcp_proxy 之前:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: goserver-wasm
namespace: wasm
spec:
configPatches:
- applyTo: NETWORK_FILTER
match:
context: SIDECAR_OUTBOUND
listener:
name: 10.107.169.228_9091 # (goserver) svcIP_svcPort
filterChain:
filter:
name: envoy.filters.network.tcp_proxy # tcp流量
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.network.wasm
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
config:
name: tcp-stream-data
configuration:
'@type': type.googleapis.com/google.protobuf.StringValue
value: "empty string"
vm_config:
vm_id: "same_vm_id"
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/mount/path/filter.wasm"
Dubbo协议的EnvoyFilter 配置除了listener.name 需要更换为ServiceEntry.spec.addresses ,其他几乎一致,此处不在展开。
http流量
进入sleep服务,发送一个http请求:
[root@linux ~]# kubectl -nwasm exec -it deploy/sleep -c sleep -- sh
/ # curl goserver:9091/healthz
{"status":"healthy","hostName":"goserver-7c5cc7cf6-lslcz"}
注意,此时需要使用svcName:svcPort 的方式进行调用,因为根据Envoy规则将根据请求的Host 消息头进行主机域名匹配。
此时观察sleep服务边车容器日志:
?
根据上述结果可知,在OnDownstreamData (客户端请求数据)、OnUpstreamData (服务端响应数据)中打印的各方地址及端口数据是一致的,由于Http流量也属于Tcp流量,因此Http请求中的消息头与正文均被完整的打印了出来。
dubbo(tcp)流量
由于部署的客户端是定时请求服务端数据,因此不需要手动触发请求发送。
?
在dubbo协议流量中请求与响应数据也被正常打印,由于dubbo自定义的协议头等为非纯文本字符格式,因此数据中出现部分乱码。
但是正文部分仍然能看到请求中的接口、方法、消息类型等数据。
小结
上文对Wasm过滤器在Istio服务网格中的应用,从其架构原理、标准使用规范到样例测试,完整的展现了其在Envoy代理中的可插拔、可扩展的特性。
不过在实践过程中仍然会发现其在Istio运用中的问题:
相信之后官方会支持愈来愈多的语言编写Envoy的Wasm扩展。我们能够轻松选择本身熟悉的语言实现诸如度量,可观察性,转换,数据丢失预防,合规性验证或其余功能。
|