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 对账。DeleteMcpBridgeReconcile(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 同思想)。
对花名册:应到 vs 实到 → 三组动作 期望(应到) Nacos Consul 现状(实到) Nacos Zookeeper Nacos:两边都有 → 不动 Consul:只在应到 → 新建 ZK:只在实到 → Stop 请走 key = type/name;三组分别 toBeCreated / 不变 / toBeDeleted
图注:只动变化的那一格——正在服务的 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 从 [nacos/A, zookeeper/B] 改成 [nacos/A, consul/C]
key (type/name)在期望?在现状?动作
nacos/A不动(连接保留)
zookeeper/BtoBeDeleted → Stop()
consul/CtoBeCreated → go Run()
结果:只停 1 个、只起 1 个,Nacos 毫发无损。最后 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 08Nacos 注册表——Higress 最重用的注册源。看 Nacos watcher 怎么订阅、轮询、把实例转成 ServiceEntry。三代实现的演进。
← Day 06 多注册中心 Day 08 · Nacos 注册表 →