Day 10 / 共 20 天 · 第 2 周 配置与匹配

OnPluginStart 生命周期

第 2 周收官。前面拆了配置解析和规则匹配,今天把它们串进插件"开机时序":OnPluginStart 从头到尾干了什么。这是插件从"被加载"到"就绪接客"的完整过程。

📍 你在 wasm-go 前 10 天的位置(W1 入门与生命周期 · W2 配置与匹配;W3-4 见 D11-20)
D01 全景· D02 request-block· D03 ABI· D04 三级Context· D05 SetCtx/选项 D06 配置解析· D07 RuleMatcher· D08 运行时匹配· D09 全局vs规则· D10 生命周期 W3-4 请求处理/高级
💡 第 2 周收官:把前几天全串成"芯片开机自检序列" 延续机场类比:OnPluginStart 就是安检芯片开班前的自检启动序列——按固定顺序:①先做点前置准备(prePluginStart)→②设好全局限流参数→③读今天的规章(配置)并认领工号(pluginID)→④把规章拆成规则表(ParseRuleConfig,D06-09 全在这里跑起来)→⑤挂上定时任务(tick),全部通过才亮绿灯"就绪接客"。任何一步失败就返回 Failed、拒绝上岗(保护网关不带着坏配置运行)。今天把散落 9 天的零件拼成一条启动流水线。
L01

OnPluginStart 全景

plugin_wrapper.go:637-727OnPluginStart(Plugin 级上下文的方法)是插件"开机"的总控。它返回 OnPluginStartStatusOK/Failed 告诉网关"启动成功没"。

什么时候被调用? 网关加载插件 wasm、创建 PluginContext 后,就调 OnPluginStart——每次配置变更(Higress 下发新配置)也会重新走一遍。它是"配置生效"的入口:读进配置、解析、注册定时任务、设参数。返回 Failed 会让插件启动失败(配置不合法时保护网关)。
OnPluginStart 开机自检序列(任一步 Fail 即拒绝上岗) ①prePluginStart前置准备 ②IO 限制全局参数 ③读配置+pluginID校验JSON/认工号 ④ParseRuleConfigD06-09 全跑起来 ⑤挂 tick定时任务 ✓ StatusOK:就绪接客
图注:五步顺序执行,全绿才返回 StatusOK。配置重载(Higress 下发新配置)会把这条序列整条重跑一遍。
📝 举个例子:一份带 _plugin_id_ 的配置进来会怎样 收到 {"_plugin_id_":"req-block-7","block_urls":["x"],"_rules_":[…]}
③ 校验是合法 JSON ✓ → 取出 _plugin_id_="req-block-7" 设为日志 ID(此后该实例日志都带这个 ID)→ 用 sjson 把 _plugin_id_ 从配置里删掉(不污染业务字段)→ ④ 剩下的 block_urls+_rules_ 交给 ParseRuleConfig 拆成"全局+规则表"。
L02

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
    }
}
读法:给插件一个"配置解析前先做点事"的口子(比如初始化外部连接、预热缓存)。失败就直接启动失败。这是生命周期第一步。
L03

IO 并发限制

:645-652:若配了 maxRequestsPerIoCycle(Day 05 的选项),调 setGlobalMaxRequestsPerIoCycle(Day 03 见过的 CallForeignFunction)。

这个限制为什么重要(Day 19 详讲) 当插件逻辑复杂、要做外部调用(HTTP/Redis)时,一个 IO 周期里挤太多并发请求,会导致外部调用的回调来不及在本周期处理、误判超时。限制每个 IO 周期的并发请求数能缓解。注意它是全局生效的(多插件设置时取最小值),所以是"启动时设一次"的全局参数。今天先知道它在启动流程里被设置,Day 19 讲为什么要它。
L04

读配置 + 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)。
L05

调 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 }
读法:这里把 Day 06(parseConfig)、Day 07-08(RuleMatcher)、Day 09(override)全串起来了:OnPluginStart 是把你注册的解析函数真正跑起来、生成规则表的地方。解析失败则插件启动失败。
L06

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 的倍数)。

tick 能干什么? 定时任务——比如每秒刷新一次令牌桶(限流)、每 5 秒上报一次指标、定期续租 Leader(Day 17)。你在 parseConfig 里 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 开头先清空旧的,再重新收集本次注册的。这正是它存在的意义:防止重载后定时任务越堆越多。

L07

完整时序图

1
网关加载 wasm → init()SetCtx 注册好 VM 上下文
2
NewPluginContext 造 Plugin 上下文
3
OnPluginStart:prePluginStart → 设 IO 限制 → 读配置+pluginID → ParseRuleConfig → 注册 tick
4
就绪。请求到来 → NewHttpContext 造请求上下文(按注册的钩子决定要不要读 body)
5
各阶段调你的钩子(GetMatchConfig 先选好 config);OnTick 后台跑定时任务
第 1 周(生命周期/Context)+ 第 2 周(配置/匹配)到此形成闭环。第 3 周进入"请求真正来了怎么处理"。
L08

今日小结 + 动手(第 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
下周预告 · 第 3 周HTTP 与外部调用——请求/响应各阶段钩子与 Action 语义、HttpContext 的丰富能力、Cluster 8 种集群抽象、以及最有挑战的 HttpCall/RedisCall 异步回调式外部调用。
← Day 09 Day 11 · HTTP 钩子 →