Day 11 / 共 20 天 · 第 3 周 Wasm 插件

Wasm 插件系统概览

第 3 周讲 Higress 的插件生态——它最活跃、最"AI"的部分。昨天(Day 10)讲完配置与证书这些控制面收尾;今天转到数据面的扩展能力,建立全景:Wasm 插件 vs golang-filter 两套机制、和 Envoy proxy-wasm 的关系、SDK 已迁到独立 module。这是后面 Day 12-15 一切"写插件"的地基。

📍 你在第 3 周(Wasm 插件)的位置
D11 插件概览 D12 wasm-go SDK D13 写插件 key-auth D14 实战插件 D15 golang-filter
💡 先用一个类比兜住整周("网关 = 一栋大楼的门口"世界观) 把 Higress 网关想成一栋写字楼的大门:所有访客(HTTP 请求)都得从这里进出。插件就是你在门口临时加装的一道道安检关卡——查证件(鉴权)、限流量(限流)、翻包检查(改写/拦截)。今天要讲的核心是:Higress 提供两种方式装关卡——Wasm 插件是"可热插拔、装在防爆玻璃隔间里的标准关卡"(安全隔离、坏了不炸整栋楼),golang-filter是"直接焊进大楼结构的高性能特殊通道"(快、能干重活,但拆装要动主体)。记住这个类比,本周全通。
L01

两套扩展机制

🤔 痛点:网关的功能千变万化,总不能全塞进 Envoy 主体吧? 今天要鉴权、明天要限流、后天来个 AI Token 计费……如果每加一个功能都要改 Envoy 源码、重新编译、全量重启,那太痛苦了:迭代慢、风险高(一个 bug 崩掉整个网关)、还只能用 C++ 写。需要一种"不改主体、就能往门口加关卡"的机制。
💡 本质:插件 = 可插拔的安检关卡 Higress 的解法就是把功能做成插件——像大楼门口临时加装/拆除的安检关卡,不用动大楼本身。它给了两种关卡:Wasm(防爆隔间里的标准关卡,安全、跨语言、可热换)golang-filter(焊死的高性能特殊通道)。下面这张表就是这两种关卡的对照。
Envoy 网关这栋"大楼"上装关卡的两种方式 Envoy (大楼主体·C++) Wasm 插件(防爆隔间) .wasm · 沙箱隔离 · 跨语言 · 热换 崩了不影响大楼 · 绝大多数插件 golang-filter(焊进结构) .so · 原生 Go · 高性能 · 无隔离 能开常驻 goroutine(SSE 长连接) HTTP 请求 (访客)
图注:请求进大楼前先过关卡。Wasm 是隔间里的标准关卡(安全、可热换),golang-filter 是焊进结构的高性能通道(能维护长连接)。
Wasm 插件Golang Filter
目录plugins/wasm-go(+rust/cpp)plugins/golang-filter
运行编译成 .wasm,跑 proxy-wasm 沙箱编译成 .so,Envoy Go filter 动态加载
隔离沙箱隔离、跨语言、热更原生 Go、无沙箱、性能高、可用完整 Go 生态
用途绝大多数用户插件、AI 插件mcp-server/mcp-session(需常驻 goroutine 的 SSE)
两套机制的取舍 Wasm 插件(主力):编译成 .wasm,跑在 Envoy 的 proxy-wasm 沙箱里——安全隔离(插件崩了不影响 Envoy)、跨语言(Go/Rust/C++/JS 都能写)、热插拔(不重启换插件)。缺点:沙箱有性能开销、能力受 ABI 限制。golang-filter(特殊场景):编译成 .so,是 Envoy 原生 Go 扩展——无沙箱、性能高、能用完整 Go 生态(redis/gorm)、能开常驻 goroutine(维护 SSE 长连接)。缺点:无隔离、要重编 Envoy。绝大多数插件用 Wasm;只有 mcp-server 这种需要长连接的用 golang-filter。今天到 Day 14 讲 Wasm,Day 15 讲 golang-filter。
L02

SDK 已独立

一个必须知道的坑plugins/wasm-go/pkg/ 现在只剩 MCP Server SDK。核心插件框架(wrapper/log/matcher)已抽成独立 module github.com/higress-group/wasm-go,通过 go.mod 依赖引入。
所以看 SDK 源码要去 module 缓存:~/go/pkg/mod/github.com/higress-group/wasm-go@<版本>/pkg/,插件 import 路径是 github.com/higress-group/wasm-go/pkg/wrapper不是 仓库内路径)。
读法:写教程/看源码时,SDK 在 go env GOPATH/pkg/mod 下找,仓库 plugins/wasm-go/pkg 只剩 MCP SDK。每个插件 go.mod 锁不同 SDK 版本。

👶 小白:为什么要把核心 SDK 从主仓库搬出去、单独做一个 module?留在仓库里不是更好找吗?

👨‍🏫 老师:因为 SDK 和主仓库的发布节奏不一样。SDK 是"写插件的人"依赖的稳定 API,需要独立打版本号、独立升级;主仓库改得频繁。搬出去后,你的插件 go.mod 能锁定"我就用 wasm-go v1.x.x",主仓库怎么改都不影响你。坑就在这:你在仓库里翻 plugins/wasm-go/pkgwrapper 会扑空——它已经在 ~/go/pkg/mod/github.com/higress-group/wasm-go@版本/ 里了。

L03

proxy-wasm 绑定

底层是 github.com/higress-group/proxy-wasm-go-sdk(Higress fork 了 tetratelabs 版,加了 redis、route call、streaming body)。入口 SetVMContextentrypoint.go:27)把 VMContext 存入全局。

proxy-wasm-go-sdk = "Go 和 Envoy 的桥" 回想 Envoy 课 Day 17:Envoy 有 proxy-wasm ABI(宿主和插件的合同)。proxy-wasm-go-sdk 是这个 ABI 的 Go 语言绑定——它把 Envoy 的 proxy-wasm 回调(proxy_on_request_headers 等)和宿主函数(proxy_get_header_map_value 等)封装成 Go 能调的形式。用户几乎不直接碰它——都通过上层的 wrapper(Day 12)。这一层就是 Envoy 课学的 proxy-wasm 在 Go 侧的落地:Envoy 调 Go 函数、Go 调 Envoy 宿主函数。Higress fork 加了 redis 调用等增强 ABI(呼应 Envoy 课 Day 17 的 Higress 定制 host functions)。
L04

下行回调(Host→Wasm)

// proxywasm/internal/abi_callback_l7.go(//go:wasmexport,Envoy 调 Go)
proxy_on_request_headers  → proxyOnRequestHeaders(:23)
proxy_on_request_body(:37)/ proxy_on_response_headers(:63)/ proxy_on_response_body(:76)
proxy_on_http_call_response(:102,外呼响应回调)
proxy_on_redis_call_response(:133,Higress 加的 Redis 回调)
读法:"下行"= Envoy 调 Wasm。请求头来了 → Envoy 调 proxy_on_request_headers → Go 侧处理 → 返回 types.Action(Continue/Pause)。这些是 //go:wasmexport 导出给 Envoy 调的函数(对应 Envoy 课 Day 17 的"下行 filter 钩子")。
📝 举个例子:一个请求经过 Wasm 关卡时,"下行+上行"怎么配合 访客带着请求头 Authorization: Bearer xxx 走到关卡:
① Envoy 下行proxy_on_request_headers 通知插件"有请求头了";
② 插件想看这个头,就上行proxy_get_header_map_value("authorization") 读出 Bearer xxx
③ 插件判断非法,上行proxy_send_local_response(401) 当场拦下;
④ 插件返回 types.ActionPause 告诉 Envoy"别往上游转了"。
一进一出全靠"下行(Envoy 通知)+ 上行(插件操作 Envoy)"这两个方向的调用完成。
L05

上行宿主调用(Wasm→Host)

// proxywasm/internal/abi_hostcalls.go(//go:wasmimport,Go 调 Envoy)
proxy_log / proxy_send_local_response
proxy_get/add/replace/remove_header_map_value(读写 header)
proxy_get_buffer_bytes(读 body)/ proxy_http_call(外呼)
proxy_redis_call(Higress)/ proxy_define_metric / proxy_increment_metric
读法:"上行"= Wasm 调 Envoy。插件想读 header/发外部请求/加指标,就调这些 //go:wasmimport 的宿主函数(对应 Envoy 课 Day 17 的"上行 host 函数")。下行(Envoy 通知插件)+ 上行(插件操作 Envoy)= 插件的全部能力边界。
🧠 一句话口诀记住方向 "下行是 Envoy 喊你(export 给它调),上行是你喊 Envoy(import 它的函数)。" 换个角度://go:wasmexport = 我导出、别人(Envoy)用;//go:wasmimport = 我导入、我去用别人(Envoy)的。插件能做的一切,无非"被通知"和"去操作"这两下——记住这句,看任何插件代码都不会迷路。
L06

连回 Envoy 课

三站在 Wasm 这里完整贯通
Envoy 课 Day 17:讲了 Envoy 的 proxy-wasm ABI、Context 双向翻译、VM 生命周期——那是 C++ 宿主侧。
Istio 课 Day 17:讲了 Higress 用 EnvoyFilter 的 WasmPhase/WasmPriority 把 Wasm 插件插进 Envoy 过滤器链。
Higress 课(本周):讲用 wasm-go SDK 插件(Go 侧),编译成 .wasm,由 Higress 控制面下发、Envoy 加载执行。
完整链路:wasm-go 写插件(Higress)→ 编译成 .wasm → WasmPlugin CRD 配置 → istiod 生成 EnvoyFilter 插入过滤器链(Istio)→ Envoy 的 Wasm runtime 加载执行(Envoy)。你三站都学了,就打通了整个 Wasm 插件生态的技术底座。
L07

配置下发闭环

WasmPlugin CRD → Higress 控制面 convertIstioWasmPluginingress_config.go:1123)转成标准 Istio WasmPlugin(含 Url 拉 .wasm、Sha256PluginConfig)→ 经 MCP 喂 istiod → Envoy 从 URL 拉 .wasm 加载。

读法:插件的 .wasm 文件放在 OCI/HTTP,WasmPlugin CRD 声明 URL + 配置。控制面翻译成 Istio WasmPlugin(Day 04-05 的翻译流程),Envoy 按 URL 拉取 .wasm 加载。Day 12-14 会看插件配置里的 _rules_ 怎么和 SDK 的 RuleMatcher 闭环(控制面写、数据面读)。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • Wasm 插件 vs golang-filter 两套机制的取舍?
  • SDK 现在在哪(独立 module)?import 路径?
  • proxy-wasm-go-sdk 是什么桥?
  • 下行回调 vs 上行宿主调用各是什么方向?
  • Wasm 插件怎么连接 Envoy 课和 Istio 课?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress
ls plugins/
ls plugins/wasm-go/pkg/  # 只剩 mcp
cat plugins/wasm-go/extensions/hello-world/go.mod | grep wasm-go
明天预告 · Day 12wasm-go SDK——用户真正写插件的 API:SetCtx + Option 模式、三级 Context、配置解析、请求生命周期。
← Day 10 配置+证书 Day 12 · wasm-go SDK →