使用 Rust 为 Envoy 构建插件

NaNFT.png

今天就带大家使用 Rust 构建 WebAssembly 作为 Envoy 的插件。

什么是 WebAssembly

WebAssembly(Wasm)是一种由多种语言编写的,可移植的字节码格式,它能以以接近本机的速度执行。其最初的设计目标与上述挑战很相符,并且在其背后得到了相当大的行业支持。Wasm 是在所有主流浏览器中可以本地运行的第四种标准语言(继 HTML,CSS 和 JavaScript 之后),于 2019 年 12 月成为 W3C 正式建议。这使我们有信心对其进行战略下注。

WebAssembly In Envoy

在早期的 Envoy 版本中,支持两种插件模式 C++动态库 / Lua脚本 的模式,不过这两种模式都对语言本身产生了需求,因此在后续提出了 wasm 的支持,一开始作为一个 side 项目独立演进的 envoy-wasm

使用 Wasm 扩展 Envoy 带来了几个主要好处:

  • 敏捷性:可以用 Istio 控制平面在运行时下发和重载扩展。这就可以快速的进行扩展开发→ 测试→ 发布周期,而无需重启 Envoy
  • 发布库:一旦完成合并到主树中之后,Istio 和其它程序将能够使用 Envoy 的发布库,而不是自己构建。这也方便 Envoy 社区迁移某些内置扩展到这个模型,从而减少他们的工作。
  • 可靠性和隔离性:扩展部署在具有资源限制的沙箱中,这意味着它们现在可以崩溃或泄漏内存,但不会让整个 Envoy 挂掉。CPU 和内存使用率也可以受到限制。
  • 安全性:沙盒具有一个明确定义的 API,用于和 Envoy 通信,因此扩展只能访问和修改链接或者请求中有限数量的属性。此外,由于 Envoy 协调整个交互,因此它可以隐藏或清除扩展中的敏感信息(例如,HTTP 头中的 “Authorization”和“Cookie”,或者客户端的 IP 地址)。
  • 灵活性:可以将超过 30 种编程语言编译为 WebAssembly,可以让各种技术背景的开发人员都可以用他们选择的语言来编写 Envoy 扩展,比如:C++,Go,Rust,Java,TypeScript 等。

proxy-wasm spec 有着 ABI 的定义,并且为很多语言提供了 SDK,那我们先来看看我们最适合的 Rust SDK

Rust WebAssembly In Action

proxy-wasm-rust-sdk 提供了操作 Proxy SDK

准备工作

因为需要 wasm 的支持,先安装我们的目标架构

1
rustup target add wasm32-unknown-unknown

我们首先创建一个我们的 Rust 项目

1
cargo new --lib my-frist-wasm-filter

因为我们最终生成的代码给 envoy 使用,因此还需要修改生成的 lib 类型,顺带加上我们的 lib 依赖

Cargo.toml
1
2
3
4
5
[lib]
crate-type = ["cdylib"]

[dependencies]
proxy-wasm = "0.1.2"

插件运行机制

Nar7Q.png

作为插件十之八九都是被 宿主 回调的,因此查阅 abi 有哪些函数,我们大致上就知道我们能做哪些工作了,文档在此

重要的函数

回调点
  • _start: 在模块的载入和初始化的时候会调用,一般用来设置一些状态量
  • proxy_on_context_create: 上下文创建的时候回调
  • proxy_on_done: 在context 处理完成
  • proxy_on_vm_start: 在启动 wasm 虚拟器的时候
  • proxy_on_new_connection: 建立新连接时
  • proxy_on_upstream_data: 收到数据时
  • proxy_on_http_request_headers: 收到 http headers 的时候
虚拟机环境实现的函数
  • proxy_log: 打印日志
  • proxy_done: 当前context 处理完成
  • proxy_get_shared_data: 获得 context 共享的对象

对象

NTJh8.png

整个 WASM 中最重要的就是 Context 对象,这个封装了对于系统的操作,实现 Proxy-Wasm 插件需要进行如下 2 个步骤。

  • 定义 Context
  • 使用定义的 Context 重载默认实现

Context 对象,在系统中多种定义,比如 RootContext HttpContext StreamContext 对应不同处理流程中的

比如 HttpContext

HttpContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pub trait HttpContext: Context {
fn on_http_request_headers(&mut self, _num_headers: usize) -> Action {
Action::Continue
}

fn on_http_request_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action {
Action::Continue
}

fn on_http_request_trailers(&mut self, _num_trailers: usize) -> Action {
Action::Continue
}

fn get_http_request_trailers(&self) -> Vec<(String, String)> {
hostcalls::get_map(MapType::HttpRequestTrailers).unwrap()
}

fn set_http_request_trailers(&self, trailers: Vec<(&str, &str)>) {
hostcalls::set_map(MapType::HttpRequestTrailers, trailers).unwrap()
}
fn on_log(&mut self) {}
}

Example

我们来实现我们第一个 Hello World 吧,就是在我们每一次接收到请求的时候,打印出一句 Hello World

lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use log::info;
use proxy_wasm::traits::Context;
use proxy_wasm::traits::HttpContext;
use proxy_wasm::{types::Action};

// 因为内存管理委托给 Envoy,这里加上 no_mangle 禁止内存管理
#[no_mangle]
pub fn _start() {
proxy_wasm::set_log_level(proxy_wasm::types::LogLevel::Trace);
proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> { Box::new(HelloContext {}) });
}

struct HelloContext {}

impl Context for HelloContext {}

impl HttpContext for HelloContext {
fn on_http_request_headers(&mut self, num_headers: usize) -> Action {
info!("Hello new Connection.");
return Action::Continue;
}
}

单元测试的写法和普通系统无异,这里就不做展开。

集成测试

首先我们其编译

1
cargo build --target wasm32-unknown-unknown --release

我们现在需要一个 Envoy 来进行我们的测试。

1
docker pull envoyproxy/envoy-dev:af418e1096a386000f936744a1a884b6ce87cee0

创建一个 bootstrap.yml 然后启动

envoy-bootstrap.yml
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
http_filters:
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: "my_plugin"
vm_config:
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/my_frist_wasm_filter.wasm"
allow_precompiled: true
- name: envoy.filters.http.router
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io

clusters:
- name: service_envoyproxy_io
connect_timeout: 30s
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: service_envoyproxy_io
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: www.envoyproxy.io
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: www.envoyproxy.io

admin:
access_log_path: /dev/null
address:
socket_address:
address: 0.0.0.0
port_value: 9901

然后就是最激动人心的运行时刻。

1
2
3
4
$ docker run --rm -it -v $(pwd)/envoy-custom.yaml:/envoy-custom.yaml -v $(pwd)/target/wasm32-unknown-unknown/release/my_frist_wasm_filter.wasm:/my_frist_wasm_filter.wasm -p 9901:9901 -p 10000:10000 envoyproxy/envoy-dev -c /envoy-custom.yaml

$ curl localhost:10000
[2020-10-30 09:38:40.116][26][info][wasm] [source/extensions/common/wasm/context.cc:1154] wasm log: Hello new Connection.

我们看到了我们打印的


[2020-10-30 09:38:40.116][26][info][wasm] [source/extensions/common/wasm/context.cc:1154] wasm log: Hello new Connection.

代表着我们的插件运行成功了。

周边生态

webassembly hub

(https://webassemblyhub.io/): 因为很多插件可以复用,因此istio 联合 solo.io 社区完成了 webassemblyhub 的建设
NaSyE.png

除此之外还提供了 wasme 这个 cli 工具可以帮助我们简单的开发系统。

参考