OnPluginStart 生命周期
第 2 周收官。前面拆了配置解析和规则匹配,今天把它们串进插件"开机时序":OnPluginStart 从头到尾干了什么。这是插件从"被加载"到"就绪接客"的完整过程。
OnPluginStart 就是安检芯片开班前的自检启动序列——按固定顺序:①先做点前置准备(prePluginStart)→②设好全局限流参数→③读今天的规章(配置)并认领工号(pluginID)→④把规章拆成规则表(ParseRuleConfig,D06-09 全在这里跑起来)→⑤挂上定时任务(tick),全部通过才亮绿灯"就绪接客"。任何一步失败就返回 Failed、拒绝上岗(保护网关不带着坏配置运行)。今天把散落 9 天的零件拼成一条启动流水线。OnPluginStart 全景
plugin_wrapper.go:637-727 的 OnPluginStart(Plugin 级上下文的方法)是插件"开机"的总控。它返回 OnPluginStartStatusOK/Failed 告诉网关"启动成功没"。
OnPluginStart——每次配置变更(Higress 下发新配置)也会重新走一遍。它是"配置生效"的入口:读进配置、解析、注册定时任务、设参数。返回 Failed 会让插件启动失败(配置不合法时保护网关)。{"_plugin_id_":"req-block-7","block_urls":["x"],"_rules_":[…]}:③ 校验是合法 JSON ✓ → 取出
_plugin_id_="req-block-7" 设为日志 ID(此后该实例日志都带这个 ID)→ 用 sjson 把 _plugin_id_ 从配置里删掉(不污染业务字段)→ ④ 剩下的 block_urls+_rules_ 交给 ParseRuleConfig 拆成"全局+规则表"。prePluginStart 钩子
:638-643:先执行用户注册的前置钩子(PrePluginStartOrReload 选项,Day 05):
if ctx.vm.prePluginStartOrReload != nil {
err := ctx.vm.prePluginStartOrReload(ctx)
if err != nil {
log.Errorf("prePluginStartOrReload hook failed: %v", err)
return types.OnPluginStartStatusFailed
}
}
IO 并发限制
:645-652:若配了 maxRequestsPerIoCycle(Day 05 的选项),调 setGlobalMaxRequestsPerIoCycle(Day 03 见过的 CallForeignFunction)。
读配置 + pluginID
:653-680:读取并预处理配置:
data, err := proxywasm.GetPluginConfiguration() // 拿到配置字节
globalOnTickFuncs = nil // 重置 tick(重载时清旧的)
// 校验 JSON 合法性
if !gjson.ValidBytes(data) { return Failed }
// 提取 _plugin_id_ 用于日志标识,然后从配置里删掉
pluginID := gjson.GetBytes(data, PluginIDKey).String()
if pluginID != "" {
ctx.vm.log.ResetID(pluginID)
data, _ = sjson.DeleteBytes([]byte(data), PluginIDKey)
}
jsonData = gjson.ParseBytes(data)
_plugin_id_ 是控制面注入的插件实例标识——提取出来设为日志 ID,这样这个实例的所有日志都带同一个 ID 好排查,再从配置里删掉(不污染业务配置)。globalOnTickFuncs = nil 很关键:配置重载时先清空旧的定时任务,避免重复注册。用 sjson 删字段(对应 Day 01 说的 sjson 改 JSON)。调 ParseRuleConfig
:700-716:把配置交给昨天讲的 ParseRuleConfig(RuleMatcher 的方法),拆全局 + 规则:
err = ctx.ParseRuleConfig(ctx, jsonData,
func(js gjson.Result, cfg *PluginConfig) error { // 全局解析
return ctx.vm.parseConfig(ctx, []byte(js.Raw), cfg)
},
parseOverrideConfig, // 规则解析(可能为 nil)
)
if err != nil { return types.OnPluginStartStatusFailed }
tick 定时任务
:717-725:若解析阶段注册了 tick 函数(RegisterTickFunc,:116-118),设置 100ms 的 tick 周期:
if globalOnTickFuncs != nil {
ctx.onTickFuncs = globalOnTickFuncs
proxywasm.SetTickPeriodMilliSeconds(100) // 每 100ms 触发 OnTick
}
OnTick(:730-737)遍历注册的 tick 函数,到点就执行(各自的 tickPeriod 必须是 100 的倍数)。
wrapper.RegisterTickFunc(1000, func(){...}) 注册"每秒执行"。底层 tick 固定 100ms 一跳,SDK 按你设的周期判断该不该执行你的函数——所以周期必须是 100 的倍数。这是插件里做"周期性后台工作"的唯一途径(Wasm 不能自己开线程)。👶 小白:我 RegisterTickFunc(1500, fn) 想每 1.5 秒执行一次,行不行?
👨🏫 老师:行——1500 是 100 的倍数。底层每 100ms 跳一次表(OnTick),SDK 拿"当前时间 − 上次执行时间 ≥ 你设的周期"来判断该不该跑你的函数。所以周期必须是 100 的倍数;写 1550 就对不齐 100ms 的跳点、不精确。想要精准就用 100 的整数倍。
👶 小白:配置重载时,上次注册的 tick 会不会残留、变成注册了两份?
👨🏫 老师:不会。看 L04 那句 globalOnTickFuncs = nil——每次 OnPluginStart 开头先清空旧的,再重新收集本次注册的。这正是它存在的意义:防止重载后定时任务越堆越多。
完整时序图
init() 里 SetCtx 注册好 VM 上下文NewPluginContext 造 Plugin 上下文OnPluginStart:prePluginStart → 设 IO 限制 → 读配置+pluginID → ParseRuleConfig → 注册 tickNewHttpContext 造请求上下文(按注册的钩子决定要不要读 body)GetMatchConfig 先选好 config);OnTick 后台跑定时任务今日小结 + 动手(第 2 周收官)
🧠 第 2 周你应该能回答
- OnPluginStart 的几个步骤(前置钩子→IO限制→读配置→解析→tick)?
_plugin_id_为什么提取出来又删掉?- 配置重载时为什么先
globalOnTickFuncs = nil? - tick 定时任务能干什么?为什么周期必须是 100 的倍数?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '637,737p' pkg/wrapper/plugin_wrapper.go
sed -n '103,118p' pkg/wrapper/plugin_wrapper.go # RegisterTickFunc