Day 12 / 共 20 天 · 第 3 周 Wasm 插件
wasm-go SDK
昨天(Day 11)知道了"插件是门口的安检关卡、SDK 已独立成 module";今天正式打开这套 SDK——用户真正写插件用的是 wasm-go SDK 的 wrapper 层(<wasm-go>/pkg/wrapper/)。看它的核心:SetCtx + Option 模式、三级 Context、配置解析、请求生命周期。学完这天,你就能读懂 Day 13 的真实插件。
📍 你在第 3 周(Wasm 插件)的位置
D11 插件概览→
D12 wasm-go SDK→
D13 写插件 key-auth→
D14 实战插件→
D15 golang-filter
💡 先兜住今天(延续"关卡"世界观)
如果说 Day 11 讲的是"门口能装关卡",今天讲的 SDK 就是发给每个关卡保安的"岗位说明书 + 标准装备"。
SetCtx = 保安上岗登记(报上工号"插件名"、勾选自己负责哪些环节);三级 Context = 保安干活的三种"作用域":整个安保公司在岗期间(VM 级)、这一版排班表期间(插件级)、接待某一位访客期间(请求级);SDK 还替你干了脏活(规则匹配、body 缓冲、崩溃兜底),你只管写"遇到访客怎么办"。L01
最小骨架
// extensions/hello-world/main.go
func main() {} // 空!
func init() {
wrapper.SetCtx("hello-world", wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders))
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config HelloWorldConfig, log log.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
proxywasm.SendHttpResponseWithDetail(200, "hello-world", nil, []byte("hello world"), -1)
return types.ActionContinue
}
🤔 痛点:新手看到"空的 main() + 全在 init()"会懵——程序入口不是 main 吗?
写惯了普通程序的人,第一反应是"逻辑应该写在
main() 里"。可插件的 main() 是空的,一切都塞在 init() 里,这看着很反直觉。到底谁在调这些函数?💡 本质:插件不是"主动跑",而是"被事件驱动"
插件像门口的保安——他不会自己"从头跑到尾",而是坐在岗位上等事件:来了请求头就被叫一次、来了 body 再被叫一次。所以没有"主流程"(main 空),只有"上岗登记 + 各环节回调"(init 里
SetCtx 注册)。Wasm 模块加载时 Go runtime 自动跑 init(),于是登记就完成了。📝 举个例子:hello-world 关卡上岗后发生了什么
客户端
curl http://gateway/ → 请求头到达关卡 → Envoy 下行调 onHttpRequestHeaders → 插件加了个头 hello: world,并直接 SendHttpResponse(200, "hello world") → 客户端收到 hello world(请求根本没往上游转,被关卡当场"截胡"回复了)。这就是最小插件的完整一生。为什么 main 是空的?
Wasm 模块加载时,Go runtime 会执行
init()。插件的逻辑全在 init() 里通过 SetCtx 注册——注册"插件名 + 各阶段回调"。main() 空着是因为没有"主流程",一切都是事件驱动(Envoy 调回调)。这个骨架就是所有 Higress Wasm 插件的模板:init 里 SetCtx 注册回调,回调函数里写业务逻辑(读写 header/body、外呼、返回响应)。onHttpRequestHeaders 收到请求头时被调用,加个 header、直接返回响应。L02
SetCtx + Option
// plugin_wrapper.go SetCtx[PluginConfig any](pluginName, options ...CtxOption)(:114)
// → proxywasm.SetVMContext(NewCommonVmCtx(...))
// Option 注册各阶段回调(有新旧两套 API):
// ProcessRequestHeaders/By(:246/:242)、ProcessRequestBody(:270)
// ProcessStreamingRequestBody(:294)、ProcessResponseHeaders(:318)
// ParseConfig(:158)、ParseOverrideConfig(:212 全局+规则两个函数)
读法:
SetCtx 用泛型 PluginConfig 承载"插件配置结构体",用 Option 注册各阶段回调。你想处理哪个阶段(请求头/请求体/响应头/流式响应体),就注册对应 Option。ParseConfig 注册"怎么解析配置"。L03
三级 Context
对应 proxy-wasm 三级上下文:
CommonVmCtx[PluginConfig](:73):VM 级,持全部回调指针。CommonPluginCtx[PluginConfig](:462):插件级,含RuleMatcher、配置解析、OnPluginStart、leader 选举。CommonHttpCtx[PluginConfig](:653):每请求级,持config、userContext、body 缓冲控制。
图注:像三层套娃/三种班次——安保公司在岗期间(VM)套着这版排班表(插件),再套着接待某位访客(请求)。里层随请求生灭,外层长存。
三级 Context = 三个生命周期
VM 级(最长)——整个 Wasm 实例活着期间存在,持有插件的全局定义。插件级(中)——一次插件配置加载期间,管配置和规则匹配。请求级(最短)——每个 HTTP 请求一个,管这个请求的状态。回想 Envoy 课 Day 17 的 proxy-wasm 三级上下文——SDK 把它们封装成三个 Go 结构。你写插件时主要在"请求级 Context"(
HttpContext)里操作——它是每个请求独立的,跨阶段传值用 SetContext/GetContext。L04
配置解析
// plugin_wrapper.go OnPluginStart(:561)
// proxywasm.GetPluginConfiguration() 读配置(:569)→ 校验 JSON(:581)
// 抽出内部 _plugin_id_(:585)→ ctx.ParseRuleConfig(...)(:598,交给 RuleMatcher)
// 注册 OnTick(SetTickPeriodMilliSeconds(100),:611)
读法:插件启动时
OnPluginStart 读配置(Envoy 通过 proxy_on_configure 传进来的,源头是 WasmPlugin CRD,Day 11)、校验、交给 RuleMatcher 解析(L06)。你的 ParseConfig 回调在这里被调用,把 JSON 反序列化成你的配置结构体。L05
请求生命周期
// plugin_wrapper.go(wrapper 把 proxy-wasm 回调翻译成用户回调)
// OnHttpRequestHeaders(:856):先 GetMatchConfig() 做规则匹配(:861)→ 调用户回调(:881)
// OnHttpRequestBody(:884):区分流式 vs 整体缓冲(未结束返回 ActionPause)
// OnHttpResponseHeaders(:918)/ OnHttpResponseBody(:948)
// 每个回调 defer recoverFunc() 捕获 panic(:1061,避免整个 VM 崩)
SDK 帮你做了"脏活"
你只写"收到请求头怎么办"(
onHttpRequestHeaders),SDK 在背后帮你:①GetMatchConfig 算出"这个请求该用哪份配置"(规则匹配,L06);②区分流式 body vs 整体 body(整体的要缓冲到 endOfStream 才回调,未结束返回 ActionPause 让 Envoy 等);③用 recover 捕获你代码里的 panic(防止一个插件 bug 崩掉整个 Wasm VM)。这些复杂性 SDK 都封装了——你专注业务。回调返回 types.Action(Continue/Pause)控制 Envoy 是否继续(呼应 Envoy 课 Day 08 的 FilterStatus)。拿"一个带 body 的 POST 请求"单步走一遍,看清 SDK 在每一步替你做了什么:
| 步骤 | Envoy 事件 | SDK 背后做的"脏活" | 调你的回调吗?返回什么 |
|---|---|---|---|
| 1 | 请求头到达 | GetMatchConfig 算出该用哪份配置(规则匹配) | 调 onHttpRequestHeaders → 返回 Continue |
| 2 | body 第 1 块到达 | 整体模式:缓冲,尚未收齐 | 不调你 → 返回 ActionPause(让 Envoy 等) |
| 3 | body 最后一块(endOfStream) | body 收齐,拼成完整 buffer | 调 onHttpRequestBody → 返回 Continue |
| 4 | 你的代码里 panic 了 | defer recoverFunc() 捕获 | 不崩 VM,本请求安全兜底 |
L06
RuleMatcher(配置匹配)
// matcher/rule_matcher.go
// Category(:33):Route / Host / Service / RoutePrefix / RouteAndService
// 约定字段(:49):_rules_、_match_route_、_match_domain_、_match_service_
// ParseRuleConfig(:182):空配置=全局生效;有 _rules_ 则逐条解析
// GetMatchConfig(:127):运行时读 :authority/route_name/cluster_name,按类别匹配
一份插件,不同 route/domain/service 用不同配置
RuleMatcher 是"一份插件如何按路由/域名/服务生效不同配置"的核心。比如同一个限流插件,对
/api 路由限 100 QPS、对 /admin 限 10 QPS。配置里用 _rules_ 数组 + _match_route_/_match_domain_ 表达这些规则。运行时 GetMatchConfig 读请求的域名/路由/集群,匹配到对应规则的配置。关键闭环:控制面 convertIstioWasmPlugin(Day 04)写 _rules_ 结构,数据面 SDK 的 RuleMatcher 读 _rules_——控制面写、数据面读,闭环。这就是 Higress WasmPlugin 的"多级配置"能力。L07
外呼与协调
- HTTP 外呼(
http_wrapper.go):HttpClient/ClusterClient,底层DispatchHttpCall——AI 插件调 LLM/鉴权服务的基础。 - Redis(
redis_wrapper.go):RedisClient(Eval/Get/Incr),走proxy_redis_call(限流用)。 - leader 选举(
:478 DoLeaderElection):多 Envoy worker 用 SharedData + CAS 选一个干活(限流计数、定时上报)。
读法:插件不只处理请求,还能主动外呼(调 LLM/鉴权)、访问 Redis(分布式限流)、做 leader 选举(多 worker 协调)。这些能力让 Wasm 插件能实现复杂的 AI 网关逻辑(Day 16-17 的 ai-proxy/限流就靠这些)。外呼是异步的——发出后在回调里处理响应(Envoy 事件驱动)。
L08
今日小结 + 动手
🧠 今天你应该能回答
- 为什么插件的 main 是空的?逻辑在哪注册?
- SetCtx + Option 模式怎么注册回调?
- 三级 Context 对应哪三个生命周期?
- 请求生命周期里 SDK 帮你做了哪些"脏活"?
- RuleMatcher 怎么实现"多级配置"?控制面/数据面怎么闭环?
✋ 动手
# SDK 在 module 缓存,先下载
cd /Users/bitmart/work/codes/github/higress-group/higress/plugins/wasm-go/extensions/hello-world
cat main.go
go env GOPATH # SDK 在 $GOPATH/pkg/mod/github.com/higress-group/wasm-go@*/pkg/wrapper/
明天预告 · Day 13:写一个插件——把 SDK 用起来:逐行拆解一个插件的 HttpContext/OnHttpRequestHeaders,理解从配置到处理的完整流程。