一、Wasm和Envoy Filter基础
在服务网格的世界里,Envoy 扮演着数据平面的核心角色——它像一扇智能大门,所有进出的流量都要经过它的检查。为了让这扇门具备更灵活的处理能力,Envoy 提供了“过滤器”(Filter)机制,你可以把它想象成门上的各种小机关:有的检查身份证(认证),有的记录通行记录(日志),有的修改行李内容(请求/响应改写)。这些机关通常是用 C++ 写的,但 C++ 学起来门槛高,部署起来也麻烦。于是,WebAssembly(Wasm)就派上了用场——它让你能用更简单的语言(比如 Rust、Go、AssemblyScript)写一个过滤器,然后编译成轻量级的 wasm 模块,热插拔到 Envoy 里,不需要重启 Envoy 进程。
1.1 什么是 Envoy HTTP Filter
在 Envoy 中,HTTP 过滤器就是一段处理 HTTP 请求或响应的代码。它可以在请求到达上游服务之前修改请求头、检查请求体,或者在响应返回给客户端之前改写响应内容。Envoy 官方支持多种内置过滤器,比如限流、熔断、路由等,但如果你有特殊需求,比如给某些请求打标、动态注入安全头,就得自己写自定义过滤器。传统的做法是用 C++ 写一个 Envoy 插件,编译进 Envoy 二进制文件中,但这样太笨重——改一点代码就得重新编译、重启整个代理。Wasm 过滤器则像手机里的 App,随时安装卸载,不影响系统运行。
1.2 为什么选择 Wasm
选择 Wasm 的理由很实在:生态系统成熟、安全隔离、跨语言支持。Rust 因为有零成本抽象和严格的类型系统,写 Wasm 特别靠谱;而且 Envoy 的 proxy-wasm 接口已经标准化,几乎不需要关心底层网络细节。你再也不用纠结 C++ 的内存泄漏,也不用害怕 Go 的 GC 抖动。另一个优势是 Wasm 模块可以被签名验证,防止恶意代码混入服务网格。
二、环境准备与工具链
写 Wasm 过滤器前,得先搭好厨房。我们选 Rust 作为主力语言,因为它编译出来的 wasm 模块小巧高效,而且社区生态最活跃。
2.1 安装 Rust 和 Wasm 目标
打开终端,先确保你有 Rust 工具链。如果没有,去官网(rustup.rs)下载安装,或者直接跑一行命令:
# 安装 Rust 工具链(如果还没装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 添加 wasm32-unknown-unknown 编译目标
rustup target add wasm32-unknown-unknown
# 安装 wasm-tools 用于检查和优化
cargo install wasm-tools
2.2 创建项目
用 Cargo 新建一个库项目,取个名字叫 custom-filter:
cargo new --lib custom-filter
cd custom-filter
然后编辑 Cargo.toml,添加依赖:proxy-wasm 是 Envoy Wasm SDK 的 Rust 绑定,log 用来打印日志方便调试。
[package]
name = "custom-filter"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
proxy-wasm = "0.2"
log = "0.4"
注意 crate-type = ["cdylib"] 是产生动态库,最后编译成 .wasm 文件。
三、编写第一个 Wasm Filter
现在开始写核心逻辑。我们的目标很简单:在每一个 HTTP 请求上追加一个自定义头 X-Custom-Flag: true,并在响应的末尾加上一个 X-Response-Modified: yes 头。这样你就可以在网格里跟踪哪些流量被这个过滤器处理过。
3.1 项目结构
Rust 代码写在 src/lib.rs 里。整个文件就一个模块,通过 proxy_wasm 提供的宏来注册根上下文(RootContext)和流上下文(StreamContext)。StreamContext 才是每次请求/响应的生命期。
3.2 实现 filter 逻辑
我们直接上完整的 Rust 代码,每一行都有注释:
// src/lib.rs
// 技术栈:Rust + proxy-wasm
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use log::info;
// 定义我们的根上下文,用于初始化阶段
struct MyRootContext;
// 实现 RootContext 特征
impl Context for MyRootContext {}
impl RootContext for MyRootContext {
// 创建流上下文时调用
fn create_stream_context(&self, _context_id: u32) -> Option<Box<dyn StreamContext>> {
Some(Box::new(MyStreamContext))
}
// 返回过滤器的类型:HTTP 方向
fn get_type(&self) -> Option<ContextType> {
Some(ContextType::HttpContext)
}
}
// 定义流上下文,处理每个请求/响应
struct MyStreamContext;
impl Context for MyStreamContext {}
impl StreamContext for MyStreamContext {}
// 实现 HttpContext 特征,处理 HTTP 相关事件
impl HttpContext for MyStreamContext {
// 当收到请求头时调用
fn on_http_request_headers(&self, _num_headers: usize, _end_of_stream: bool) -> Action {
// 在请求头上追加一个自定义 header
self.set_http_request_header("X-Custom-Flag", Some("true"));
info!("Added X-Custom-Flag: true to request");
// 继续处理请求链
Action::Continue
}
// 当收到响应头时调用
fn on_http_response_headers(&self, _num_headers: usize, _end_of_stream: bool) -> Action {
// 在响应头上追加一个标记
self.set_http_response_header("X-Response-Modified", Some("yes"));
info!("Added X-Response-Modified: yes to response");
Action::Continue
}
}
// 必须导出这个函数,告诉 proxy-wasm 如何创建根上下文
#[no_mangle]
pub fn _start() {
proxy_wasm::set_root_context(|_| Box::new(MyRootContext));
}
代码解释:
_start是 Wasm 模块的入口,就像 main 函数。set_root_context注册了我们的根上下文,每次有新连接进来,Envoy 会通过create_stream_context创建对应的流上下文。on_http_request_headers和on_http_response_headers分别在请求头和响应头到达时触发,我们就在那里面修改头部。Action::Continue表示让过滤器链继续处理,别停。
3.3 编译为 Wasm
写好后,编译成 wasm 文件:
cargo build --target wasm32-unknown-unknown --release
编译成功后,在 target/wasm32-unknown-unknown/release/ 目录下会有一个 custom_filter.wasm 文件。你可以用 wasm-tools validate custom_filter.wasm 检查是否有效。注意,默认编译出来的 wasm 可能比较大,可以用 wasm-opt -Oz -o custom_filter_opt.wasm custom_filter.wasm 优化一下。
四、注入服务网格
有了 wasm 文件,接下来要把它塞进 Envoy 代理里。服务网格以 Istio 为例,它默认用 Envoy 作为 sidecar。我们需要修改 Envoy 的静态配置或者通过 Istio 的 EnvoyFilter 资源来注入自定义过滤器。
4.1 配置 Envoy 代理
如果你手动管理 Envoy,直接在 envoy.yaml 里加上 Wasm 的 HttpFilter 配置:
# envoy.yaml 部分配置
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
codec_type: AUTO
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: my_service
http_filters:
# 这里注入我们自定义的 Wasm 过滤器
- name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: "custom_filter"
root_id: "my_root"
vm_config:
vm_id: "my_vm"
runtime: "envoy.wasm.runtime.v8"
code:
local:
filename: "/etc/envoy/custom_filter.wasm" # 把 wasm 文件放到这个路径
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
这里面 vm_config 指定了运行时为 V8,这是 Envoy 内置的 Wasm 运行时。root_id 要与 Rust 代码里 set_root_context 时的 id 匹配(我们没指定,默认空字符串)。
4.2 部署到服务网格
如果使用 Istio,更推荐通过 EnvoyFilter 自定义资源来动态注入,不用直接改 Sidecar 配置。创建一个 YAML 文件,比如 custom-filter.yaml:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: custom-filter
namespace: your-namespace
spec:
workloadSelector:
labels:
app: your-service # 只要这个标签的 Pod 才生效
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND # 入站流量方向
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
subFilter:
name: envoy.filters.http.router
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.wasm
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
config:
name: custom_filter
root_id: my_root
vm_config:
vm_id: my_vm
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/custom_filter.wasm
注意:这里假设你的 wasm 文件已经放在 sidecar 里了。可以通过 Istio 的 sidecar.istio.io/userVolume 挂载 ConfigMap 或者从远程下载。生产环境中更常用的是放在一个 HTTP 服务器上,用 remote 字段引用。
五、应用场景与优缺点分析
应用场景:
- 灰度发布标记:给特定用户的请求加上
X-Canary: true头,让上游服务路由到 canary 版本。 - 安全策略注入:比如给每个响应添加安全头
X-XSS-Protection,无需修改应用代码。 - 流量监控:提取请求中的 trace id 并附加到日志上下文里。
- 动态参数改写:在服务网格中统一修改请求路径前缀或参数。
优点:
- 热更新:Wasm 模块可以随时替换,无需重启 Envoy,滚动更新零中断。
- 语言无关:团队可以用熟悉的语言(Rust、Go、JS、C++)开发,降低门槛。
- 安全沙箱:Wasm 运行在 V8 沙箱里,即使模块崩溃也不会影响 Envoy 主进程。
- 体积小:编译后的 wasm 文件通常只有几十 KB,相比 C++ 插件几 MB 小得多。
缺点:
- 性能损耗:Wasm 运行时比原生 C++ 插件慢 10%-20%,对延迟敏感的场景需要谨慎。
- 能力限制:不能直接调用系统调用,只能通过 proxy-wasm API 操作,复杂逻辑(比如网络请求、文件操作)受限。
- 生态不成熟:部分语言的支持还不完善,调试工具较少,遇到问题可能难以排查。
六、注意事项
- Wasm 大小与加载时间:优化后的 wasm 文件最好小于 100KB,否则 Envoy 启动或热更新时加载慢,会影响恢复速度。
- 日志输出:开发时用
log::info!打印调试信息,但在生产环境要控制日志级别,避免打爆 Envoy 的日志。 - 错误处理:Wasm 模块里如果 panic,会导致当前请求被中断。一定要处理好错误边界,比如用
unwrap谨慎或改为优雅降级。 - 版本兼容:proxy-wasm 规范还在演进,不同版本的 Envoy 可能要求不同的 SDK 版本。务必参阅 Envoy 文档中的兼容性矩阵。
- 多语言选择:Rust 是最推荐的语言,但如果团队有 Go 基础,也可以使用
go-ext-wasm,只是编译产物体积会大一些。AssemblyScript 语法类似 TypeScript,上手快但性能较差。 - 生产部署:建议使用签名验证,确保 wasm 模块来自可信源。Envoy 支持对 wasm 文件进行 SHA256 校验。
七、总结
通过这个实战,我们从零搭建了一个 Rust 编写的 Envoy HTTP Filter,编译成 Wasm 模块,并注入到服务网格中。整个过程就像给家里的智能门装一个插件:写几行代码、编译、配置、生效。Wasm 过滤器让服务网格的扩展能力变得更加灵活和轻量,你可以快速实验新功能,而不用担心影响核心系统。下次遇到需要在网格层统一处理 HTTP 头、限流或监控的需求时,不妨试试这个方案——也许几分钟就能搞定,再也不用求着平台开发团队改代码了。
Comments