通过WebAssembly扩展Envoy
WebAssembly(简称Wasm)是一种供基于栈的虚拟机使用的二进制指令格式。它作为C/C++/Rust这样的高级语言的编译目标,部署在现代浏览器或者服务器端应用程序中运行。
Wasm的优势:
- 性能:基于通用硬件能力实现Native运行速度
- 安全:在内存安全的沙盒环境下执行
- 易用:容易调试、编写、测试
你可以用多种语言编写逻辑,并利用相应的工具链,将代码编译为Wasm字节码。本节以C/C++为例。
Emscripten是一个基于LLVM的将C/C++编译为asm.js或WebAssembly的工具链。执行下面的命令下载预编译的工具链并安装:
1 2 3 4 |
git clone https://github.com/emscripten-core/emsdk.git cd emsdk ./emsdk install latest ./emsdk activate latest |
安装工具链后,需要执行下面的命令,进入Emscripten编译环境:
1 |
source ./emsdk_env.sh --build=Release |
这里我们编写一个很简单的C应用:
1 2 3 4 |
#include <stdio.h> int main(int argc, char ** argv) { printf("Hello, world!\n"); } |
执行命令: emcc hello.c -o hello.html, 会编译出Wsam,以及用于测试的HTML、JS文件。
执行命令: emrun --no_browser --port 8080 .,可以开启Web服务器。访问http://localhost:8080/hello.html可以看到Wasm运行结果。
在浏览器端,未来可能支持通过 <script type='module'>直接加载Wasm模块,就像加载ES6模块一样。目前则必须通过JS来加载和编译,步骤如下:
- 获取.wasm字节码,存储到ArrayBuffer中
- 编译字节码为 WebAssembly.Module
- 实例化WebAssembly.Module
后面两步骤可以在下面的函数中完成
1 2 3 4 5 |
function instantiate(bytes, imports) { // 返回一个解析为WebAssembly.Module的Promise // 实例化,传入Module以及Module需要的imports return WebAssembly.compile(bytes).then(m => new WebAssembly.Instance(m, imports)); } |
类似于ES6模块,Wasm模块也可以导入、导出函数(或其它对象) :
1 2 3 4 5 6 |
;; simple.wasm (module (func $i (import "imports" "i") (param i32)) (func (export "e") i32.const 42 call $i)) |
上面是一段文本格式(text format)的Wasm,它将 imports.i导入为内部名称 $i,我们在实例化此Wasm模块时,需要传入适当的导入对象:
1 2 3 4 5 6 |
var importObject = { imports: { i: arg => console.log(arg) } }; fetch('simple.wasm').then(response => response.arrayBuffer()) .then(bytes => instantiate(bytes, importObject)) .then(instance => instance.exports.e()); // 调用Wasm导出的函数 |
利用Wasm,可以将Istio的扩展能力从控制平面下沉到Sidecar中。2019年Google团队付出努力,为Envoy添加了基于Wasm的动态扩展能力(proxy-wasm)。Google团队还和Solo.io合作构建了WebAssembly Hub,作为Wasm扩展的公共仓库。通过Wasm Hub可以很容易的下载Wasm扩展 、安装并以容器方式运行。
目前proxy-wasm处于Alpha状态,Istio 1.5使用此Alpha状态的Envoy。Wasm扩展在仓库https://github.com/envoyproxy/envoy-wasm/中开发,它:
- 使用V8作为Wasm运行时
- 设计Wasm for Proxies(Proxy-Wasm)这一ABI,用于将Wasm嵌入到Envoy代理中。Telemetry V2是此ABI的第一个应用。需要注意,此ABI被设计为Proxy无感知的,也就是说不针对Envoy,其它代理也可以使用
- 用于简化基于Wasm的Envoy扩展开发的SDK,目前已经支持C++ / Rust / AssemblyScript
和运行在浏览器中的Wasm类似,Envoy中的Wasm也在基于栈的虚拟机中运行,其内存和Envoy是隔离的。Envoy和Wasm过滤器的交互,全部基于proxy-wasm SDK提供的函数、回调进行。
通过Wasm来扩展Envoy,具有以下优势:
- 敏捷性:可以在运行时,通过Istio控制平面来分发、重新载入扩展,而不需要重新发版Envoy
- 稳定的Envoy版本:一旦envoy-wasm成熟并且合并到Envoy主线,Istio以及其它基于Envoy的框架都可以使用稳定Envoy版本,而不需要自行构建。Envoy社区本身也可以将一些in-tree的扩展改为基于Wasm实现
- 可靠/隔离性:Wasm扩展运行在沙盒中,具有资源约束。Wasm可以崩溃、泄漏内存,却不会导致整个Envoy进程挂掉。内存和CPU用量可以被限制
- 安全性:Wasm运行沙盒具有明确定义的、和Envoy通信的API,Wasm扩展因而只能修改连接/请求的受限数量的属性
- 灵活性:超过30种语言可以编译为Wasm
- 性能不如C++扩展,可以达到70%性能
- 由于需要启动一或多个Wasm虚拟机,具有较高的内存消耗
在Istio 1.5中,很多扩展被下沉到Envoy中,基于Wasm实现,这极大的提升了性能。
Istio控制平面及其Envoy配置API也在积极改进,以支持Wasm。
Istio社区也整合Mixer适配器的供应商进行协作,将Mixer适配器迁移到Wasm。
WebAssembly Hub是一个仓库 + 一套工具集,便于构建、共享、发现Envoy的Wasm扩展。
WasmHub很大程度上自动化了Envoy Wasm扩展的开发、部署。使用它提供的工具集你能够很方便的把任何支持语言的代码编译为Wasm扩展,上传到Hub,一键部署到Istio。
在本节内容中,我们体验以下基于Wasm的Envoy扩展的开发流程。这个流程包括以下步骤:
- 编写自定义过滤器
- 构建为Wasm模块,存储在OCI镜像中
- 推送到Wasm Hub
- 将镜像部署到运行中的Envoy
- 测试过滤器的效果
可以通过命令行工具wasme来创建、部署WASM过滤器。
1 2 3 4 |
curl -sL https://run.solo.io/wasme/install | sh export PATH=$HOME/.wasme/bin:$PATH # 也可以到 https://github.com/solo-io/wasme/releases 下载期望的版本 |
创建一个名为new-filter的新项目:
1 |
wasme init ./new-filter |
命令会提示你:
- 选择开发语言,目前仅仅支持CPP和AssemblyScript(Typescript的子集)
- 选择运行Envoy的平台,目前支持gloo:1.3.x, istio:1.5.x
产生的新项目的结构如下:
1 2 3 4 5 6 7 |
. ├── assembly │ ├── index.ts │ └── tsconfig.json ├── package-lock.json ├── package.json └── runtime-config.json |
过滤器编写在assembly/index.ts中,主要包括RootContext、Context两个类。
1 |
wasme build assemblyscript -t webassemblyhub.io/gmemcc/add-header:v0.1 . |
1 2 |
wasme list wasme list --search $YOUR_USERNAME |
1 |
wasme login -u $YOUR_USERNAME -p $YOUR_PASSWORD |
1 |
wasme deploy gloo webassemblyhub.io/$YOUR_USERNAME/add-header:v0.1 --id=add-header |
1 |
wasme undeploy istio --id add-header --namespace bookinfo |
你也可以不使用Wasm Hub提供的工具,直接基于proxy-wasm SDK进行过滤器开发。
SDK提供多种语言的绑定:
语言 | 绑定 |
C++ |
https://github.com/proxy-wasm/proxy-wasm-cpp-sdk
|
Rust | https://github.com/proxy-wasm/proxy-wasm-rust-sdk |
AssemblyScript | https://github.com/solo-io/proxy-runtime |
Go | https://github.com/mathetake/proxy-wasm-go 目前处于试验状态 |
这里我们演示一下如何基于C++来开发Wasm过滤器。C++ SDK中关键的类包括:
类 | 说明 |
RootContext |
当Wasm插件(包含了过滤器的Wasm二进制文件)并加载后,此根上下文被创建。根上下文具有和VM实例一样的生命周期 VM实例负责执行过滤器,并且:
onConfigure(size_t)仅被Envoy调用,传递VM、插件配置。如果插件包含一或多个需要被传入配置的过滤器,你需要覆盖此函数,并通过 WasmBufferType::VmConfiguration and WasmBufferType::PluginConfiguration调用助手函数 getBufferBytes()
|
Context |
Envoy处理的网络流量,会穿过监听器的某个过滤器链。对于每个新的Stream,Envoy会为它创建一个Context,此Context的生命周期截至Stream终止 Context类以onXXX的形式提供若干钩子(回调)函数,这些虚函数中一部分用于HTTP流量,另一部分用于TCP流量。到底哪些钩子被调用,取决于你的Wasm被插入的过滤器链的级别(L4/L7)。例如,如果被插入到TCP过滤器链链,则 FilterHeadersStatus onRequestHeaders(uint32_t) 不会被调用 在Stream的完整生命周期中,你实现的Context都会被Envoy调用,在Context的回调函数中,你可以修改/静默掉流量,操控流量的方法包括:
回调函数返回的Status提示Envoy,是否需要将流量转给过滤器链中下一个过滤器处理 |
RegisterContextFactory | 你需要声明此类型的静态变量,从而注册用于创建RootContext、Context的工厂 |
下面的过滤器使用了C++ SDK:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
#include "proxy_wasm_intrinsics.h" // 根上下文类实现 class ExampleRootContext: public RootContext { public: explicit ExampleRootContext(uint32_t id, StringView root_id): RootContext(id, root_id) {} bool onStart(size_t) override; }; // 上下文类实现 class ExampleContext: public Context { public: explicit ExampleContext(uint32_t id, RootContext* root) : Context(id, root) {} FilterHeadersStatus onResponseHeaders(uint32_t) override; FilterStatus onDownstreamData(size_t, bool) override; }; // 注册工厂类的静态变量 static RegisterContextFactory register_FilterContext(CONTEXT_FACTORY(ExampleContext), ROOT_FACTORY(ExampleRootContext), "my_root_id"); // 插件初始化完毕,可以处理流之后,调用此钩子 bool ExampleRootContext::onStart(size_t n) { LOG_DEBUG("ready to process streams"); return true; } // 当HTTP响应头被解码后,调用此钩子 FilterHeadersStatus ExampleContext::onResponseHeaders(uint32_t) { // 增加一个响应头 addResponseHeader("resp-header-demo", "added by our filter"); // 让下一个过滤器继续处理流 return FilterHeadersStatus::Continue; } // 当接收到下游TCP数据块后,调用此钩子 FilterStatus ExampleContext::onDownstreamData(size_t, bool) { auto res = setBuffer(WasmBufferType::NetworkDownstreamData, 0, 0, "prepend payload to downstream data"); if (res != WasmResult::Ok) { LOG_ERROR("Modifying downstream data failed: " + toString(res)); return FilterStatus::StopIteration; } return FilterStatus::Continue; } |
要构建上述过滤器,最键单方式是使用容器(不需要在本机上安装依赖):
- 参考这里的步骤,创建包含了C++ SDK的Docker镜像
- 为Wasm过滤器创建Makefile:
1234567.PHONY = all cleanPROXY_WASM_CPP_SDK=/sdkall: example-filter.wasminclude ${PROXY_WASM_CPP_SDK}/Makefile.base_lite - 构建过滤器: docker run -v $PWD:/work -w /work wasmsdk:v2 /build_wasm.sh
构建结果是一个 .wasm文件。
用在Istio中使用Wasm过滤器,你需要将.wasm文件挂载到Pod,然后使用EnvoyFilter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: frontpage-v1-examplefilter spec: configPatches: - applyTo: HTTP_FILTER match: context: SIDECAR_INBOUND proxy: proxyVersion: '^1\.5.*' listener: portNumber: 8080 filterChain: filter: name: envoy.http_connection_manager subFilter: name: envoy.router patch: operation: INSERT_BEFORE value: config: config: name: example-filter rootId: my_root_id # V8虚拟机配置 vmConfig: code: local: filename: /var/local/lib/wasm-filters/example-filter.wasm runtime: envoy.wasm.runtime.v8 vmId: example-filter allow_precompiled: true # Wasm HTTP过滤器,envoy.filters.network.wasm是TCP过滤器 name: envoy.filters.http.wasm workloadSelector: labels: app: frontpage version: v1 |
Leave a Reply