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 运行时:
| 运行时 | 特点 | 适用场景 |
|---|---|---|
| V8 | Chrome 引擎,性能最优 | 生产环境推荐 |
| WAMR | 轻量级,资源占用少 | 边缘计算 |
| Wasmtime | Rust 实现,安全性好 | 安全敏感场景 |
| 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 |
| Metadata | get_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_start | VM 启动时 |
on_configure | 插件配置加载时 |
HTTP 流回调:
| 函数 | 说明 |
|---|---|
on_http_request_headers | 请求头到达 |
on_http_request_body | 请求体到达 |
on_http_response_headers | 响应头到达 |
on_http_response_body | 响应体到达 |
on_http_call_response | HTTP 调用返回 |
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
}
}
语言选择#
对比分析#
| 语言 | 性能 | 体积 | 开发效率 | 生态 | 推荐场景 |
|---|---|---|---|---|---|
| Rust | 100% | 小 | 中 | 好 | 生产环境、高性能 |
| C++ | 100% | 最小 | 低 | 好 | 极致性能、团队熟悉 |
| TinyGo | 50-70% | 中 | 高 | 中 | 快速开发、简单逻辑 |
| AssemblyScript | 60-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 Registry | OCI Registry |
|---|---|---|
| 内容类型 | 主要是容器镜像 | 任意 artifact(镜像、Helm、Wasm) |
| Media Type | application/vnd.docker.* | application/vnd.oci.* |
| 工具 | docker push/pull | oras 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
平滑过渡
关键点:
- 修改
WasmPlugin的urltag(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 Registry | NAS(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 CRD | ECDS |
|---|---|---|
| 控制平面 | Istiod | 自建 ECDS Server |
| 配置方式 | K8s CRD | gRPC API |
| 灵活性 | 中等 | 高(完全自定义) |
| 运维复杂度 | 低 | 高 |
| 适用场景 | 标准 Istio 部署 | 自定义控制平面、多集群 |
| 热更新 | 通过 CRD 修改 | gRPC 流式推送 |
Wasm vs EnvoyFilter Lua#
核心对比#
| 对比项 | Wasm | EnvoyFilter + 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) |
| 需要调用外部服务 |
语言选择速查#
| 场景 | 推荐语言 |
|---|---|
| 生产环境、高 QPS | Rust |
| 快速原型开发 | TinyGo |
| 团队熟悉 C++ | C++ |
| 前端团队 | AssemblyScript |
检查清单#
□ 选择合适的语言和 SDK
□ 设计插件配置结构
□ 实现核心逻辑和错误处理
□ 添加日志和指标
□ 构建并推送 OCI 镜像
□ 部署 WasmPlugin CRD
□ 配置 failStrategy
□ 测试和性能验证
□ 监控运行状态