Day 07 / 共 20 天 · 第 2 周 服务发现与配置
McpBridge + reconcile
用户用 McpBridge CRD 声明"从哪些注册中心发现服务",Reconciler 是那个"控制循环"——diff 出该创建/停止哪些 watcher。典型的 K8s Operator 模式。昨天(Day 06)看的是"单个 Watcher 长什么样",今天看谁来管这一群 Watcher——用户改一次接入清单,怎么精准地增删对应 watcher,为 Day 08 深入某个具体 watcher(Nacos) 铺路。
📍 你在 Higress 源码课的位置(第1周 全景与控制器 · 第2周 服务发现与配置)
D01 全景→
D02 启动→
D03 Ingress控制器→
D04 配置翻译→
D05 xDS-over-MCP→
D06 多注册中心→
D07 McpBridge→
D08 Nacos→
D09 ServiceEntry→
D10 配置+证书
🤔 痛点:用户改了接入清单,怎么知道该新起/停掉哪些 watcher?
用户在 McpBridge 里原本配了 Nacos + Zookeeper,现在改成 Nacos + Consul。最笨的做法是"全停掉重来"——但正在服务的 Nacos watcher 被无谓重启,流量会抖动。理想做法是只动变化的部分:Nacos 不动、停掉 ZK、新起 Consul。怎么精准算出这个差异?
💡 本质:Reconcile = 拿着两份名单"点名对表"
把 Reconcile 想成开学第一天班主任对花名册:手上一份"应到名单"(McpBridge 声明的注册中心),眼前一份"实到名单"(当前跑着的 watcher)。逐个对:应到又实到的→不动;应到没实到的→喊来(新建 watcher);实到但不在应到的→请走(Stop watcher)。你不需要告诉系统"具体做哪几步",只需给出"最终应该有谁在",系统自己算出增删——这就是声明式(K8s 的灵魂),和你在 Istio Operator 见过的是同一套。
L01
McpBridge CRD
// api/networking/v1/mcp_bridge.proto:47
McpBridge { registries []RegistryConfig; proxies []ProxyConfig }
// RegistryConfig(:52):type(必填)/name/domain/port/authSecretName
// Nacos 专属:nacosNamespaceId/nacosGroups/nacosRefreshInterval…
// Consul 专属:consulDatacenter/consulServiceTag…
// MCP Server(nacos3):mcpServerExportDomains/enableMCPServer/allowMcpServers
McpBridge = "注册中心接入清单"
你写一个 McpBridge YAML,列出"我要从这个 Nacos、那个 Consul 发现服务",每个 registry 配好类型、地址、端口、认证。Higress 读这个 CRD,就知道该连哪些注册中心。全局单例名
default(一个集群一个 McpBridge)。这是"声明式配置"——你声明想要什么,Higress 负责实现(连上、订阅、转 ServiceEntry)。Higress 有三个自定义 CRD:McpBridge(注册中心)、WasmPlugin(插件)、Http2Rpc(协议转换)。今天是第一个。L02
谁监听它
// pkg/ingress/kube/mcpbridge/controller.go:33 NewController
// 基于 informer 监听 McpBridge,复用通用 CommonController(Day 03)
// 装配:pkg/ingress/config/ingress_config.go:215
mcpbridgeController.AddEventHandler(config.AddOrUpdateMcpBridge, config.DeleteMcpBridge)
// :2010 go m.mcpbridgeController.Run(stop)
读法:mcpbridge 控制器(Day 03 的六大子控制器之一)watch McpBridge CRD,变更时回调
AddOrUpdateMcpBridge/DeleteMcpBridge。复用泛型 CommonController(Day 03)。L03
AddOrUpdateMcpBridge
// pkg/ingress/config/ingress_config.go:1341
// 只处理名为 default 的 McpBridge(单例约定,:1343)
// 懒初始化 RegistryReconciler = reconcile.NewReconciler(serviceUpdate, ...)(:1352)
// serviceUpdate 回调:服务变化时对 se/dr/vs/wasm/ef 所有 handler 触发 EventUpdate
// (带 AlwaysPushLabel 强制全量推 xDS)
// reconciler.Reconcile(mcpbridge)(:1414)真正干活
读法:McpBridge 变了 → 懒建 Reconciler(传入
serviceUpdate 回调:服务变了就通知 istiod 全量刷新)→ 调 Reconcile 对账。DeleteMcpBridge → Reconcile(nil) 清空所有 watcher。这里把 Day 06 的 UpdateService 回调接到了向 istiod 推送。L04
Reconciler 控制循环
Reconcile = "让现状逼近期望"
这是 Kubernetes 最核心的编程模式——Reconcile(对账/调谐):拿"期望状态"(McpBridge 里声明的注册中心列表)和"现状"(当前跑着的 watcher)对比,算出差异,让现状变成期望。比如 McpBridge 里加了一个 Consul,Reconcile 发现"期望有 Consul 但现状没有"→ 创建一个 Consul watcher。删了一个 Nacos → 停掉对应 watcher。不是命令式("创建 X"),而是声明式("最终应该有这些",系统自己算怎么达到)。Higress 的 McpBridge + Reconciler 就是这个模式(和 Istio Operator 的 Reconcile 同思想)。
图注:只动变化的那一格——正在服务的 Nacos 不受打扰,避免无谓重启导致流量抖动。
👶 小白:如果每次都"全停重建",代码不是更简单吗?为什么非要 diff?
👨🏫 老师:简单,但代价大。全停重建意味着没变化的 Nacos watcher 也被拆了重连——重连期间它发现的服务会短暂"消失",网关可能把流量转到已经下线的实例,或短时找不到后端。diff 只动真正变化的部分,让稳定的连接稳稳地留着。这就是为什么 K8s 全生态都用"对账"而非"重来"。
L05
diff 式协调
// registry/reconcile/reconcile.go reconcileRegistries(:108)
// 用 path.Join(type, name) 做 key,算出 toBeCreated/toBeUpdated/toBeDeleted
// 删除/更新先 watcher.Stop()(:136)
// 新建则 generateWatcherFromRegistryConfig 后 go watcher.Run()(:156)
// 最后用 WaitGroup + 60s 超时等所有 watcher ready(:171)
读法:典型 diff:按
type/name 做 key,对比期望和现状,分成"要建/要改/要删"三组,分别处理。改和删都先 Stop() 旧 watcher,建则起新 watcher 的 goroutine。PurgeStaleItems(:101)在最后物理删除标记删除项。等所有 watcher ready(60s 超时)才算对账完成。📝 单步走查表:一次 Reconcile 具体做了什么
场景:McpBridge 从
结果:只停 1 个、只起 1 个,Nacos 毫发无损。最后
[nacos/A, zookeeper/B] 改成 [nacos/A, consul/C]
| key (type/name) | 在期望? | 在现状? | 动作 |
|---|---|---|---|
nacos/A | ✅ | ✅ | 不动(连接保留) |
zookeeper/B | ❌ | ✅ | toBeDeleted → Stop() |
consul/C | ✅ | ❌ | toBeCreated → go Run() |
WaitGroup 等新 watcher ready(60s 超时)才返回。L06
watcher 工厂
// reconcile.go generateWatcherFromRegistryConfig(:186)—— 工厂分发中枢
switch registry.Type {
case "nacos": nacos.NewWatcher(...)
case "nacos2","nacos3": nacosv2.NewWatcher(...)
case "zookeeper": zookeeper.NewWatcher(...)
case "consul": consul.NewWatcher(...)
case "static","dns": direct.NewWatcher(...)
case "eureka": eureka.NewWatcher(...)
}
// :281 给每个 watcher 挂 ReadyHandler(wg.Done) + AppendServiceUpdateHandler(serviceUpdate)
读法:又是工厂模式(Envoy/Istio 课反复见过)——按
registry.Type 分发到对应 watcher 构造。这是"多源发现的开关中枢"。挂上 Day 06 的两个回调(Ready→wg.Done、ServiceUpdate→serviceUpdate)联动上层。L07
getAuthOption
reconcile.go:296 getAuthOption:若配了 AuthSecretName,从 K8s Secret 读用户名/密码/token 填进 AuthOption(Day 06)。reconcileProxies(:332)同样 diff 处理 HTTP 正向代理配置。
读法:连注册中心需要认证时,从 McpBridge 引用的 Secret 里读凭据(不明文存 CRD)。这坐实了 Day 06 的"认证信息存 Secret"。
GetRegistryWatcherStatusList(:399)暴露每个 watcher 的健康/就绪状态供运维。L08
今日小结 + 动手
🧠 今天你应该能回答
- McpBridge CRD 声明什么?为什么是单例?
- 谁监听 McpBridge?变更回调是什么?
- Reconcile 控制循环的思想(期望 vs 现状)?
- diff 式协调怎么算出建/改/删?
- watcher 工厂按什么分发?认证凭据怎么读?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/higress
sed -n '47,90p' api/networking/v1/mcp_bridge.proto
grep -n 'func.*Reconcile\|reconcileRegistries\|generateWatcherFromRegistryConfig' registry/reconcile/reconcile.go
grep -n 'AddOrUpdateMcpBridge' pkg/ingress/config/ingress_config.go
明天预告 · Day 08:Nacos 注册表——Higress 最重用的注册源。看 Nacos watcher 怎么订阅、轮询、把实例转成 ServiceEntry。三代实现的演进。