Envoy中Wasm Filter相关概念解释

本文涉及的产品
云原生网关 MSE Higress,422元/月
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
应用实时监控服务-用户体验监控,每月100OCU免费额度
简介: 本文旨在介绍Envoy中Wasm Filter相关概念,让用户对相关架构有更加深入的了解,可以快速开发出自己的Wasm插件。阿里云服务网格(Service Mesh,简称ASM)提供一个全托管式的服务网格平台,兼容社区Istio开源服务网格,用于简化服务的治理,包括服务调用之间的流量路由与拆分管理、服务间通信的认证安全以及网格可观测性能力,从而极大地减轻开发与运维的工作负担。ASM支持Wasm插件。

Wasm in Envoy

spec/docs/WebAssembly-in-Envoy.md at master · proxy-wasm/spec

wasm in envoy:为什么在envoy中使用Wasm扩展?

优点:

  1. Agility:敏捷,不用更新或者重启envoy就可以完成wasm二进制更新。
  2. 可靠性和隔离性:plugin运行在沙箱内,即使插件崩溃了,envoy本身不会受影响。
  3. 安全:proxy沙箱提供的API很明确,插件的行为完全可控。
  4. 多样性:支持的语言多
  5. 通用性:后续可能其他proxy会支持这个标准。

缺点:

  1. 如果使用多个VM的话,内存消耗会比较多。
  2. 对payload转码的性能会比较低,因为涉及到大量数据要和沙箱交互。
  3. CPU密集型的plugin性能比较低,和原生代码相比慢不到2倍。

image.png

对于VM和proxy-wasm扩展之间的映射模型,需要用户考虑启动延迟、资源利用率、隔离性以及安全多种因素之后合理选择。有以下这些模型可以考虑:

  • 每个工作线程的每个wasm模块都有一个VM(在使用这个wasm extension的多个配置之间共享)。一个wasm模块可以有多个extension(比如lister filter和trasport socket都在同一个package里)。对于每个wasm模块,一旦创建了一个固定的VM,这个VM就可以被所有在配置中引用了它的wasm扩展共享。
  • 每个工作线程的每个wasm扩展有一个VM。
  • 每个wasm配置对应一个VM。多租首选,就是资源有点浪费。
  • 每个请求一个VM,太浪费了。
  • 线程外虚拟机:不做介绍。

看一下envoy中wasm filter的定义

{
  "name": ...,   # 
  "root_id": ...,
  "vm_config": {...},
  "configuration": {...},
  "fail_open": ...,
  "capability_restriction_config": {...}
}

name:当有多个filter都用了同一个vm_id和root_id时,这个字段可以用来唯一确定当前filter

root_id:主要是同一个VM中的一系列filter的集合,这个root id用来区分这个集合。如果使用了这个字段的话,这些filter将会共享RootContext以及其他Context。如果是空,所有VM_id相同的且没有填root_id的filter共享一套上下文。

vm_config:用来查找或者启动VM

configuration:Wasm插件配置。

fail_open:如果VM出现了致命错误,所有和这个VM关联的plugin都会关闭,此时会根据这个配置,返回503,或或者继续这个请求。注意:在on_start或者on_configure在xDS配置更新期间返回false,xDS config就会被拒绝;在on_start或者on_configureenvoy启动期间返回false,envoy就起不来了。

capability_restriction_config:限制WASM程序可以做的系统调用。

{
  "vm_id": ...,
  "runtime": ...,
  "code": {...},
  "configuration": {...},
  "allow_precompiled": ...,
  "nack_on_code_cache_miss": ...,
  "environment_variables": {...}
}

vm_id:一个id,和代码的hash值一起用来决定这个plugin要在哪个VM上运行。所有vm_id相同的plugin都将使用相同的VM。可以是空。多个plugin共享VM,可以更方便的共享数据,但是也可能造成安全隐患。

runtime:选择的wasm runtime,不多解释。

code:wasm二进制程序。

configuration:在启动一个新的VM时(proxy_on_start)需要用到的配置。如果是结构体,就会被序列化成hson,如果是bytes或者string,就会原封不动的传进去。

allow_precompiled:不多解释。

nack_on_code_cache_miss:不多解释。

environment_variables:指定环境变量,wasmplugin可以通过一些系统变量获取到。

Istio中和Wasm有关的内容

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: auth-wasm-plugin
  namespace: default
spec:
  imagePullPolicy: Always
  match:
    - mode: SERVER
  selector:
    matchLabels:
      app: xxxxx
  url: xxxxxxxxx
{
   "@type": "type.googleapis.com/envoy.admin.v3.EcdsConfigDump",
   "ecds_filters": [
    {
     "version_info": "2024-02-29T08:09:15Z/52",
     "ecds_filter": {
      "@type": "type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig",
      "name": "default.auth-wasm-plugin",
      "typed_config": {
       "@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm",
       "config": {
        "name": "default.auth-wasm-plugin",
        "vm_config": {
         "runtime": "envoy.wasm.runtime.v8",
         "code": {
          "local": {
           "filename": "xxxxxxx.wasm"
          }
         }
        },
        "configuration": {}
       }
      }
     },
     "last_updated": "2024-02-29T08:46:51.496Z"
    }
   ]
  }
  

Proxy-Wasm Go SDK介绍

术语简单解释:

  • VM:用来运行wasm程序,wasm程序要被拷贝到VM中,然后运行。
  • plugin是你扩展proxy的基本配置单元。允许一个VM中有多个plugin。使用本SDK,你可以在envoy中配置三种plugin:HTTPFilter、NetworkFilter以及wasm service。有了这些之后,你就可以编写一个程序,同时作为NetworkFilter和HTTPFilter去运行了。
  • HTTPFilter用来处理HTTP层级的数据
  • NetworkFilter用来处理TCP层面的数据
  • wasm service通常是单独运行在虚拟机中的,它主要用来多一些和networkfilter以及httpfilter无关的事情,比如aggregating metrics。

image.png

envoy的配置

Http Filter

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

在这种情况下,envoy的每个工作线程都会创建VM,每个VM就会处理对应工作线程上的listener上的http流。

Network Filter

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

在这种情况下,envoy的每个工作线程都会创建VM,每个VM就会处理对应工作线程上的listener上的tcp流。

wasm的http filter和networkfilter配置字段完全相同,唯一的不同就是两者操作的数据。

Wasm Service

你也可以单独跑一个wasm程序,不用融入envoy的那套请求处理流程

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


在每个线程上的多个plugin之间共享VM

static_resources:
  listeners:
    - name: http-header-operation
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 18000
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                # ....
                http_filters:
                  - name: envoy.filters.http.wasm
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      config:
                        configuration:
                          "@type": type.googleapis.com/google.protobuf.StringValue
                          value: "http-header-operation"
                        vm_config:
                          vm_id: "my-vm-id"
                          runtime: "envoy.wasm.runtime.v8"
                          configuration:
                            "@type": type.googleapis.com/google.protobuf.StringValue
                            value: "my-vm-configuration"
                          code:
                            local:
                              filename: "all-in-one.wasm"
                  - name: envoy.filters.http.router
    - name: http-body-operation
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 18001
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                # ....
                http_filters:
                  - name: envoy.filters.http.wasm
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
                      config:
                        configuration:
                          "@type": type.googleapis.com/google.protobuf.StringValue
                          value: "http-body-operation"
                        vm_config:
                          vm_id: "my-vm-id"
                          runtime: "envoy.wasm.runtime.v8"
                          configuration:
                            "@type": type.googleapis.com/google.protobuf.StringValue
                            value: "my-vm-configuration"
                          code:
                            local:
                              filename: "all-in-one.wasm"
                  - name: envoy.filters.http.router
    - name: tcp-total-data-size-counter
      address:
        socket_address:
          address: 0.0.0.0
          port_value: 18002
      filter_chains:
        - filters:
            - name: envoy.filters.network.wasm
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
                config:
                  configuration:
                    "@type": type.googleapis.com/google.protobuf.StringValue
                    value: "tcp-total-data-size-counter"
                    vm_config:
                      vm_id: "my-vm-id"
                      runtime: "envoy.wasm.runtime.v8"
                      configuration:
                        "@type": type.googleapis.com/google.protobuf.StringValue
                        value: "my-vm-configuration"
                      code:
                        local:
                          filename: "all-in-one.wasm"
            - name: envoy.tcp_proxy
              typed_config: # ...

你可以看见,三个listener上的vm_config都一样,每个工作线程上的多个plugin会共享同一个wasm VM。这里又个小要求:如果要使用同一个VM,所有的这些vm_config.vm_idvm_config.runtimevm_config.configurationvm_config.code必须要相同。

应用上述配置之后,每个Wasm VM将会创建三个plugincontext,每个plugincontext对应上述的每个filter。

Proxy-Wasm Go SDK API

Contexts

Contexts是一系列接口的集合。用户需要实现这些接口,来实现特定的扩展。

有4类Context:VMContextPluginContextTcpContextHttpContext,他们的映射关系如下所示:

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)    │
└────────────────────────────────────────────────────────────────┘
  1. 每个.vm_config.code对应一个VMContextVMContext存在于每个虚拟机上。
  2. VMContext负责创建PluginContext,可以创建任意数量。
  3. PluginContext对应一个plugin instance。这意味着一个PluginContext对应一个HttpFilter或者一个NetworkFilter或者一个WasmService,它通过pluginconfig中的.configuration字段来配置。
  4. PluginContext负责创建TcpContext以及HttpContext,他可以创建任意数量的这些context,只要用户在http filter或者networkfilter中配置了
  5. TcpContext负责处理每一个TCP stream。
  6. HttpContext负责处理每个Http stream。

所以你要做的就实现VMContextPluginContext,如果你想插入到HttpFilter或者NetworkFilter中,只需要继续实现HttpContextTcpContext

type VMContext interface {
  // OnVMStart is called after the VM is created and main function is called.
  // During this call, GetVMConfiguration hostcall is available and can be used to
  // retrieve the configuration set at vm_config.configuration.
  // This is mainly used for doing Wasm VM-wise initialization.
  OnVMStart(vmConfigurationSize int) OnVMStartStatus
  // NewPluginContext is used for creating PluginContext for each plugin configurations.
  NewPluginContext(contextID uint32) PluginContext
}

VM启动时,你可以通过GetVMConfiguration来获取.vm_config.configuration。通过这种方式,您可以进行VM级别的初始化控制。

然后就是PluginContext,这里简化了一些方法:

type PluginContext interface {
  // OnPluginStart is called on all plugin contexts (after OnVmStart if this is the VM context).
  // During this call, GetPluginConfiguration is available and can be used to
  // retrieve the configuration set at config.configuration in envoy.yaml
  OnPluginStart(pluginConfigurationSize int) OnPluginStartStatus
  // The following functions are used for creating contexts on streams,
  // and developers *must* implement either of them corresponding to
  // extension points. For example, if you configure this plugin context is running
  // at Http filters, then NewHttpContext must be implemented. Same goes for
  // Tcp filters.
  //
  // NewTcpContext is used for creating TcpContext for each Tcp streams.
  NewTcpContext(contextID uint32) TcpContext
  // NewHttpContext is used for creating HttpContext for each Http streams.
  NewHttpContext(contextID uint32) HttpContext
}

VMContext类似,PluginContext也有OnPluginStart,当pulgin初始化之后会被调用。调用这个方法时,.configuration的值可以通过GetPluginConfiguration方法获取。通过这种方式,开发人员可以告知一个PluginContext改做什么动作,比如指定一个PluginContext应该扮演一个HttpFilter,并且将指定的header插入http header中。

其他请自行参考具体的接口定义。

Hostcall API

一些提供的系统调用。主要用来和Proxy交互。在proxywasm包的hostcall.go中定义。比如GetHttpRequestHeadersAPI可以通过HttpContext访问一个http请求的header。hostcall.go中有完整的API列表。

Entrypoint

当envoy创建了VM之后,它会在创建VMContext之前,在启动阶段先调用程序里的main函数。因此你比如在main函数中传递自己的VMContext实现。

proxywasm包的SetVMContext就是用来实现这个目的的,所以通常,main函数应该如下所示:

func main() {
  proxywasm.SetVMContext(&myVMContext{})
}
type myVMContext struct { .... }
var _ types.VMContext = &myVMContext{}
// Implementations follow...

Cross-VM communications

VM是线程级别的,又是我们可能想和另一个VM通信,比如聚合数据、缓存数据等。

这里有两个概念:Shared Date和Shared Queue,可以看这个this talk

Shared Data

如果你想搞一个跨多个工作线程的所有VM统一的全局请求技术,该怎么办呢?或者你想缓存一些数据,所有的VM都会用到,又该怎么做呢?此时就应该考虑SharedData,或者称为Shared KVS(key-value store)。

Shared DAta相当于一个跨所有VM的kv存储。一个共享数据的kvs由vm_config中指定的vm_id创建。这意味着要共享kvs的话,必须要有相同的wasm二进制代码vm_config.code。只要有相同的vm_id就行。

image.png

上图就可以看出来,及时wasm二进制不通,也可以共享数据。

下面的api可以用来操作shared data,在hostcall.go中:

// GetSharedData is used for retrieving the value for given "key".
// Returned "cas" is be used for SetSharedData on that key for
// thread-safe updates.
func GetSharedData(key string) (value []byte, cas uint32, err error)
// SetSharedData is used for setting key-value pairs in the shared data storage
// which is defined per "vm_config.vm_id" in the hosts.
//
// ErrorStatusCasMismatch will be returned when a given CAS value is mismatched
// with the current value. That indicates that other Wasm VMs has already succeeded
// to set a value on the same key and the current CAS for the key is incremented.
// Having retry logic in the face of this error is recommended.
//
// Setting cas = 0 will never return ErrorStatusCasMismatch and always succeed, but
// it is not thread-safe, i.e. maybe another VM has already set the value
// and the value you see is already different from the one stored by the time
// when you call this function.
func SetSharedData(key string, value []byte, cas uint32) error

这个API很简单,但是他却通过cas机制实现了进程安全性以及跨VM安全性。

Shared Queue

如果你想要在并发的请求处理中跨VM做一些aggregate metrics改怎么搞呢?或者你想要将一些跨VM的集合信息发送给一个远程server呢?此时可以用Shared Queue。

Shared Queue是根据vm_id和queue的名称来创建的一个FIFO队列。使用queue id和名称可以唯一的确定一个queue,可以用来做入队和出队操作。

这个队列里入队和出队操作都是线程安全并且是跨VM安全的。可以简单看下hostcall.go的说明:

// DequeueSharedQueue dequeues an data from the shared queue of the given queueID.
// In order to get queue id for a target queue, use "ResolveSharedQueue" first.
func DequeueSharedQueue(queueID uint32) ([]byte, error)
// RegisterSharedQueue registers the shared queue on this plugin context.
// "Register" means that OnQueueReady is called for this plugin context whenever a new item is enqueued on that queueID.
// Only available for types.PluginContext. The returned queueID can be used for Enqueue/DequeueSharedQueue.
// Note that "name" must be unique across all Wasm VMs which share a same "vm_id".
// That means you can use "vm_id" can be used for separating shared queue namespace.
//
// Only after RegisterSharedQueue is called, ResolveSharedQueue("this vm_id", "name") succeeds
// to retrive queueID by other VMs.
func RegisterSharedQueue(name string) (queueID uint32, err error)
// EnqueueSharedQueue enqueues an data to the shared queue of the given queueID.
// In order to get queue id for a target queue, use "ResolveSharedQueue" first.
func EnqueueSharedQueue(queueID uint32, data []byte) error
// ResolveSharedQueue acquires the queueID for the given vm_id and queue name.
// The returned queueID can be used for Enqueue/DequeueSharedQueue.
func ResolveSharedQueue(vmID, queueName string) (queueID uint32, err error)

消费者负责调用:RegisterSharedQueue和DequeueSharedQueue。创建一个queue,从queue里取数据。

生产者使用ResolveSharedQueue和EnqueueSharedQueue来获取一个queue,并且给queue中加数据。

  • RegisterSharedQueue用来根据name和vm_id来创建一个queue。如果你想要使用一个queue,就必须要先有一个vm创建它。比如可以在一个PluginContext中创建,此时就会将它视为消费者。
  • ResolveSharedQueue用来通过name和vm_id获取一个queue。然后就可以添加数据。

从消费者的视角来看,该怎么知道什么时候queue里面有数据了呢?PluginContext中有一个接口叫OnQueueReady(queueID uint32)。当前PluginContext注册的queue中如果有数据,这个方法就会被调用。

强烈推荐shared queue应该由一个单例的Wasm Service来创建(在envoy的主线程上)。否则,工作线程上调用OnQueueReady的话,可能会导致这些工作线程阻塞,进而影响http或者tcp流的处理。(目前istio暴露的接口,应该还不支持使用WasmService。

image.png

限制和考量

现在不支持goroutine,不支持recover。

强烈建议使用OnTick来完成异步编程,而不是goroutine。

注意

如何修改route

envoy的wasmfilter有一个tricky的地方,可能会让大家迷惑。

正常来说,要在httpfilter中更改路由决策,envoy的标准方法是:clearRouteCache()

proxywasm并没有提供这个接口。所以我们无法主动在wasm中清除路由缓存。

但是golang的proxywasm给了一个示例,改了header之后,路由目标变了。。。

原因是:envoy的wasmfilter实现的时候,在addHeaderMapValue、setHeaderMapPairs、removeHeaderMapValue、replaceHeaderMapValue这四个函数中默认调用了clearRouteCache(),所以修改header就可以实现在wasm中修改路由。

此时,如果想搞metadatamatch,在wasm中加一个metadata,然后实现用metadata修改路由目标是不可行的。除非再同时改一个不关键的header,这个时候就能顺便清除路由缓存了。

在多说一句,envoy中其实也提供了一个测试的filter,专门用来clear-route-cache。这个在test包下,不确定默认会不会编译进去。

目录
相关文章
|
6月前
|
缓存 JavaScript 数据安全/隐私保护
js开发:请解释什么是ES6的Proxy,以及它的用途。
`ES6`的`Proxy`对象用于创建一个代理,能拦截并自定义目标对象的访问和操作,应用于数据绑定、访问控制、函数调用的拦截与修改以及异步操作处理。
73 3
|
6月前
|
开发工具 C++
WebAssembly01-- 暴露接口 避免编译时优化
WebAssembly01-- 暴露接口 避免编译时优化
56 0
|
6月前
|
存储 应用服务中间件 nginx
Nginx模块开发:模块结构的源码阅读以及过滤器(Filter)模块的实现
Nginx模块开发:模块结构的源码阅读以及过滤器(Filter)模块的实现
192 0
|
6月前
|
消息中间件 Dubbo Java
Simple RPC - 01 框架原理及总体架构初探
Simple RPC - 01 框架原理及总体架构初探
83 0
|
SQL 分布式计算 Go
Go编程模式 - 6-映射、归约与过滤
但是,我不建议大家在实际项目中直接使用这一块代码,毕竟其中大量的反射操作是比较耗时的,尤其是在延迟非常敏感的web服务器中。 如果我们多花点时间、直接编写指定类型的代码,那么就能在编译期发现错误,运行时也可以跳过反射的耗时。
56 0
MoE 系列(七)| Envoy Go 扩展之沙箱安全
在本系列的第 5 篇《MoE 系列(五)|Envoy Go 扩展之内存安全》中我们介绍了内存安全如何实现。第 6 篇《MoE 系列(六)| Envoy Go 扩展之并发安全》又谈到了并发场景下的内存安全。今天,我们来到了安全性的最后一篇:沙箱安全,也是相对来说,最简单的一篇。
|
Rust 安全 Java
MoE 系列(六)|Envoy Go 扩展之并发安全
本系列前一篇介绍了 Envoy Go 扩展的内存安全,相对来说,还是比较好理解的,主要是 Envoy C++ 和 Go GC 都有自己一套的内存对象的生命周期管理。这篇聊的并发安全,则是专注在并发场景下的内存安全,相对来说会复杂一些。
|
缓存 安全 Java
MoE 系列(五)|Envoy Go 扩展之内存安全
前面几篇介绍了 Envoy Go 扩展的基本用法,接下来几篇将介绍实现机制和原理。
|
Cloud Native Go 数据安全/隐私保护
MoE 系列(二)|Golang 扩展从 Envoy 接收配置
上一篇我们用一个简单的示例,体验了用 Golang 扩展 Envoy 的极速上手。这次我们再通过一个示例,来体验 Golang 扩展的一个强大的特性:从 Envoy 接收配置。
|
负载均衡 程序员 Go
Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?
就是因为无法在同一个进程内,或者无法在同一个服务器上通过本地调用的方式实现我们的需求。 HTTP能满足需求但是不够高效,所以我们需要使用RPC。
176 0
Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?