跳过正文
  1. Posts/

Istio Wasm 插件开发指南

·3088 字·15 分钟
王二麻
作者
王二麻
混迹 Linux 运维多年,专注 Kubernetes 生产实战、Golang 工具开发与稳定性工程。写点踩坑心得,聊聊技术人生。
目录
Istio 实战 - 这篇文章属于一个选集。
§ 17: 本文

WebAssembly (Wasm) 为 Istio/Envoy 提供了一种安全、高效的扩展机制。本文将深入介绍 Wasm 在 Istio 中的工作原理、应用场景及开发实践。

Wasm 基础
#

什么是 WebAssembly
#

WebAssembly 是一种二进制指令格式,最初为浏览器设计,现已扩展到服务端场景:

flowchart LR
    subgraph 源码
        A[Rust/C++/Go]
    end
    
    subgraph 编译
        B[LLVM/编译器]
    end
    
    subgraph 运行时
        C[Wasm 虚拟机]
    end
    
    subgraph 执行
        D[沙箱执行]
    end
    
    A --> B --> C --> D

Wasm 核心特性
#

特性说明对 Istio 的价值
沙箱隔离独立内存空间,无法访问宿主安全扩展,插件崩溃不影响 Envoy
跨平台统一的字节码格式一次编译,到处运行
接近原生性能编译为机器码执行低延迟,适合数据面
动态加载运行时加载/卸载热更新,无需重启 Envoy
多语言支持Rust/C++/Go/AssemblyScript团队技术栈灵活

Envoy 中的 Wasm 运行时
#

Envoy 支持多种 Wasm 运行时:

运行时特点适用场景
V8Chrome 引擎,性能最优生产环境推荐
WAMR轻量级,资源占用少边缘计算
WasmtimeRust 实现,安全性好安全敏感场景
WaZero纯 Go 实现特殊环境

Istio 默认使用 V8 运行时。

Proxy-Wasm 规范
#

架构概览
#

Proxy-Wasm 是 Wasm 与代理(如 Envoy)交互的 ABI 规范:

flowchart TB
    subgraph Envoy进程
        A[Envoy Core] <--> B[Wasm VM]
        B <--> C[Wasm 插件]
    end
    
    subgraph 交互接口
        D[Host Functions
Envoy 提供给 Wasm] E[Guest Functions
Wasm 暴露给 Envoy] end A --> D --> C C --> E --> A

Host Functions(Envoy → Wasm)
#

Envoy 提供给 Wasm 插件调用的函数:

类别函数
Header 操作get_header, add_header, remove_header
Body 操作get_body, set_body, append_body
Metadataget_property, set_property
HTTP 调用http_call, grpc_call
日志log
指标increment_metric, record_metric
共享数据get_shared_data, set_shared_data

Guest Functions(Wasm → Envoy)
#

Wasm 插件暴露给 Envoy 的回调函数:

生命周期回调

函数说明
on_vm_startVM 启动时
on_configure插件配置加载时

HTTP 流回调

函数说明
on_http_request_headers请求头到达
on_http_request_body请求体到达
on_http_response_headers响应头到达
on_http_response_body响应体到达
on_http_call_responseHTTP 调用返回

TCP 流回调

函数说明
on_downstream_data下游数据到达
on_upstream_data上游数据到达

Istio 中的 Wasm 集成点
#

集成位置
#

flowchart LR
    subgraph 外部流量
        Client[客户端]
    end
    
    subgraph Istio网格
        GW[Ingress Gateway
可加载 Wasm] subgraph Pod_A SA[Sidecar A
可加载 Wasm] AppA[App A] end subgraph Pod_B SB[Sidecar B
可加载 Wasm] AppB[App B] end EGW[Egress Gateway
可加载 Wasm] end subgraph 外部服务 External[External API] end Client --> GW --> SA --> AppA AppA --> SA --> SB --> AppB AppB --> SB --> EGW --> External

WasmPlugin CRD
#

Istio 1.12+ 提供了 WasmPlugin CRD 来管理 Wasm 插件:

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-wasm-plugin
  namespace: istio-system
spec:
  # 选择器:应用到哪些工作负载
  selector:
    matchLabels:
      istio: ingressgateway  # 应用到 Gateway
  # 或者
  # targetRefs:
  # - kind: Gateway
  #   name: my-gateway

  # Wasm 模块来源
  url: oci://ghcr.io/my-org/my-plugin:v1.0.0
  # 或本地文件
  # url: file:///etc/istio/plugins/my-plugin.wasm
  
  # 插件配置
  pluginConfig:
    key1: value1
    key2: value2
  
  # 插件阶段
  phase: AUTHN  # UNSPECIFIED_PHASE, AUTHN, AUTHZ, STATS
  
  # 优先级(同阶段内)
  priority: 10
  
  # 镜像拉取策略
  imagePullPolicy: IfNotPresent
  
  # 镜像拉取凭证
  imagePullSecret: my-registry-secret
  
  # 失败策略
  failStrategy: FAIL_CLOSE  # FAIL_CLOSE, FAIL_OPEN
  
  # VM 配置
  vmConfig:
    env:
    - name: MY_ENV
      value: my-value

作用范围
#

# 1. 全局(所有 Sidecar + Gateway)
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: global-plugin
  namespace: istio-system  # 在 istio-system 且无 selector
spec:
  url: oci://my-plugin:v1

---
# 2. 命名空间级别
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: namespace-plugin
  namespace: production  # 仅影响 production 命名空间
spec:
  url: oci://my-plugin:v1

---
# 3. 工作负载级别
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: workload-plugin
  namespace: production
spec:
  selector:
    matchLabels:
      app: my-service  # 仅影响特定服务
  url: oci://my-plugin:v1

插件阶段(Phase)
#

flowchart LR
    A[请求进入] --> B[AUTHN
认证] B --> C[AUTHZ
授权] C --> D[STATS
统计] D --> E[路由处理] E --> F[上游服务]
阶段说明典型用途
AUTHN认证阶段,最早执行JWT 验证、Token 解析
AUTHZ授权阶段权限检查、黑白名单
STATS统计阶段,路由后指标采集、日志增强

常用场景
#

场景 1:自定义认证
#

// Rust 实现 JWT 验证
use proxy_wasm::traits::*;
use proxy_wasm::types::*;
use jsonwebtoken::{decode, DecodingKey, Validation};

struct JwtAuthFilter {
    secret: String,
}

impl HttpContext for JwtAuthFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // 获取 Authorization Header
        let auth_header = match self.get_http_request_header("authorization") {
            Some(h) => h,
            None => {
                self.send_http_response(401, vec![], Some(b"Missing Authorization"));
                return Action::Pause;
            }
        };
        
        // 解析 Bearer Token
        let token = auth_header.strip_prefix("Bearer ").unwrap_or("");
        
        // 验证 JWT
        match decode::<Claims>(
            token,
            &DecodingKey::from_secret(self.secret.as_bytes()),
            &Validation::default(),
        ) {
            Ok(token_data) => {
                // 将用户信息传递给上游
                self.set_http_request_header("x-user-id", Some(&token_data.claims.sub));
                Action::Continue
            }
            Err(_) => {
                self.send_http_response(401, vec![], Some(b"Invalid Token"));
                Action::Pause
            }
        }
    }
}
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: jwt-auth
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  url: oci://my-registry/jwt-auth:v1
  phase: AUTHN
  pluginConfig:
    secret: "my-jwt-secret"
  failStrategy: FAIL_CLOSE

场景 2:请求/响应改写
#

// 添加请求追踪 Header
impl HttpContext for TraceHeaderFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // 生成或传播 Trace ID
        let trace_id = match self.get_http_request_header("x-trace-id") {
            Some(id) => id,
            None => generate_trace_id(),
        };
        
        // 设置 Header
        self.set_http_request_header("x-trace-id", Some(&trace_id));
        
        // 存储到 Context,用于响应
        self.set_property(vec!["trace_id"], Some(trace_id.as_bytes()));
        
        Action::Continue
    }
    
    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        // 在响应中也添加 Trace ID
        if let Some(trace_id) = self.get_property(vec!["trace_id"]) {
            let trace_id_str = String::from_utf8(trace_id).unwrap();
            self.set_http_response_header("x-trace-id", Some(&trace_id_str));
        }
        Action::Continue
    }
}

场景 3:流量染色(灰度标记)
#

// 根据用户特征染色
impl HttpContext for TrafficTagFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // 已有泳道标记,不处理
        if self.get_http_request_header("x-swimlane").is_some() {
            return Action::Continue;
        }
        
        // 根据用户 ID 染色
        if let Some(user_id) = self.get_http_request_header("x-user-id") {
            let hash = calculate_hash(&user_id);
            
            let swimlane = if hash % 100 < 10 {
                "canary"  // 10% 用户走金丝雀
            } else {
                "production"
            };
            
            self.set_http_request_header("x-swimlane", Some(swimlane));
        }
        
        Action::Continue
    }
}

场景 4:限流
#

use std::collections::HashMap;
use std::time::{Duration, Instant};

struct RateLimitFilter {
    limits: HashMap<String, (u32, Instant)>,  // IP -> (count, window_start)
    max_requests: u32,
    window_seconds: u64,
}

impl HttpContext for RateLimitFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        let client_ip = self.get_http_request_header("x-forwarded-for")
            .unwrap_or_else(|| "unknown".to_string());
        
        let now = Instant::now();
        let window = Duration::from_secs(self.window_seconds);
        
        let (count, window_start) = self.limits
            .entry(client_ip.clone())
            .or_insert((0, now));
        
        // 窗口过期,重置
        if now.duration_since(*window_start) > window {
            *count = 0;
            *window_start = now;
        }
        
        *count += 1;
        
        if *count > self.max_requests {
            self.send_http_response(
                429,
                vec![("x-rate-limit-exceeded", "true")],
                Some(b"Rate limit exceeded"),
            );
            return Action::Pause;
        }
        
        // 添加限流信息 Header
        self.set_http_response_header(
            "x-rate-limit-remaining",
            Some(&(self.max_requests - *count).to_string()),
        );
        
        Action::Continue
    }
}

场景 5:请求体修改
#

impl HttpContext for BodyModifyFilter {
    fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
        if !end_of_stream {
            return Action::Pause;  // 等待完整 body
        }
        
        if let Some(body) = self.get_http_request_body(0, body_size) {
            // 解析 JSON
            if let Ok(mut json) = serde_json::from_slice::<serde_json::Value>(&body) {
                // 添加字段
                json["server_timestamp"] = serde_json::json!(get_current_time());
                json["request_id"] = serde_json::json!(generate_request_id());
                
                // 替换 body
                let new_body = serde_json::to_vec(&json).unwrap();
                self.set_http_request_body(0, body_size, &new_body);
            }
        }
        
        Action::Continue
    }
}

场景 6:外部服务调用
#

impl HttpContext for ExternalCallFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        let token = self.get_http_request_header("authorization").unwrap_or_default();
        
        // 调用外部认证服务
        self.dispatch_http_call(
            "auth-service",  // cluster name
            vec![
                (":method", "POST"),
                (":path", "/verify"),
                (":authority", "auth.example.com"),
                ("content-type", "application/json"),
            ],
            Some(format!(r#"{{"token":"{}"}}"#, token).as_bytes()),
            vec![],
            Duration::from_secs(5),
        ).unwrap();
        
        Action::Pause  // 等待外部调用返回
    }
    
    fn on_http_call_response(&mut self, _: u32, _: usize, body_size: usize, _: usize) {
        if let Some(body) = self.get_http_call_response_body(0, body_size) {
            let response: AuthResponse = serde_json::from_slice(&body).unwrap();
            
            if response.valid {
                self.set_http_request_header("x-user-id", Some(&response.user_id));
                self.resume_http_request();
            } else {
                self.send_http_response(401, vec![], Some(b"Unauthorized"));
            }
        }
    }
}

场景 7:自定义指标
#

impl HttpContext for MetricsFilter {
    fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action {
        let status = self.get_http_response_header(":status").unwrap_or_default();
        let path = self.get_http_request_header(":path").unwrap_or_default();
        
        // 记录指标
        self.increment_metric(
            self.define_metric(
                MetricType::Counter,
                &format!("custom_requests_total{{path=\"{}\",status=\"{}\"}}", path, status),
            ).unwrap(),
            1,
        );
        
        // 记录延迟
        if let Some(start_time) = self.get_property(vec!["request_start_time"]) {
            let duration = get_current_time_ms() - parse_time(&start_time);
            self.record_metric(
                self.define_metric(
                    MetricType::Histogram,
                    &format!("custom_request_duration_ms{{path=\"{}\"}}", path),
                ).unwrap(),
                duration as u64,
            );
        }
        
        Action::Continue
    }
}

语言选择
#

对比分析
#

语言性能体积开发效率生态推荐场景
Rust100%生产环境、高性能
C++100%最小极致性能、团队熟悉
TinyGo50-70%快速开发、简单逻辑
AssemblyScript60-80%前端团队、TypeScript

Rust(推荐)
#

优势

  • 性能最优
  • 内存安全
  • proxy-wasm SDK 最活跃
  • Istio 官方示例使用 Rust

开发环境

# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 添加 Wasm target
rustup target add wasm32-unknown-unknown

# 创建项目
cargo new --lib my-wasm-plugin
cd my-wasm-plugin

Cargo.toml

[package]
name = "my-wasm-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
proxy-wasm = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[profile.release]
lto = true
opt-level = 3
strip = true

构建

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

# 输出文件
ls target/wasm32-unknown-unknown/release/*.wasm

TinyGo
#

优势

  • Go 语法,学习成本低
  • 开发速度快
  • 适合简单逻辑

开发环境

# 安装 TinyGo
brew install tinygo  # macOS

# 创建项目
mkdir my-wasm-plugin && cd my-wasm-plugin
go mod init my-wasm-plugin

main.go

package main

import (
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
    "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {
    proxywasm.SetVMContext(&vmContext{})
}

type vmContext struct{}

func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
    return types.OnVMStartStatusOK
}

func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
    return &pluginContext{}
}

type pluginContext struct {
    types.DefaultPluginContext
}

func (*pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
    return &httpContext{}
}

type httpContext struct {
    types.DefaultHttpContext
}

func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
    path, _ := proxywasm.GetHttpRequestHeader(":path")
    proxywasm.LogInfof("Request path: %s", path)
    
    // 添加 Header
    proxywasm.AddHttpRequestHeader("x-wasm-plugin", "true")
    
    return types.ActionContinue
}

构建

tinygo build -o plugin.wasm -scheduler=none -target=wasi ./main.go

AssemblyScript
#

优势

  • TypeScript 语法
  • 前端团队友好

示例

import {
  RootContext,
  Context,
  FilterHeadersStatusValues,
  stream_context,
} from "@solo-io/proxy-runtime";

class MyHttpContext extends Context {
  onRequestHeaders(headersCount: u32, endOfStream: bool): FilterHeadersStatusValues {
    const path = stream_context.headers.request.get(":path");
    stream_context.headers.request.add("x-wasm-plugin", "true");
    return FilterHeadersStatusValues.Continue;
  }
}

分发与加载机制
#

OCI vs Docker Registry
#

OCI (Open Container Initiative) 是容器镜像的开放标准,Docker Registry 是其实现之一:

flowchart TB
    subgraph OCI["OCI Distribution Spec"]
        A[Docker Image Layer]
        B[Helm Chart Artifact]
        C[Wasm Module Artifact]
    end
    
    OCI --> D[Docker Hub]
    OCI --> E[GitHub GHCR]
    OCI --> F[Harbor]
对比项Docker RegistryOCI Registry
内容类型主要是容器镜像任意 artifact(镜像、Helm、Wasm)
Media Typeapplication/vnd.docker.*application/vnd.oci.*
工具docker push/pulloras push/pull, crane
兼容性OCI 兼容原生 OCI 标准

Wasm 模块的 OCI 分发

# 安装 ORAS CLI
brew install oras

# 推送 Wasm 模块
oras push ghcr.io/my-org/my-plugin:v1.0.0 \
    my-plugin.wasm:application/vnd.module.wasm.content.layer.v1+wasm

# 拉取 Wasm 模块
oras pull ghcr.io/my-org/my-plugin:v1.0.0

# 查看 manifest
oras manifest fetch ghcr.io/my-org/my-plugin:v1.0.0

为什么使用 OCI 分发 Wasm

  • 复用现有 Registry 基础设施(Harbor、GHCR)
  • 统一的版本管理和标签
  • 支持签名和安全扫描
  • 与 GitOps 流程集成

热更新机制
#

Wasm 插件的"热更新"实际上有三种模式:

1. OCI 模式热更新
#

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
spec:
  url: oci://ghcr.io/my-org/my-plugin:v1.0.0  # 修改 tag 触发更新
  imagePullPolicy: Always  # 或 IfNotPresent

工作原理

sequenceDiagram
    participant User as 用户
    participant K8s as Kubernetes
    participant Istiod as Istiod
    participant Envoy as Envoy Sidecar
    participant Registry as OCI Registry

    User->>K8s: 更新 WasmPlugin (v1→v2)
    K8s->>Istiod: Watch 到变更
    Istiod->>Registry: 拉取新 Wasm 模块
    Registry-->>Istiod: 返回 v2 模块
    Istiod->>Envoy: xDS 推送新配置
    Envoy->>Envoy: 加载新 Wasm VM
    Note over Envoy: 新请求使用 v2
旧请求继续用 v1
平滑过渡

关键点

  • 修改 WasmPluginurl tag(v1→v2)触发更新
  • Istiod 检测变更后拉取新模块
  • 通过 xDS 推送给 Envoy
  • Envoy 创建新的 Wasm VM,旧 VM 处理完存量请求后释放

2. File 模式热更新
#

File 模式本身不支持自动热更新,需要手动触发:

# 方式一:更新 ConfigMap(需要重启 Pod)
apiVersion: v1
kind: ConfigMap
metadata:
  name: wasm-plugins
binaryData:
  my-plugin.wasm: <new-base64-content>

# Pod 需要重启才能读取新文件
kubectl rollout restart deployment/istio-ingressgateway -n istio-system
# 方式二:使用 subPath 挂载 + inotify(复杂)
# ConfigMap 更新不会自动同步到 subPath 挂载的文件
# 需要额外的 sidecar 监听文件变化并通知 Envoy

File 模式热更新的变通方案

# 使用版本化文件名 + WasmPlugin 更新
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
spec:
  url: file:///var/local/wasm/my-plugin-v2.wasm  # 改文件名触发更新

3. HTTP 模式热更新
#

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
spec:
  url: http://wasm-server.internal/plugins/my-plugin.wasm
  # 无法自动检测远程文件变更
  # 需要修改 WasmPlugin 资源触发重新拉取

热更新模式对比

模式自动热更新触发方式推荐场景
OCI修改 tag生产环境
File重启 Pod 或改文件名本地开发
HTTP修改 WasmPlugin内部测试

OCI vs NAS 生产环境选型
#

在生产环境中,OCI Registry 和 NAS(网络附加存储)是两种常见的 Wasm 分发方式:

对比项OCI RegistryNAS(File 模式)
版本管理原生支持(tag/digest)需自建(文件名版本化)
热更新修改 tag 触发 xDS需改文件名或重启 Pod
安全扫描支持(Trivy/Harbor)需额外工具
签名验证支持(cosign/notation)需额外实现
依赖需要 Registry 服务需要 NAS 存储
网络依赖拉取时需访问 Registry挂载后本地读取
启动速度首次拉取较慢本地读取快
CI/CD 集成原生支持需额外脚本
多集群天然支持(Registry 共享)需多集群 NAS 同步
故障隔离Registry 故障影响拉取NAS 故障影响挂载

推荐 OCI 的场景
#

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: auth-plugin
spec:
  url: oci://harbor.internal/wasm/auth-plugin:v1.2.3
  imagePullPolicy: IfNotPresent
  imagePullSecret: harbor-credentials

适合

  • 已有 Harbor/GHCR 等 Registry 基础设施
  • GitOps 发布流程(Argo CD / Flux)
  • 需要严格版本控制和审计
  • 多集群统一分发
  • 需要安全扫描和签名

推荐 NAS 的场景
#

# 挂载 NAS 卷
apiVersion: apps/v1
kind: Deployment
metadata:
  name: istio-ingressgateway
spec:
  template:
    spec:
      containers:
      - name: istio-proxy
        volumeMounts:
        - name: wasm-plugins
          mountPath: /var/local/wasm
          readOnly: true
      volumes:
      - name: wasm-plugins
        nfs:
          server: nas.internal
          path: /wasm-plugins
---
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: auth-plugin
spec:
  url: file:///var/local/wasm/auth-plugin-v1.2.3.wasm

适合

  • 内网隔离环境(无法访问外部 Registry)
  • 对启动速度敏感(避免拉取延迟)
  • 已有 NAS 基础设施
  • 插件文件较大(避免重复拉取)

高可用方案:OCI + 本地缓存
#

apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: auth-plugin
spec:
  url: oci://harbor.internal/wasm/auth-plugin:v1.2.3
  imagePullPolicy: IfNotPresent  # 缓存后不再拉取
  failStrategy: FAIL_OPEN        # Registry 故障时降级

选型决策
#

场景推荐方案
标准生产环境OCI(Harbor/GHCR)
离线/内网隔离NAS + 文件名版本化
高可用要求OCI + IfNotPresent + FAIL_OPEN
多集群OCI(Registry 共享)
快速迭代开发NAS(避免 push/pull 延迟)

结论OCI 更适合生产环境,因为原生版本管理、GitOps 集成、安全扫描支持。NAS 适合作为离线环境的补充方案。

ECDS 机制加载 Wasm
#

ECDS (Extension Configuration Discovery Service) 是 Envoy 的扩展配置发现服务,允许动态加载扩展:

flowchart TB
    subgraph 控制平面
        ECDS[ECDS Server]
        Config[配置存储]
    end
    
    subgraph 数据平面
        Envoy1[Envoy 1]
        Envoy2[Envoy 2]
        Envoy3[Envoy 3]
    end
    
    Config --> ECDS
    ECDS <-->|gRPC Stream| Envoy1
    ECDS <-->|gRPC Stream| Envoy2
    ECDS <-->|gRPC Stream| Envoy3

ECDS 工作原理
#

# Envoy Bootstrap 配置中启用 ECDS
static_resources:
  clusters:
  - name: ecds_cluster
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: ecds_cluster
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: ecds-server.example.com
                port_value: 18000

dynamic_resources:
  ecds_config:
    api_config_source:
      api_type: GRPC
      transport_api_version: V3
      grpc_services:
      - envoy_grpc:
          cluster_name: ecds_cluster

通过 EnvoyFilter 使用 ECDS 加载 Wasm
#

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: ecds-wasm-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: GATEWAY
      listener:
        filterChain:
          filter:
            name: envoy.filters.network.http_connection_manager
            subFilter:
              name: envoy.filters.http.router
    patch:
      operation: INSERT_BEFORE
      value:
        name: my-wasm-filter
        config_discovery:
          # 启用 ECDS
          config_source:
            api_config_source:
              api_type: GRPC
              transport_api_version: V3
              grpc_services:
              - envoy_grpc:
                  cluster_name: ecds_cluster
          # 无配置时的默认行为
          apply_default_config_without_warming: true
          default_config:
            "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
            config:
              name: "my-wasm-filter"
              root_id: "my-wasm-filter"
              vm_config:
                runtime: "envoy.wasm.runtime.v8"
                code:
                  local:
                    filename: /var/local/wasm/default-plugin.wasm
          type_urls:
          - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm

ECDS Server 实现示例
#

// 简化的 ECDS Server 实现
type ECDSServer struct {
    configs map[string]*anypb.Any
}

func (s *ECDSServer) StreamExtensionConfigs(
    stream ecds.ExtensionConfigDiscoveryService_StreamExtensionConfigsServer,
) error {
    for {
        req, err := stream.Recv()
        if err != nil {
            return err
        }
        
        // 根据请求返回对应的扩展配置
        var resources []*anypb.Any
        for _, name := range req.ResourceNames {
            if config, ok := s.configs[name]; ok {
                resources = append(resources, config)
            }
        }
        
        // 推送配置
        resp := &discovery.DiscoveryResponse{
            TypeUrl:   "type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig",
            Resources: resources,
            VersionInfo: s.getVersion(),
        }
        
        if err := stream.Send(resp); err != nil {
            return err
        }
    }
}

// 更新配置时,ECDS Server 主动推送给所有连接的 Envoy
func (s *ECDSServer) UpdateWasmConfig(name string, wasmConfig *wasm.Wasm) {
    config, _ := anypb.New(&core.TypedExtensionConfig{
        Name: name,
        TypedConfig: mustAny(wasmConfig),
    })
    s.configs[name] = config
    s.pushToAllClients()  // 推送给所有 Envoy
}

ECDS vs WasmPlugin
#

对比项WasmPlugin CRDECDS
控制平面Istiod自建 ECDS Server
配置方式K8s CRDgRPC API
灵活性中等高(完全自定义)
运维复杂度
适用场景标准 Istio 部署自定义控制平面、多集群
热更新通过 CRD 修改gRPC 流式推送

Wasm vs EnvoyFilter Lua
#

核心对比
#

对比项WasmEnvoyFilter + Lua
性能接近原生(V8 JIT)较慢(解释执行)
启动时间较长(VM 初始化)快(脚本加载)
内存隔离完全隔离(沙箱)共享 Envoy 内存
语言选择Rust/C++/Go/AS仅 Lua
调试困难(二进制)简单(脚本)
热更新支持(新 VM)支持(脚本替换)
包管理Cargo/Go Modules手动管理
外部调用支持 HTTP/gRPC受限
类型安全编译时检查运行时检查

性能对比
#

基准测试:Header 操作 (1000 QPS)

操作类型          Wasm (Rust)    Lua         差距
─────────────────────────────────────────────────
读取 Header       ~100 ns        ~200 ns     2x
设置 Header       ~120 ns        ~250 ns     2x
JSON 解析         ~500 ns        ~3000 ns    6x
正则匹配          ~1 μs          ~5 μs       5x
字符串拼接        ~50 ns         ~300 ns     6x

结论:简单操作 Lua 慢 2 倍,复杂操作慢 5-6 倍

使用场景对比
#

适合 Lua 的场景
#

# 1. 简单的 Header 操作
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: add-header
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.filters.http.lua
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
          inlineCode: |
            function envoy_on_request(handle)
              handle:headers():add("x-custom-header", "value")
            end
# 2. 简单的日志增强
inlineCode: |
  function envoy_on_request(handle)
    local path = handle:headers():get(":path")
    handle:logInfo("Request path: " .. path)
  end
# 3. 简单的条件判断
inlineCode: |
  function envoy_on_request(handle)
    local user_agent = handle:headers():get("user-agent") or ""
    if string.find(user_agent, "bot") then
      handle:respond({[":status"] = "403"}, "Forbidden")
    end
  end

适合 Wasm 的场景
#

// 1. 复杂的认证逻辑(JWT 解析、签名验证)
use jsonwebtoken::{decode, DecodingKey, Validation};

fn verify_jwt(token: &str, secret: &str) -> Result<Claims, Error> {
    decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::default(),
    ).map(|data| data.claims)
}
// 2. 高 QPS 场景
// Wasm 的 JIT 编译在高 QPS 下优势明显
// 每请求节省的微秒累积起来很可观
// 3. 需要外部服务调用
self.dispatch_http_call(
    "auth-service",
    vec![(":method", "POST"), (":path", "/verify")],
    Some(body.as_bytes()),
    vec![],
    Duration::from_secs(5),
)?;
// 4. 复杂的数据处理(JSON/Protobuf)
let request: MyRequest = serde_json::from_slice(&body)?;
let transformed = transform(request);
let response = serde_json::to_vec(&transformed)?;

混合使用策略
#

flowchart LR
    A["Wasm Filter
(认证/限流)
高性能需求"] --> B["Lua Filter
(简单标记)
快速迭代"] --> C["Wasm Filter
(指标采集)
高性能需求"]

选型决策树
#

需要高性能(高 QPS/复杂计算)?
    ├── 是 → Wasm
    └── 否 → 继续判断
            需要调用外部服务?
            ├── 是 → Wasm
            └── 否 → 继续判断
                    需要复杂数据处理(JSON/加密)?
                    ├── 是 → Wasm
                    └── 否 → 继续判断
                            需要快速迭代/频繁修改?
                            ├── 是 → Lua
                            └── 否 → 继续判断
                                    团队熟悉 Rust/Go?
                                    ├── 是 → Wasm
                                    └── 否 → Lua

迁移策略
#

从 Lua 迁移到 Wasm 的典型路径:

# 阶段 1:Lua 原型
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: rate-limit-lua
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.filters.http.lua
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
          inlineCode: |
            -- 简单限流原型
            local counter = {}
            function envoy_on_request(handle)
              local ip = handle:headers():get("x-forwarded-for")
              counter[ip] = (counter[ip] or 0) + 1
              if counter[ip] > 100 then
                handle:respond({[":status"] = "429"}, "Rate limited")
              end
            end
# 阶段 2:性能验证后迁移到 Wasm
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: rate-limit-wasm
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  url: oci://my-registry/rate-limit:v1
  phase: AUTHZ
  pluginConfig:
    max_requests: 100
    window_seconds: 60

部署与调试
#

OCI 镜像分发
#

# 构建 Wasm 模块
cargo build --target wasm32-unknown-unknown --release

# 使用 ORAS 推送到 OCI Registry
oras push ghcr.io/my-org/my-plugin:v1.0.0 \
    target/wasm32-unknown-unknown/release/my_plugin.wasm:application/vnd.module.wasm.content.layer.v1+wasm

本地开发调试
#

# 使用 ConfigMap 挂载 Wasm 文件
apiVersion: v1
kind: ConfigMap
metadata:
  name: wasm-plugins
  namespace: istio-system
binaryData:
  my-plugin.wasm: <base64-encoded-wasm>
---
# 挂载到 Gateway
apiVersion: apps/v1
kind: Deployment
metadata:
  name: istio-ingressgateway
  namespace: istio-system
spec:
  template:
    spec:
      containers:
      - name: istio-proxy
        volumeMounts:
        - name: wasm-plugins
          mountPath: /var/local/wasm
      volumes:
      - name: wasm-plugins
        configMap:
          name: wasm-plugins
---
# WasmPlugin 引用本地文件
apiVersion: extensions.istio.io/v1alpha1
kind: WasmPlugin
metadata:
  name: my-plugin
  namespace: istio-system
spec:
  selector:
    matchLabels:
      istio: ingressgateway
  url: file:///var/local/wasm/my-plugin.wasm

日志调试
#

// Rust 中使用日志
use proxy_wasm::hostcalls::log;
use proxy_wasm::types::LogLevel;

log(LogLevel::Info, "Processing request...");
log(LogLevel::Debug, &format!("Header value: {:?}", header_value));
log(LogLevel::Error, "Something went wrong!");

查看日志:

# Gateway 日志
kubectl logs -n istio-system deployment/istio-ingressgateway -c istio-proxy | grep wasm

# Sidecar 日志
kubectl logs <pod-name> -c istio-proxy | grep wasm

性能分析
#

# 启用 Envoy Admin 接口
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: enable-wasm-stats
spec:
  configPatches:
  - applyTo: BOOTSTRAP
    patch:
      operation: MERGE
      value:
        stats_config:
          stats_tags:
          - tag_name: wasm_filter
            regex: "^wasm\\.(.+?)\\."

查看指标:

# 进入 Pod 执行
kubectl exec -it <pod-name> -c istio-proxy -- \
    curl localhost:15000/stats | grep wasm

最佳实践
#

1. 错误处理
#

impl HttpContext for SafeFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // 使用 Result 处理错误
        match self.process_request() {
            Ok(_) => Action::Continue,
            Err(e) => {
                log(LogLevel::Error, &format!("Error: {:?}", e));
                // 根据 failStrategy 决定行为
                if self.fail_open {
                    Action::Continue
                } else {
                    self.send_http_response(500, vec![], Some(b"Internal Error"));
                    Action::Pause
                }
            }
        }
    }
}

2. 配置管理
#

#[derive(Deserialize)]
struct PluginConfig {
    enabled: bool,
    rules: Vec<Rule>,
    timeout_ms: u64,
}

impl RootContext for MyRootContext {
    fn on_configure(&mut self, _: usize) -> bool {
        if let Some(config_bytes) = self.get_plugin_configuration() {
            match serde_json::from_slice::<PluginConfig>(&config_bytes) {
                Ok(config) => {
                    self.config = config;
                    true
                }
                Err(e) => {
                    log(LogLevel::Error, &format!("Config parse error: {:?}", e));
                    false
                }
            }
        } else {
            log(LogLevel::Warn, "No configuration provided");
            true  // 使用默认配置
        }
    }
}

3. 性能优化
#

// 1. 避免不必要的内存分配
impl HttpContext for OptimizedFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        // 差:每次创建新 String
        // let path = self.get_http_request_header(":path").unwrap_or(String::new());
        
        // 好:使用 Option 避免不必要的 String 创建
        if let Some(path) = self.get_http_request_header(":path") {
            if path.starts_with("/api/") {
                // 处理
            }
        }
        Action::Continue
    }
}

// 2. 复用编译好的正则
lazy_static! {
    static ref PATH_REGEX: Regex = Regex::new(r"^/api/v\d+/").unwrap();
}

// 3. 使用共享数据缓存
fn get_cached_config(&self) -> Option<Config> {
    self.get_shared_data("config_cache")
        .and_then(|(data, _)| serde_json::from_slice(&data).ok())
}

4. 测试
#

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_header_processing() {
        let filter = MyFilter::new();
        // 单元测试逻辑
    }
}

// 集成测试使用 Envoy 的测试框架
// 或使用 wasme 的测试工具

总结
#

何时使用 Wasm
#

适合不适合
自定义认证/授权简单 Header 操作(用 EnvoyFilter)
复杂流量染色逻辑静态配置(用 VirtualService)
请求/响应改写已有 Envoy 原生 Filter 满足
自定义指标采集对延迟极度敏感(考虑原生 Filter)
需要调用外部服务

语言选择速查
#

场景推荐语言
生产环境、高 QPSRust
快速原型开发TinyGo
团队熟悉 C++C++
前端团队AssemblyScript

检查清单
#

□ 选择合适的语言和 SDK
□ 设计插件配置结构
□ 实现核心逻辑和错误处理
□ 添加日志和指标
□ 构建并推送 OCI 镜像
□ 部署 WasmPlugin CRD
□ 配置 failStrategy
□ 测试和性能验证
□ 监控运行状态
Istio 实战 - 这篇文章属于一个选集。
§ 17: 本文

相关文章