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

RuleMatcher 规则匹配

同一个插件,能对不同路由/域名/服务用不同配置——这靠 _rules_ 机制。今天拆 rule_matcher.go:配置怎么写、怎么被拆成一条条带匹配条件的规则。这是 wasm-go 最有特色的能力。

📍 你在 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 请求处理/高级
💡 延续机场类比:一台芯片,按航班分区执行不同规矩 昨天(D06)芯片学会了"读一份规章";今天它要读一本"分区规章"——同一台安检芯片,对不同航班/区域(路由/域名/服务)执行不同松紧。_rules_ 就是这本分区规章:顶层是"默认规矩"(全局配置),_rules_ 里每条是"某区专属规矩"(匹配条件 + 该区配置)。这跟"公司报销制度=公司默认规则+部门可覆盖"一模一样。今天先看规章怎么被拆成一条条规则,明天(D08)看请求来了怎么对号入座。
L01

为什么要规则匹配

🤔 痛点:路由 A 限 100 QPS、路由 B 限 50、其余限 10,你会怎么办? 最笨的办法是给每个路由各装一份限流插件、各配一个数——插件实例爆炸、配置到处散、改一处要动一堆。就像给机场每个登机口单独派一支安检队、各带一本规章,人力全浪费在重复上。
一个插件,多套配置 想象一个限流插件:路由 A 限 100 QPS,路由 B 限 50 QPS,其余路由限 10 QPS。如果每个路由都装一份插件太浪费。更好的做法:装一份插件,配置里写多条规则——"匹配路由 A 用配置 X、匹配路由 B 用配置 Y、其余用全局配置"。请求来了先匹配用哪条规则的配置。RuleMatcher 就干这个:把"按位置区分配置"的能力做进 SDK,所有插件白拿。
读法:回想上一站 Higress/Console:插件实例有 GLOBAL/DOMAIN/ROUTE/SERVICE 四作用域——那些作用域最终就编码成这里的 _rules_ 配置,由 wasm-go 的 RuleMatcher 在数据面解析匹配。控制面写、数据面读的闭环。
L02

_rules_ 配置格式

常量在 rule_matcher.go:46-52_rules__match_route__match_domain__match_service__match_route_prefix_。配置长这样:

# 顶层键(除 _rules_)= 全局配置
block_urls: ["/global"]
# _rules_ = 规则数组
_rules_:
- _match_route_: ["route-a"]     # 匹配路由 route-a
  block_urls: ["/a-only"]
- _match_domain_: ["*.example.com"]  # 匹配域名
  block_urls: ["/example-only"]
读法:配置里 _rules_ 之外的键构成"全局配置",_rules_ 里每一项是一条规则(含匹配条件 + 该规则专属配置)。这套约定由 wasm-go 定义,控制面(Higress/Console)按它生成配置。
一份配置 → 拆成"全局默认" + 若干"分区规则" 整份 JSON 规章 block_urls:[/global] _rules_: - _match_route_:[route-a] - _match_domain_:[*.ex.com] ParseRuleConfig globalConfig(默认规矩:/global) RuleConfig #1 category=Route routes={route-a} · config:/a-only RuleConfig #2 category=Host hosts={*.ex.com} · config:/example-only
图注:顶层键沉淀为 globalConfig_rules_ 里每项变成一条带 category 和匹配集合的 RuleConfig。明天请求来了就在这几条里挑一条。
L03

五种 Category

rule_matcher.go:30-37

const (
    Route Category = iota  // 按路由名匹配(_match_route_)
    Host                   // 按域名匹配(_match_domain_)
    Service                // 按服务名匹配(_match_service_)
    RoutePrefix            // 按路由名前缀匹配(_match_route_prefix_)
    RouteAndService        // 路由 + 服务都匹配
)
五种匹配维度 Route(精确路由名)、Host(域名)、Service(后端服务名)、RoutePrefix(路由名前缀)、RouteAndService(同时满足路由和服务)。不同插件按需选:入口鉴权常按域名/路由,AI 相关常按服务(对应某个 LLM upstream)。一条规则的 category 由它写了哪种 _match_xxx_ 自动推断(L06)。
L04

RuleConfig 结构

rule_matcher.go:57-64

type RuleConfig[PluginConfig any] struct {
    category     Category
    routes       map[string]struct{}    // 匹配的路由名集合
    services     map[string]struct{}
    routePrefixs map[string]struct{}
    hosts        []HostMatcher          // 域名匹配(带类型)
    config       PluginConfig           // 这条规则专属的配置
}
读法:map[string]struct{} 存路由/服务集合——这是 Go 里"集合"的惯用法(struct{} 不占内存,只用 key 判存在)。查一个路由名在不在,map 的 O(1) 比遍历 slice 快。RuleMatcher:116-120)持有 []RuleConfig + globalConfig + hasGlobalConfig
L05

解析四类匹配

ParseRuleConfig:182+)对每条规则调四个 parse 方法(:307+):

rule.routes       = m.parseRouteMatchConfig(ruleJson)      // 读 _match_route_
rule.hosts        = m.parseHostMatchConfig(ruleJson)       // 读 _match_domain_
rule.services     = m.parseServiceMatchConfig(ruleJson)    // 读 _match_service_
rule.routePrefixs = m.parseRoutePrefixMatchConfig(ruleJson) // 读 _match_route_prefix_

例如 parseRouteMatchConfig:307-317)把 _match_route_ 数组读进 map[string]struct{}

读法:四个 parse 各读一种匹配键,把值收进对应集合。一条规则可以同时写多种匹配键(比如既 route 又 service)——但 category 只能是一种,由 L06 的优先级决定。
L06

category 推断

rule_matcher.go:233-247:解析完四类匹配后,按"有没有"推断 category:

if boolToInt(hasRoute)+boolToInt(hasService)+boolToInt(hasHosts)+boolToInt(hasRoutePrefix) == 0 {
    return errors.New("...at least one of _match_route_/_match_domain_/_match_service_/_match_route_prefix_...")
}
if hasRoute {
    rule.category = Route
    if hasService { rule.category = RouteAndService }  // 路由+服务
} else if hasHosts {
    rule.category = Host
} else if hasService {
    rule.category = Service
} else {
    rule.category = RoutePrefix
}
推断的优先级 先看有没有路由:有路由 + 有服务 = RouteAndService,只有路由 = Route;没路由看域名 = Host;再看服务 = Service;都没有看前缀 = RoutePrefix。并且强制"至少写一种匹配条件",否则报错(一条规则不能什么都不匹配)。这个推断让配置更简洁——你写哪个 _match_,category 自动定。
📝 单步走查:几条规则各被推断成什么 category
规则写了哪些 _match_hasRoute/Service/Hosts/Prefix推断结果
_match_route_:[a]✓ / ✗ / ✗ / ✗Route
_match_route_:[a] + _match_service_:[s]✓ / ✓ / ✗ / ✗RouteAndService
_match_domain_:[*.ex.com]✗ / ✗ / ✓ / ✗Host
_match_service_:[s]✗ / ✓ / ✗ / ✗Service
(一个 _match_ 都没写)✗ / ✗ / ✗ / ✗❌ 报错:至少写一种
注意优先级:route 一旦出现就压过 host/prefix;route+service 是特例升级成 RouteAndService。
L07

Host 匹配三型

域名匹配比路由复杂(要支持通配)。MatchType:39-44):Prefix*.example.com)/ Exactwww.example.com)/ Suffixexample.*)。HostMatcher:54-57)存 matchType + host

为什么域名要三种匹配? 域名常带通配符:*.example.com(前缀通配,匹配所有子域名)、example.*(后缀通配)、精确域名。SDK 解析域名时判断通配符位置,归类成 Prefix/Suffix/Exact,运行时(明天)用对应逻辑匹配。这比路由名(只精确或前缀)更细。GenerateHashKey:67-113)还能为规则生成稳定哈希键(用于规则级隔离的备份定位,Day 09)。

👶 小白:*.example.com 里的 *,能匹配 a.b.example.com 这种多级子域名吗?

👨‍🏫 老师:这类通配的具体匹配逻辑在明天(D08)的运行时 hostMatch 里。今天你只要记住:SDK 解析域名时先看 * 在开头还是结尾,把规则归成 Prefix / Suffix / Exact 三种"型号",运行时再按型号套对应的比较逻辑。今天是"给规则贴标签",明天才是"拿请求去比对"。

L08

今日小结 + 动手

🧠 今天你应该能回答

  • 为什么需要 _rules_?和 Console 的四作用域什么关系?
  • 五种 Category 分别按什么匹配?
  • 为什么用 map[string]struct{} 存路由集合?
  • category 怎么从 _match_xxx_ 推断?域名为什么要三种匹配型?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '30,64p' pkg/matcher/rule_matcher.go
sed -n '225,247p' pkg/matcher/rule_matcher.go
sed -n '307,330p' pkg/matcher/rule_matcher.go
明天预告 · Day 08运行时匹配 GetMatchConfig——请求来了,SDK 怎么用 :authority/route_name/cluster_name 逐条规则匹配,选出该用哪份配置。
← Day 06 Day 08 · 运行时匹配 →