WebAssembly(Wasm)

Wasm 是一种可执行代码的可移植二进制格式,依赖于一个开放的标准。它允许开发人员用自己喜欢的编程语言编写,然后将代码编译成 Wasm 模块

代码编译成 wasm
图 8.4.1:代码编译成 wasm

Wasm 模块与主机环境隔离,并在一个称为虚拟机(VM)的内存安全沙盒中执行。Wasm 模块使用一个 API 与主机环境进行通信。

Wasm 的主要目标是在网页上实现高性能应用。例如,假设我们要用 Javascript 构建一个网页应用程序。我们可以用 Go(或其他语言)写一些,并将其编译成一个二进制文件,即 Wasm 模块。然后,我们可以在与 Javascript 网页应用程序相同的沙盒中运行已编译的 Wasm 模块。

最初,Wasm 被设计为在网络浏览器中运行。然而,我们可以将虚拟机嵌入到其他主机应用程序中,并执行它们。这就是 Envoy 的作用!

Envoy 嵌入了 V8 虚拟机的一个子集。V8 是一个用 C++ 编写的高性能 JavaScript 和 WebAssembly 引擎,它被用于 Chrome 和 Node.js 等。

我们在本课程的前面提到,Envoy 使用多线程模式运行。这意味着有一个主线程,负责处理配置更新和执行全局任务。

除了主线程之外,还有负责代理单个 HTTP 请求和 TCP 连接的 worker 线程。这些 worker 线程被设计为相互独立。例如,处理一个 HTTP 请求的 worker 线程不会受到其他处理其他请求的 worker 线程的影响。

Envoy 线程
图 8.4.2:Envoy 线程

每个线程拥有自己的资源副本,包括 Wasm 虚拟机。这样做的原因是为了避免任何昂贵的跨线程同步,即实现更高的内存使用率。

Envoy 在运行时将每个独特的 Wasm 模块(所有 *.wasm 文件)加载到一个独特的 Wasm VM。由于 Wasm VM 不是线程安全的(即,多个线程必须同步访问一个 Wasm VM),Envoy 为每个将执行扩展的线程创建一个单独的 Wasm VM 副本。因此,每个线程可能同时有多个 Wasm VM 在使用。

Proxy-Wasm

我们将使用的 SDK 允许我们编写 Wasm 扩展,这些扩展是 HTTP 过滤器,网络过滤器,或称为 Wasm 服务的专用扩展类型。这些扩展在 Wasm 虚拟机内的 worker 线程(HTTP 过滤器,网络过滤器)或主线程(Wasm 服务)上执行。正如我们提到的,这些线程是独立的,它们本质上不知道其他线程上发生的请求处理。

HTTP 过滤器是处理 HTTP 协议的,它对 HTTP Header、body 等进行操作。同样,网络过滤器处理 TCP 协议,对数据帧和连接进行操作。我们也可以说,这两种插件类型是无状态的。

Envoy 还支持有状态的场景。例如,你可以编写一个扩展,将请求数据、日志或指标等统计信息在多个请求之间进行汇总——这意味着跨越了许多 worker 线程。对于这种情况,我们会使用 Wasm 服务类型。Wasm 服务类型运行在单个虚拟机上;这个虚拟机只有一个实例,它运行在 Envoy 主线程上。你可以用它来汇总无状态过滤器的指标或日志。

下图显示了 Wasm 服务扩展是如何在主线程上执行的,而不是 HTTP 或网络过滤器,后者是在 worker 线程上执行。

API
图 8.4.3:API

事实上,Wasm 服务扩展是在主线程上执行的,并不影响请求延迟。另一方面,网络或 HTTP 过滤器会影响延迟。

图中显示了在主线程上运行的 Wasm 服务扩展,它使用消息队列 API 订阅队列并接收由运行在 worker 线程上的 HTTP 过滤器或网络过滤器发送的消息。然后,Wasm 服务扩展可以聚合从 worker 线程上收到的数据。

Wasm 服务扩展并不是持久化数据的唯一方法。你也可以调用 HTTP 或 gRPC API。此外,我们可以使用定时器 API 在请求之外执行行动。

我们提到的 API、消息队列、定时器和共享数据都是由 Proxy-Wasm 提供的。

Proxy-Wasm 是一个代理无关的 ABI(应用二进制接口)标准,它规定了代理(我们的主机)和 Wasm 模块如何互动。这些互动是以函数和回调的形式实现的。

Proxy-Wasm 中的 API 与代理无关,这意味着它们可以与 Envoy 代理以及任何其他代理(例如 MOSN)一起实现 Proxy-Wasm 标准。这使得你的 Wasm 过滤器可以在不同的代理之间移植,而且它们并不局限于 Envoy。

Proxy-Wasm
图 8.4.4:Proxy-Wasm

当请求进入 Envoy 时,它们会经过不同的过滤器链,被过滤器处理,在链中的某个点,请求数据会流经本地 Proxy-Wasm 扩展。

这个扩展使用 Proxy-Wasm 接口与运行在虚拟机内的扩展通信。 过滤器处理完数据后,该链就会继续,或停止,这取决于从扩展返回的结果。

基于 Proxy-Wasm 规范,我们可以使用一些特定语言的 SDK 实现来编写扩展。

在其中一个实验中,我们将使用 Go SDK for Proxy-Wasm 来编写 Go 中的 Proxy-Wasm 插件。

TinyGo 是一个用于嵌入式系统和 WebAssembly 的编译器。它不支持使用所有的标准 Go 包。例如,不支持一些标准包,如 net 和其他。

你还可以选择使用 Assembly Script、C++、Rust 或 Zig。

配置 Wasm 扩展

Envoy 中的通用 Wasm 扩展配置看起来像这样。

- name: envoy.filters.http.wasm
  typed_config:
    "@type": type.googleapis.com/udpa.type.v1.TypedStruct
    type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
    value:
      config:
        vm_config:
          vm_id: "my_vm"
          runtime: "envoy.wasm.runtime.v8"
          configuration:
            "@type": type.googleapis.com/google.protobuf.StringValue
            value: '{"plugin-config": "some-value"}'
          code:
            local:
              filename: "my-plugin.wasm"
        configuration:
          "@type": type.googleapis.com/google.protobuf.StringValue
          value: '{"vm-wide-config": "some-value"}'

vm_config 字段用于指定 Wasm 虚拟机、运行时,以及我们要执行的.wasm 扩展的实际指针。

vm_id 字段在虚拟机之间进行通信时使用。然后这个 ID 可以用来通过共享数据 API 和队列在虚拟机之间共享数据。请注意,要在多个插件中重用虚拟机,你必须使用相同的 vm_id、运行时、配置和代码。

下一个项目是 runtime。这通常被设置为 envoy.wasm.runtime.v8。例如,如果我们用 Envoy 编译 Wasm 扩展,我们会在这里使用 null 运行时。其他选项是 Wasm micro runtime、Wasm VM 或 Wasmtime;不过,这些在官方 Envoy 构建中都没有启用。

vm_config 字段下的配置是用来配置虚拟机本身的。除了虚拟机 ID 和运行时外,另一个重要的部分是code字段。

code 字段是我们引用编译后的 Wasm 扩展的地方。这可以是一个指向本地文件的指针(例如,/etc/envoy/my-plugin.wasm)或一个远程位置(例如,https://wasm.example.com/my-plugin.wasm)。

configuration 文件,一个在 vm_config 下,另一个在 config 层,用于为虚拟机和插件提供配置。然后当虚拟机或插件启动时,可以从 Wasm 扩展代码中读取这些值。

要运行一个 Wasm 服务插件,我们必须在 bootstrap_extensions 字段中定义配置,并将 singleton 布尔字段的值设置为真。

bootstrap_extensions:
- name: envoy.bootstrap.wasm
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.wasm.3.WasmService
    singleton: true
    config:
      vm_config:{ ...}

开发 Wasm 扩展 - Proxy-Wasm Go SDK API

在开发 Wasm 扩展时,我们将学习上下文、hostcall API 和入口点。

上下文

上下文是 Proxy-Wasm SDK 中的一个接口集合,并与我们前面解释的概念相匹配。

上下文
图 8.4.5:上下文

例如,每个虚拟机中都有一个 VMContext,可以有一个或多个 PluginContexts。这意味着我们可以在同一个虚拟机上下文中运行不同的插件(即使用同一个 vm_id 时)。每个 PluginContext 对应于一个插件实例。那就是 TcpContext(TCP 网络过滤器)或 HttpContext(HTTP 过滤器)。

VMContext 接口定义了两个函数:OnVMStart 函数和 NewPluginContext 函数。

type VMContext interface {
  OnVMStart(vmConfigurationSize int) OnVMStartStatus
  NewPluginContext(contextID uint32) PluginContext
}

顾名思义,OnVMStart 在虚拟机创建后被调用。在这个函数中,我们可以使用 GetVMConfiguration hostcall 检索可选的虚拟机配置。这个函数的目的是执行任何虚拟机范围的初始化。

作为开发者,我们需要实现 NewPluginContext 函数,在该函数中我们创建一个 PluginContext 的实例。

PluginContext 接口定义了与 VMContext 类似的功能。下面是这个接口。

type PluginContext interface {
  OnPluginStart(pluginConfigurationSize int) OnPluginStartStatus
  OnPluginDone() bool

  OnQueueReady(queueID uint32)
  OnTick()

  NewTcpContext(contextID uint32) TcpContext
  NewHttpContext(contextID uint32) HttpContext
}

OnPluginStart 函数与我们前面提到的 OnVMStart 函数类似。它在插件被创建时被调用。在这个函数中,我们也可以使用 GetPluginConfiguration API 来检索插件的特定配置。我们还必须实现 NewTcpContextNewHttpContext,在代理中响应 HTTP/TCP 流时被调用。这个上下文还包含一些其他的函数,用于设置队列(OnQueueReady)或在流处理的同时做异步任务(OnTick)。

参考 Proxy Wasm Go SDK Github 仓库 中的 context.go 文件,以获得最新的接口定义。

Hostcall API

这里实现的 hostcall API ,为我们提供了与 Wasm 插件的 Envoy 代理互动的方法。

hostcall API 定义了读取配置的方法;设置共享队列并执行队列操作;调度 HTTP 调用,从请求和响应流中检索 Header、Trailer 和正文并操作这些值;配置指标;以及更多。

入口点

插件的入口点是 main 函数。Envoy 创建了虚拟机,在它试图创建 VMContext 之前,它调用了 main 函数。在典型的实现中,我们把 SetVMContext 方法称为 main 函数。

func main() {
  proxywasm.SetVMContext(&myVMContext{})
}

type myVMContext struct { ....}

var _ types.VMContext = &myVMContext{}.

results matching ""

    No results matching ""