WasmPlugin 与 wasm-go SDK
这是 Higress 扩展能力的核心载体。20 讲覆盖从 CRD 字段到 Envoy 沙箱加载,再到 wasm-go SDK 的所有关键点,让你能写出工程级 Wasm 插件。
Wasm 在网关里干嘛
Wasm 让我们用任意语言写"挂载在 Envoy 上的中间件",并具备:
- 沙箱隔离:插件崩溃不影响主进程。
- 热更新:换 .wasm 镜像即生效,无需重启。
- 跨语言:Go / Rust / C++ / AssemblyScript 都能产出 .wasm。
- 性能:编译执行,比 Lua 快。
proxy-wasm ABI
proxy-wasm 是 Envoy / Istio 社区定义的标准 ABI:定义 Wasm 模块与 Host 通信的函数列表(如 proxy_on_request_headers、proxy_send_http_response)。Higress 的 Envoy 与 plugins 都遵循它。
WasmPlugin CRD 字段
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata: { name: my-plugin, namespace: higress-system }
spec:
url: oci://reg.example.com/wasm/my-plugin:v1
imagePullPolicy: Always
phase: AUTHN
priority: 100
defaultConfig: { key: value }
matchRules:
- domain: ["example.com"]
ingress: ["foo"]
config: { key: override }phase 与 priority
phase 枚举:AUTHN / AUTHZ / STATS / UNSPECIFIED。同 phase 内按 priority 降序执行。这决定了 Wasm filter 在 HTTP filter chain 的位置:AUTHN 最早,STATS 最晚。
matchRules 路由覆盖
同一插件可对不同 host / ingress 使用不同配置。IngressConfig 把每条 matchRule 渲染成单独的 EnvoyFilter patch route,让 Envoy 在每条 route 上 attach 不同 plugin config。
加载链路全图
用户 kubectl apply WasmPlugin
→ wasmplugin Controller (Informer)
→ IngressConfig.AddOrUpdateWasmPlugin
→ convertWasmPlugin → 输出 Istio extensions.WasmPlugin + EnvoyFilter
→ Pilot xDS push
→ Envoy 收到 LDS/EnvoyFilter
→ Envoy Wasm Filter 从 url 拉镜像
→ V8 / WAVM 加载 .wasm
→ 处理 HTTP 请求OCI image 拉取
Envoy 支持 oci:// / http:// / file:// 三种 URL:
- OCI:使用 manifest + blob,支持私有 registry(imagePullSecret)。
- HTTP:直接 GET .wasm 字节。
- file:本地路径,开发测试用。
plugins/wasm-go 目录
plugins/wasm-go/
pkg/ # 由 mcp 组成的内部库(关键是 wrapper)
extensions/ # ★ 56 个 官方插件
mcp-servers/ # 内建 MCP Server 实现
examples/ # 示例
Dockerfile{,Builder}
Makefile
wrapper 是 Higress 自研的"上层 SDK",在 proxy-wasm-go-sdk 之上提供易用 API。
proxy-wasm-go-sdk 基础
上游 github.com/tetratelabs/proxy-wasm-go-sdk 提供:
types.Context:插件根上下文。types.HttpContext:每个请求一个实例。proxywasm.*:ABI 包装函数(AddHttpRequestHeader 等)。
直接用它写插件是可以的,但样板代码多。Higress 在外面再封一层。
wrapper 抽象层
位于 plugins/wasm-go/pkg/wrapper/。提供:
- 统一回调接口(OnHttpRequestHeaders / Body / OnHttpResponseHeaders / Body)。
- 自动 JSON / gjson 配置解析。
- HttpCall / RedisCall 异步外呼封装。
- Log 工具。
SetCtx 注册回调
func main() {
wrapper.SetCtx(
"my-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
wrapper.ProcessRequestBody(onHttpRequestBody),
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
wrapper.ProcessResponseBody(onHttpResponseBody),
)
}
每个 Option 都是可选的,按需注册。wrapper 内部把它们桥接到 proxy-wasm 的 ABI 回调。
配置解析 ParseConfig
type MyConfig struct {
APIKey string `json:"apiKey"`
Limit int `json:"limit"`
}
func parseConfig(json gjson.Result, c *MyConfig, log wrapper.Log) error {
c.APIKey = json.Get("apiKey").String()
c.Limit = int(json.Get("limit").Int())
if c.APIKey == "" { return errors.New("apiKey required") }
return nil
}
失败时插件不加载,避免错配上线。
请求处理回调链
func onHttpRequestHeaders(ctx wrapper.HttpContext, c MyConfig, log wrapper.Log) types.Action {
if proxywasm.GetHttpRequestHeader("x-api-key") != c.APIKey {
proxywasm.SendHttpResponse(401, nil, []byte("unauthorized"), -1)
return types.ActionPause
}
return types.ActionContinue
}
返回 ActionContinue / ActionPause 控制是否继续处理。Pause 后必须主动 Resume 否则永远卡住。
HttpCall 异步外呼
client := wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "auth.svc.cluster.local", Host: "auth", Port: 80,
})
client.Get("/check?token=xxx", nil,
func(status int, headers http.Header, body []byte) {
if status != 200 { /* deny */ }
proxywasm.ResumeHttpRequest()
}, 5000)
return types.ActionPause
异步:返回 Pause,回调里 Resume。Envoy 走标准 Cluster 转发,享受 LB / 健康检查。
RedisCall
wrapper 也封装了 Redis:用于 ai-cache、rate-limit 这类需要分布式状态的插件。底层走 Envoy 的 redis_proxy filter cluster。
redis := wrapper.NewRedisClusterClient("redis-cache")
redis.Get("key", func(resp resp.Value) { ... })SharedData 共享状态
同一 Worker 内多个 HttpContext 通过 proxywasm.GetSharedData / SetSharedData 共享 KV(CAS 语义)。注意:跨 Worker 不共享,跨节点更不共享,需要 Redis 兜底。
TinyGo 编译
Go 标准编译器输出 .wasm 太大(带运行时),插件作者都用 TinyGo:
tinygo build -o my-plugin.wasm -scheduler=none -target=wasi ./main.go
-scheduler=none 让插件单线程,符合 proxy-wasm 假设。
hello-world 插件
plugins/wasm-go/extensions/hello-world/ 是最小完整示例。代码不到 40 行:
func main() { wrapper.SetCtx("hello-world", wrapper.ProcessRequestHeaders(headers)) }
func headers(ctx wrapper.HttpContext, c struct{}, log wrapper.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
return types.ActionContinue
}本地调试与单测
wrapper 提供了 HttpContext 的 mock:
func TestHeaders(t *testing.T) {
ctx := wrapper.NewMockContext()
ctx.SetReqHeader("x-api-key", "secret")
action := headers(ctx, cfg, log)
require.Equal(t, types.ActionContinue, action)
}
插件作者可以离线写 unit test,CI 跑 go test 即可。
发布到 OCI 仓库
make build-wasm PLUGIN=hello-world
docker build -t my/hello-world:v1 \
-f plugins/wasm-go/Dockerfile \
--build-arg PLUGIN=hello-world .
docker push my/hello-world:v1
然后在 WasmPlugin url: oci://my/hello-world:v1 即可。Higress 也提供 Dockerfile 把多个插件批量打成镜像。