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

全局配置 vs 规则配置

昨天匹配出"用哪份配置"。今天看两份配置怎么关联:规则配置能"继承"全局配置再覆盖部分字段。还有一个生产级特性——规则级隔离:一条规则配错不拖垮其他规则。

📍 你在 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 请求处理/高级
💡 延续机场类比:继承+覆盖 = 公司报销制度 昨天(D08)已经会"给请求挑出该用哪份规则配置";今天看规则配置和全局配置的关系。它就是公司报销制度:公司定通用默认(超时 5s、重试 3 次 = 全局配置),部门可以只改自己关心的那几项(路由A 超时改 10s = 规则覆盖),没改的自动沿用公司默认。今天还有一个"生产级保命特性"——规则级隔离:一条规则配错了,别拖垮其他规则(一颗老鼠屎不坏一锅汤),还能读档回滚到上次能用的版本。
L01

两种解析方式

ParseRuleConfigrule_matcher.go:182)接受两个解析函数:

  • parsePluginConfig:独立解析一份配置(全局或规则各自解析)。
  • parseOverrideConfig:解析规则配置时能拿到全局配置做基础(继承+覆盖)。
两种模式对应两种心智 模式一(ParseConfig):全局和每条规则各自独立解析,互不相干。模式二(ParseOverrideConfig):先解析全局配置,规则配置以全局为"底",只覆盖它写了的字段。好比模式二是"继承默认设置再改几项",模式一是"每处都从头填"。大多数插件用模式一;需要"规则继承全局默认值"时用模式二。
L02

ParseOverrideConfig

plugin_wrapper.go:218-223

func ParseOverrideConfig[PluginConfig any](
    f ParseConfigFunc[PluginConfig],       // 解析全局配置
    g ParseRuleConfigFunc[PluginConfig],   // 解析规则配置(带 global 参数)
) CtxOption[PluginConfig] {
    return &parseOverrideConfigOption[PluginConfig]{parseConfigF: f, parseRuleConfigF: g}
}
// 规则解析函数签名(:65):
// func(json gjson.Result, global PluginConfig, config *PluginConfig) error
//                          ↑ 全局配置作为基础传进来
读法:规则解析函数 g 多了个 global PluginConfig 参数——这就是"继承"的入口。你在 g 里通常先 *config = global(拷贝全局作为默认),再按规则 JSON 覆盖需要改的字段。
L03

继承 + 覆盖(典型写法)

func parseGlobalConfig(json gjson.Result, config *MyConfig) error {
    config.timeout = json.Get("timeout").Int()   // 全局默认 timeout
    config.retries = json.Get("retries").Int()
    return nil
}
func parseRuleConfig(json gjson.Result, global MyConfig, config *MyConfig) error {
    *config = global                              // 1. 先继承全局
    if json.Get("timeout").Exists() {
        config.timeout = json.Get("timeout").Int() // 2. 只覆盖本规则写了的
    }
    return nil
}
为什么这么设计好? 你在全局设一次通用默认(超时 5s、重试 3 次),各规则只写"我要改的那项"(路由 A 超时改成 10s),其余字段自动继承全局。配置更 DRY(不重复),改全局默认能一处生效。这跟 CSS 的层叠、面向对象的继承是同一个思想——公共部分往上提,差异部分往下覆盖。
继承 + 覆盖:全局当底,规则只改差异项 全局(公司默认) timeout=5s retries=3 规则A(部门覆盖) timeout=10s ←改 (retries 没写) 规则A 最终生效 timeout=10s(覆盖) retries=3(继承) *config=global
图注:规则解析先 *config = global(继承全部默认),再按规则 JSON 覆盖写了的字段——没写的字段自动保留全局值。
📝 举个例子:合并结果一眼看清 全局 {"timeout":5,"retries":3} + 规则A {"_match_route_":["a"],"timeout":10} → 规则A 最终配置 = {timeout:10, retries:3}(timeout 被覆盖、retries 继承)。
若规则B 只写 {"_match_route_":["b"]} 不改任何字段 → 规则B 最终配置 = {timeout:5, retries:3}(完全等于全局)。
L04

全局配置先解析

rule_matcher.go:200-215ParseRuleConfig 先解析全局(_rules_ 外的顶层键),存进 m.globalConfig + 置 hasGlobalConfig=true,再解析每条规则(此时能把 globalConfig 传给 override 函数)。

读法:顺序很关键——必须先有全局配置,规则才能继承它。若全局解析失败会记 globalConfigError,但只要有成功的规则仍可继续(:210-214)。空配置(keyCount==0)则直接把整份配置当全局(:191-194),这对应"整个插件全局启用"的最简情况。
L05

规则级隔离

iface/context.go:32-35EnableRuleLevelConfigIsolation 开关,配合 ParseRuleConfigrule_matcher.go:255-292)的容错逻辑:

"一颗老鼠屎不坏一锅汤" 默认情况下,只要有一条规则配置解析失败,整个插件的配置更新就失败:280 直接 return err)——太脆弱。开启规则级隔离后:一条规则解析失败,只跳过这条,其他规则照常生效:257-278)。生产环境里,运维改错了某一条路由的插件配置,不至于让所有路由的这个插件都挂掉。这是把"故障爆炸半径"缩到最小的稳健设计。

👶 小白:既然隔离模式这么稳,为什么不默认开、非要插件自己 EnableRuleLevelConfigIsolation()

👨‍🏫 老师:因为"跳过坏规则继续跑"不总是好事。有些插件(比如强安全策略)宁可"配错就整体不生效、逼你立刻修",也不能"带着一条坏规则半生效"——那可能造成安全口子。所以 SDK 把选择权交给插件:要稳健就开隔离,要严格就不开。"按场景选合适的容错强度",比一刀切更成熟。

L06

从备份恢复

规则级隔离还有第二层保护(rule_matcher.go:258-273):一条规则解析失败时,尝试 loadRuleJsonFromBackup 加载上次成功的配置;成功解析的规则会 storeRuleToBackup 存进备份(:284-291)。备份用规则的 GenerateHashKey(Day 07)定位。

"回滚到上次能用的版本" 不仅"跳过坏规则",还能"用这条规则上次成功的配置顶上"。比如路由 A 的限流规则原本是 100 QPS(成功过、存了备份),这次运维改成非法值解析失败——隔离模式下,路由 A 继续用备份里的 100 QPS,而不是完全失去限流。备份存哪?用 SharedData(Day 17)跨 VM 共享。这是配置系统的"容灾"能力。
L07

容错策略对比

rule_matcher.go:298-305 收尾的两种模式对比:

// 隔离模式:全失败且无备份 → 报错;否则用成功的(含备份恢复的)
if isRuleLevelIsolation && len(successfulRules)==0 && hasAnyRuleParseError {
    return fmt.Errorf("all rules failed to parse and no previous successful config available")
// 非隔离模式:只要有一条失败就整体失败(前面已 return);这里是"一条都没解析出"
} else if !isRuleLevelIsolation && len(successfulRules)==0 {
    return fmt.Errorf("no valid rules parsed")
}
读法:非隔离=严格(一错全错,适合"配置必须完全正确"的场景);隔离=宽松(尽量多生效,适合"部分可用好过完全不可用"的生产场景)。由插件通过 EnableRuleLevelConfigIsolation() 自己选,SDK 两种都支持。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • ParseConfig 和 ParseOverrideConfig 的区别?
  • 规则配置怎么"继承+覆盖"全局配置?为什么好?
  • 规则级隔离解决什么问题?默认为什么脆弱?
  • "从备份恢复"怎么做?备份存哪?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '218,230p' pkg/wrapper/plugin_wrapper.go
sed -n '182,305p' pkg/matcher/rule_matcher.go
grep -n "loadRuleJsonFromBackup\|storeRuleToBackup" pkg/matcher/rule_matcher.go
明天预告 · Day 10(第 2 周收官)OnPluginStart 生命周期——把配置加载、pluginID 识别、tick 定时任务注册、IO 并发限制串成插件启动的完整时序。
← Day 08 Day 10 · 生命周期 →