原文:https://github.com/proxy-wasm/spec/blob/master/docs/WebAssembly-in-Envoy.md

背景

截至 2019 年初,Envoy 是一个静态编译的二进制文件,其所有扩展都编译在构建时。这意味着提供自定义扩展的项目(例如 Istio)必须维护和分发自己的二进制文件,而不是使用官方和未修改的 Envoy 二进制文件。

对于无法控制其部署的项目,这甚至更成问题,因为任何更新和/或对扩展的错误修复需要构建新的二进制文件,生成版本,分发它,以及更重要的是,在生产中重新部署它。

这也意味着在部署的扩展和配置它们的控制平面之间经常存在版本差异。

解决方案

虽然部分问题可以使用动态可加载的 C++ 扩展来解决,但这在目前还不是一个可行的解决方案,因为,由于 Envoy 开发的速度很快,没有针对于扩展的稳定 ABI,甚至是 API,而且更新 Envoy 往往需要代码更改,这使得更新成为一个手动过程。

相反,我们决定通过使用带有稳定 ABI 的 WebAssembly 编写和交付 Envoy 扩展来解决这个问题,因为它带来了许多额外的好处(如下所述)。

什么是 WebAssembly?

WebAssembly (Wasm)是一种新兴的可执行代码的可移植二进制格式。代码在内存安全(针对主机)的沙箱中以接近本机的速度执行,具有明确定义的资源约束,以及用于与嵌入的主机环境(如代理)通信的 API。

优点

  • 敏捷。 扩展可以在运行时直接从控制平面传递和重新加载。这意味着不仅每个人都可以使用正式版本和未经修改的代理版本来加载自定义扩展,而且任何错误修复和/或更新都可以在运行时推送和/或测试,而无需更新和/或重新部署新的二进制文件。

  • 可靠性和隔离性。由于扩展部署在有资源约束的沙箱中,因此它们可能会崩溃和/或泄漏内存,而不会导致整个代理关闭。此外,可以限制 CPU 和内存的使用。

  • 安全。由于扩展被部署在一个沙箱中,沙箱中有明确定义的用于与代理进行通信的 API,因此它们具有访问权限,并且只能修改有限数量的连接和/或请求属性。此外,由于代理协调这种交互,它可以隐藏或清理扩展中的敏感信息(例如 “Authorization” 和 “Cookie” HTTP 头,或客户端的 IP 地址)。

  • 多样性。超过30种编程语言可以编译成 WebAssembly 模块,允许所有背景的开发人员(c,Go,Rust,Java,TypeScript 等)用自己选择的语言编写 Proxy-Wasm 扩展。

  • 可维护性。 由于扩展是使用标准库编写的,独立于代理的代码库,我们可以提供一个稳定的 ABI。

  • 便携性。 由于主机环境和扩展之间的接口是与代理无关的,因此使用 Proxy-Wasm 编写的扩展可以在各种代理中执行,例如 Envoy、NGINX、ATS,甚至可以在 gRPC 库中执行(假设它们都实现了标准)。

缺点

  • 由于需要启动许多虚拟机,每个虚拟机都有自己的内存块,因此内存使用率较高

  • 由于需要在沙箱内外复制大量数据,扩展转码负载的性能较低。

  • 与 cpu 相关的扩展的性能较低。与本机代码相比,预计放慢速度小于2倍。

  • 增加了二进制文件的大小,因为需要包括 Wasm 运行时。 WAVM 大约20mb,v8大约10mb。

  • WebAssembly 的生态系统还很年轻,目前的发展主要集中在浏览器内的使用上,JavaScript 被认为是主机环境。

高级概述

使用Proxy-Wasm,开发人员可以使用他们选择的编程语言编写代理扩展, 理想情况下,使用我们提供的特定于语言的库。然后将这些扩展编译为 便携式 Wasm 模块,并以该格式分发。

在代理端,一旦加载了 Wasm 模块(直接从磁盘加载或从 xDS 上的控制平面),它经过验证是否符合定义的 Proxy-Wasm 接口,并且 使用嵌入式 Wasm 运行时实例化,该运行时在每个运行时中创建一个新的 Wasm 虚拟机 工作线程。

对于 Envoy 的每个扩展类型,我们创建了一个填充程序来转换扩展的接口 到 Proxy-Wasm 调用,因此这些接口与本机(C++)Envoy 中使用的接口非常相似 扩展,拥抱事件驱动的编程模型。

Wasm-in-Envoy

运行时

为了执行提供的 Proxy-Wasm 扩展,proxy 需要嵌入一个 Wasm 运行时,这将 在沙盒中执行代码。目前,有两个 C 或 C++ Wasm 运行时:基于 LLVM 的 WAVM 和 V8.目前,WAVM和V8都嵌入在Envoy中,我们可以选择一个或另一个。 在配置中,但我们最有可能使用只有一个运行时的上游解决方案。

虚拟机

当 Wasm 运行时实例化 Wasm 模块时,它会创建一个 Wasm 虚拟机(VM 实例) 为了它。

有几种模型可用于在 VM 实例和 Proxy-Wasm 扩展之间进行映射。最终,它是 在以下方面进行权衡:启动延迟和资源使用,以及隔离和安全性。

*每个 Wasm 模块的每个工作线程的持久进程内 VM(在多个已配置的模块之间共享) 使用 Wasm 扩展)。 单个 Wasm 模块可以包含多个扩展(例如,侦听器筛选器和传输套接字、 都在一个包中)。对于每个 Wasm 模块,单个持久性进程内 VM 实例为 创建,并且可以(但不必)由所有引用该扩展的 Proxy-Wasm 扩展共享 配置中的 Wasm 模块。

  • 每个 Wasm 扩展的每个工作线程的持久性进程内 VM。 将为每个 Wasm 扩展创建一个持久的进程内 VM 实例,并由 共享 在配置中引用给定 Wasm 模块的所有 Proxy-Wasm 扩展,类似于 本机(C++)扩展今天被实例化。

  • 每个工作线程的永久进程内 VM,每个配置的 Wasm 扩展使用量。 为代理 Wasm 的每次配置使用创建一个持久的进程内 VM 实例 在配置中引用给定的 Wasm 模块的扩展。此模型提供更强大的功能 隔离保证比以前的模型,在多租户中应该是首选 环境。

  • 临时(按请求)进程内 VM。 将为每个请求、每个 Proxy-Wasm 扩展创建一个新的临时进程内 VM 实例, 并在请求完成后立即销毁。预计这将是令人望而却步的 贵。

  • 进程外虚拟机**。 这超出了本文档的范围,但对于加载不受信任的部署(并且可能 恶意)多租户环境中的 Wasm 模块,需要强大的安全保证和 想要防止类似幽灵的攻击,代理应该与进程外的Wasm进行通信 实现 Proxy-Wasm 的沙盒(例如,使用 Filters-over-gRPC 或共享内存),这将 代表其执行 Wasm 模块并将结果流回代理。

主机环境

沙盒化的 Wasm 虚拟机使用清晰的接口与嵌入主机环境(即代理)进行通信 定义的接口,包括:从 Wasm 模块导出的函数,代理可以调用这些函数, Wasm VM 可以调用的帮助器函数,以及用于内存管理的 Wasm 函数。

由于此接口级别非常低且相当稳定,因此它允许我们定义稳定的 ABI (将在单独的文档中定义的函数原型),扩展可以使用。

支持的语言和 API

Proxy-Wasm扩展可以用任何针对WebAssembly的语言编写,但是因为那些 扩展需要遵循上述接口,我们将为 很少选择的语言(C / C++使用Emscripten,Rust,Go和TypeScript),以加快 开发这些扩展。

虽然理论上任何Wasm模块都符合ABI,但无论它是用哪种语言编写的 在,应该开箱即用,Wasm生态系统仍然非常年轻,缺乏标准, 特别是对于非 Web 环境,某些语言具有假设和/或要求 关于它们在执行它们的主机环境,例如Go编译的Wasm模块使用syscall/js, 并期望主机环境是 JavaScript。

控制平面 (xDS) 集成

Proxy-Wasm扩展可以通过使用Envoy的在配置中引用它们来加载 Config::D ataSource,它可以指向磁盘上的文件,也可以包含已发送的内联 Wasm 模块 从控制平面 (xDS)。我们正在扩展此接口,以支持从 HTTP服务器也是如此。由于加载的Wasm模块将被执行,因此更强的检查,例如 强烈建议使用 SHA256 校验和或扩展的数字签名。

故障检测和通知

如果 Wasm VM 崩溃(例如,由于 Wasm 扩展中的错误),代理应创建一个新的 VM 的实例,记录有关崩溃的信息,并将其暴露给外部系统(例如 使用统计信息),以便控制平面可以对此信息采取行动。

理想情况下,代理还应跟踪崩溃次数,并且在达到限制时应停止。 重新启动 Wasm VM(以防止进入崩溃循环),并开始拒绝连接和/或 将错误返回给客户端。

可配置的资源约束

每个已配置的 Proxy-Wasm 扩展都可以设置资源约束(每个 VM 可以的最大内存 分配,以及它在每次调用期间可以消耗的最大 CPU 时间),以限制资源 用法。

可配置的 API 限制

可以为每个已配置的 Proxy-Wasm 扩展限制可用 API 的列表,以便 仅计算扩展(例如压缩)将无法访问它们不需要的 API(例如 HTTP/gRPC sidecalls)。

此外,某些 API 可以清理输入和/或输出(例如,删除返回的标头) 值,或限制 HTTP/gRPC 端调用可以进行的主机列表)。

生态系统(“Proxy-Wasm extensions Store”)

一旦它起飞并在社区中采用,就要完成。