一、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_headerson_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 操作,复杂逻辑(比如网络请求、文件操作)受限。
  • 生态不成熟:部分语言的支持还不完善,调试工具较少,遇到问题可能难以排查。

六、注意事项

  1. Wasm 大小与加载时间:优化后的 wasm 文件最好小于 100KB,否则 Envoy 启动或热更新时加载慢,会影响恢复速度。
  2. 日志输出:开发时用 log::info! 打印调试信息,但在生产环境要控制日志级别,避免打爆 Envoy 的日志。
  3. 错误处理:Wasm 模块里如果 panic,会导致当前请求被中断。一定要处理好错误边界,比如用 unwrap 谨慎或改为优雅降级。
  4. 版本兼容:proxy-wasm 规范还在演进,不同版本的 Envoy 可能要求不同的 SDK 版本。务必参阅 Envoy 文档中的兼容性矩阵。
  5. 多语言选择:Rust 是最推荐的语言,但如果团队有 Go 基础,也可以使用 go-ext-wasm,只是编译产物体积会大一些。AssemblyScript 语法类似 TypeScript,上手快但性能较差。
  6. 生产部署:建议使用签名验证,确保 wasm 模块来自可信源。Envoy 支持对 wasm 文件进行 SHA256 校验。

七、总结

通过这个实战,我们从零搭建了一个 Rust 编写的 Envoy HTTP Filter,编译成 Wasm 模块,并注入到服务网格中。整个过程就像给家里的智能门装一个插件:写几行代码、编译、配置、生效。Wasm 过滤器让服务网格的扩展能力变得更加灵活和轻量,你可以快速实验新功能,而不用担心影响核心系统。下次遇到需要在网格层统一处理 HTTP 头、限流或监控的需求时,不妨试试这个方案——也许几分钟就能搞定,再也不用求着平台开发团队改代码了。