第 08 期 / 共 10 期

WasmPlugin 与 wasm-go SDK

这是 Higress 扩展能力的核心载体。20 讲覆盖从 CRD 字段到 Envoy 沙箱加载,再到 wasm-go SDK 的所有关键点,让你能写出工程级 Wasm 插件。

L01

Wasm 在网关里干嘛

Wasm 让我们用任意语言写"挂载在 Envoy 上的中间件",并具备:

  • 沙箱隔离:插件崩溃不影响主进程。
  • 热更新:换 .wasm 镜像即生效,无需重启。
  • 跨语言:Go / Rust / C++ / AssemblyScript 都能产出 .wasm。
  • 性能:编译执行,比 Lua 快。
思考:Wasm 不是万能,它的局限?提示:内存上限、CPU 单线程、无 OS 系统调用。
L02

proxy-wasm ABI

proxy-wasm 是 Envoy / Istio 社区定义的标准 ABI:定义 Wasm 模块与 Host 通信的函数列表(如 proxy_on_request_headersproxy_send_http_response)。Higress 的 Envoy 与 plugins 都遵循它。

思考:proxy-wasm 与 WASI 关系?答:互补,proxy-wasm 偏代理域,WASI 偏 OS 抽象。
L03

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 }
思考:matchRules 是 Higress 扩展,相比 Istio 原生 selector 强在哪?答:按 domain+ingress 精细化覆盖。
L04

phase 与 priority

phase 枚举:AUTHN / AUTHZ / STATS / UNSPECIFIED。同 phase 内按 priority 降序执行。这决定了 Wasm filter 在 HTTP filter chain 的位置:AUTHN 最早,STATS 最晚。

思考:ai-cache 应该放 AUTHZ 之前还是之后?提示:先鉴权再查缓存最稳。
L05

matchRules 路由覆盖

同一插件可对不同 host / ingress 使用不同配置。IngressConfig 把每条 matchRule 渲染成单独的 EnvoyFilter patch route,让 Envoy 在每条 route 上 attach 不同 plugin config。

思考:matchRules 太多会爆 EnvoyFilter 数量吗?答:会,注意控制规模。
L06

加载链路全图

用户 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 请求
思考:镜像首次拉取慢会阻塞流量吗?答:可配置 fail_open / fail_close 模式。
L07

OCI image 拉取

Envoy 支持 oci:// / http:// / file:// 三种 URL:

  • OCI:使用 manifest + blob,支持私有 registry(imagePullSecret)。
  • HTTP:直接 GET .wasm 字节。
  • file:本地路径,开发测试用。
思考:Wasm 镜像比 Docker 镜像通常小几个数量级,原因?提示:纯字节码。
L08

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。

思考:为什么把 wrapper 独立?答:屏蔽 ABI 复杂度,让插件作者只关心业务。
L09

proxy-wasm-go-sdk 基础

上游 github.com/tetratelabs/proxy-wasm-go-sdk 提供:

  • types.Context:插件根上下文。
  • types.HttpContext:每个请求一个实例。
  • proxywasm.*:ABI 包装函数(AddHttpRequestHeader 等)。

直接用它写插件是可以的,但样板代码多。Higress 在外面再封一层。

思考:为什么 Wasm 插件每个请求要新建 HttpContext?答:隔离请求状态。
L10

wrapper 抽象层

位于 plugins/wasm-go/pkg/wrapper/。提供:

  • 统一回调接口(OnHttpRequestHeaders / Body / OnHttpResponseHeaders / Body)。
  • 自动 JSON / gjson 配置解析。
  • HttpCall / RedisCall 异步外呼封装。
  • Log 工具。
思考:抽象多了会损失什么?答:极限性能与灵活性,但绝大多数场景够用。
L11

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 回调。

思考:只注册 ResponseBody,请求阶段会被处理吗?答:不会,性能更优。
L12

配置解析 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
}

失败时插件不加载,避免错配上线。

思考:用 gjson 而不直接 json.Unmarshal 的原因?答:体积小、TinyGo 兼容好。
L13

请求处理回调链

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 否则永远卡住。

思考:什么时候用 Pause?答:等待异步结果(HttpCall)时。
L14

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 / 健康检查。

思考:Cluster 怎么来?答:DestinationRule 自动或自定义 EnvoyFilter 创建。
L15

RedisCall

wrapper 也封装了 Redis:用于 ai-cache、rate-limit 这类需要分布式状态的插件。底层走 Envoy 的 redis_proxy filter cluster。

redis := wrapper.NewRedisClusterClient("redis-cache")
redis.Get("key", func(resp resp.Value) { ... })
思考:直连 Redis vs 走 Envoy redis filter 的优劣?
L16

SharedData 共享状态

同一 Worker 内多个 HttpContext 通过 proxywasm.GetSharedData / SetSharedData 共享 KV(CAS 语义)。注意:跨 Worker 不共享,跨节点更不共享,需要 Redis 兜底。

思考:本地令牌桶用 SharedData 就够吗?答:单 Worker OK;多 Worker 计数会偏低。
L17

TinyGo 编译

Go 标准编译器输出 .wasm 太大(带运行时),插件作者都用 TinyGo

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

-scheduler=none 让插件单线程,符合 proxy-wasm 假设。

思考:TinyGo 不支持哪些 Go 特性?答:reflect 不全、cgo 受限、goroutine 受限。
L18

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
}
思考:把它跑起来你需要哪几步?答:build → 推 OCI → apply WasmPlugin → curl 验证。
L19

本地调试与单测

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 即可。

思考:单测覆盖到 HttpCall 吗?答:mock 客户端可注入。
L20

发布到 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 把多个插件批量打成镜像。

本期收尾:你已经能从零写、调、发一个 Higress Wasm 插件。下一期我们看官方 AI 网关插件家族。