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

运行时匹配 GetMatchConfig

昨天配置被拆成一条条规则。今天看请求真正来时,GetMatchConfig 怎么用当前请求的域名/路由/服务,从这些规则里挑出"该用哪份配置"。这是控制面配置在数据面生效的最后一环。

📍 你在 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 请求处理/高级
💡 延续机场类比:门卫拿着请求去"对号入座"查规章 昨天(D07)那本"分区规章"已经拆成一条条规则了;今天来了个真旅客,门卫要按他的"航班信息"(域名 :authority / 路由 route_name / 后端服务 cluster_name)从上往下翻规章,命中第一条就照它办——这正是"门卫查访客名单"。翻到底都没命中,就走"默认通道"(全局配置)。关键记住两点:① 匹配对插件作者透明——你的钩子直接拿到已选好的 config;② 顺序即优先级,越靠前越先命中。
L01

匹配发生在哪

还记得 Day 04 说 CommonPluginCtx 内嵌了 RuleMatcher。当请求进入某个处理钩子前,SDK 会调 GetMatchConfig() 选出当前请求该用的配置,再把它作为 config 参数传给你的钩子。

你的钩子拿到的 config 是"已经匹配好的" 你写 onHttpRequestHeaders(ctx, config) 时,那个 config 已经是"针对当前请求匹配出来的正确配置"了——你不用自己判断"这个请求该用哪条规则"。SDK 在调你之前就 GetMatchConfig 选好了。这就是 RuleMatcher 内嵌在 PluginCtx 的意义:把匹配透明化,插件作者只管写业务。
L02

三个运行时属性

GetMatchConfigrule_matcher.go:122-140)开头取当前请求的三个关键信息:

host, _ := proxywasm.GetHttpRequestHeader(":authority")       // 域名
routeName, _ := proxywasm.GetProperty([]string{"route_name"}) // Envoy 匹配的路由名
serviceName, _ := proxywasm.GetProperty([]string{"cluster_name"}) // 后端集群名
这三个值从哪来? :authority 是 HTTP/2 的域名伪头(就是 Host);route_namecluster_name 是 Envoy 处理这个请求时算出来的"属性"(回想 Envoy/Istio 课:Envoy 先做路由匹配,选出 route 和 cluster)。wasm-go 通过 GetProperty 从 Envoy 拿这些运行时信息——插件站在 Envoy 已经算好的结果上做二次匹配。这就是为什么 category 有 Route/Service——它们对应 Envoy 的 route/cluster。
L03

逐条规则匹配

rule_matcher.go:140-176:遍历所有规则,按 category 判断是否命中:

for _, rule := range m.ruleConfig {
    if rule.category == Host && m.hostMatch(rule, host) {
        return &rule.config, nil
    }
    if rule.category == Route {
        if _, ok := rule.routes[string(routeName)]; ok { return &rule.config, nil }
    }
    if rule.category == Service && m.serviceMatch(rule, string(serviceName)) {
        return &rule.config, nil
    }
    if rule.category == RouteAndService {
        if _, ok := rule.routes[string(routeName)]; ok && m.serviceMatch(rule, string(serviceName)) {
            return &rule.config, nil
        }
    }
    if rule.category == RoutePrefix {
        for routePrefix := range rule.routePrefixs {
            if strings.HasPrefix(string(routeName), routePrefix) { return &rule.config, nil }
        }
    }
}
读法:Route 用 map 查(O(1)),Service/Host 调专用匹配(要处理通配),RoutePrefix 遍历前缀做 HasPrefix,RouteAndService 要两个条件都满足。命中第一条就返回它的 config——所以规则顺序 = 优先级(L06)。
🧍 第一人称请求之旅:现在你就是那个请求 你是一个请求,头上贴着标签::authority=api.ex.comroute_name=route-acluster_name=svc-x。你走到门卫台,门卫翻开规章从第一条开始比对你的标签——命中就当场按那条规矩放你走,谁也不再往下翻。下面这张表就是门卫翻规章的过程。
GetMatchConfig:从上往下翻,命中即返回 请求标签 :authority=api.ex.com route_name=route-a cluster_name=svc-x 规则#1 Host=*.other.com → 不中 规则#2 Route={route-a} → 命中! 规则#3 …(不再检查) 兜底 globalConfig(本次用不到) 返回 #2 的 config
图注:命中规则#2 后立刻返回,#3 和兜底都不再看。若#1~#3 全不中,才落到 globalConfig。
📝 单步走查:同样 3 条规则,换不同请求会命中谁
请求(route_name / authority)规则#1 Host=*.ex.com规则#2 Route={route-a}最终 config
route-a / api.ex.com先检查:命中—(已返回)规则#1
route-a / api.other.com不中命中规则#2
route-z / api.other.com不中不中globalConfig 兜底
看第一行:规则#1 排在前面,即便请求也满足#2,也是#1 先命中——再次印证"顺序即优先级"。
L04

Host 匹配实现

hostMatch 按昨天的三种 MatchType 处理通配:

  • Exact:域名完全相等。
  • Prefix*.example.com):请求域名以 .example.com 结尾。
  • Suffixexample.*):请求域名以 example. 开头。
读法:通配符 * 的位置决定匹配方式。serviceMatch 同理处理服务名(Higress 的服务名带命名空间/端口后缀,可能需要前缀匹配)。这些细节 SDK 都封好了,你配 *.example.com 就自动是前缀通配。
L05

兜底全局配置

rule_matcher.go:176-180:所有规则都没命中时——

    if m.hasGlobalConfig {
        return &m.globalConfig, nil   // 用全局配置兜底
    }
    return nil, nil                    // 没有全局配置,返回 nil
}
全局配置 = 默认规则 规则匹配是"特殊情况特殊处理",没命中任何规则的请求,就用全局配置(那些写在 _rules_ 外面的顶层键)。好比"VIP 走 VIP 通道,其余走普通通道"。如果连全局配置都没有,返回 nil——你的钩子拿到 nil config 要小心处理(通常意味着这个请求不该被这个插件处理)。

👶 小白:既然可能返回 nil,我的钩子里要不要每次都判空?

👨‍🏫 老师:要养成这个意识,但多数情况下你配了全局配置就不会为 nil。真正会 nil 的场景是"只写了 _rules_、没写任何全局键,且当前请求一条规则都不匹配"——SDK 会认为"这个插件不该管这个请求"。写关键插件时对 nil 做防御(比如直接 ActionContinue 放行)比裸用更稳妥。

L06

匹配顺序即优先级

"命中第一条即返回"的含义 GetMatchConfig从上到下遍历规则,命中第一条就返回。所以规则在配置里的顺序 = 优先级:越靠前越优先。控制面(Console/Higress)生成配置时会按作用域优先级(SERVICE > ROUTE > DOMAIN,回想上一站 Day 09)排好序,让更具体的规则排在前面。这样"路由级配置覆盖域名级配置"的语义就自然实现了——具体的先命中。
L07

钩子里怎么用(透明)

对插件作者,这一切是透明的。你只需:

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
    // config 已经是匹配出来的正确配置,直接用
    if config.enabled { /* ... */ }
    return types.ActionContinue
}
读法:SDK 在调这个钩子前已经跑完 GetMatchConfig,把结果作为 config 传进来。你从不直接调 GetMatchConfig——它是 SDK 的内部机制。这正是好 SDK 的标志:复杂的匹配藏在背后,作者只见到"当前该用的配置"。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 匹配何时发生?钩子拿到的 config 是原始的还是匹配好的?
  • 三个运行时属性(:authority/route_name/cluster_name)从哪来?
  • 为什么"命中第一条即返回"意味着顺序=优先级?
  • 没命中任何规则时怎么办?config 可能是 nil 吗?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '122,180p' pkg/matcher/rule_matcher.go
grep -n "hostMatch\|serviceMatch" pkg/matcher/rule_matcher.go pkg/matcher/utils.go
明天预告 · Day 09全局配置 vs 规则配置——ParseOverrideConfig 怎么让规则配置"继承+覆盖"全局配置,以及"规则级隔离"容错机制(一条规则解析失败不影响其他,还能从备份恢复)。
← Day 07 Day 09 · 全局vs规则 →