Day 06 / 共 20 天 · 第 2 周 xDS 生成

xDS DiscoveryServer

第 2 周进入核心:istiod 怎么向 Envoy 提供配置。今天看 DiscoveryServer——ADS gRPC 服务端的骨架:一条流怎么收请求、发配置,以及 xDS 协议精髓 ACK/NACK 状态机。

📍 你在控制面 istiod 之旅的位置(第 2 周开工:从"存好数据"切到"怎么把配置发出去")
D1 全景· D2 启动· D3 配置CRD· D4 服务注册· D5 快照· D6 xDS服务· D7 生成· D8 Builder· D9 推送· D10 连接
L01

DiscoveryServer

🤔 痛点:配置不是"发一次就完",而要持续对话 Envoy 不是拿一次配置就断线。它和 istiod 保持一条长连接,配置一变就要收到更新,收到后还要回一句"收到/这份有问题"。这种"持续双向对话 + 确认"的机制,比一次性下发复杂得多。
💡 本质:一条专线电话 + 收发两个话务员 + 回执单 DiscoveryServer 像总调度室里为每个保安(Envoy)开的一条专线电话(ADS gRPC 流)。为避免"边听边说卡住",配两个话务员:一个专门听保安说话(Receive 协程收请求/ACK),一个专门播报新指令(主循环推配置)。保安每收到指令要回回执单(ACK),看不懂就回"这条有问题(NACK)"——回执上的编号(nonce)用来对上"你确认的是哪条指令"。
// pilot/pkg/xds/discovery.go:68 DiscoveryServer 核心字段
Generators map[string]XdsResourceGenerator // :77 typeURL→生成器(Day 02 注册的)
pushChannel chan *PushRequest              // :99 debounce 输入(Day 09)
pushQueue  *PushQueue                       // :102 待推送队列
adsClients map[string]*Connection           // :108 活跃连接表
concurrentPushLimit chan struct{}           // :84 并发推送信号量
Cache model.XdsCache                        // :122
读法:DiscoveryServer 是 xDS 服务的中枢:持有生成器路由表(Day 02)、活跃连接表、推送队列、缓存。Register():200)把它注册为 ADS gRPC 服务实现。Start():217)起 5 个后台协程(handleUpdates debounce、sendPushes 消费队列等)。
L02

ADS 入口

// pilot/pkg/xds/ads.go:182 StreamAggregatedResources → Stream(:187)
// :204 server 未 ready 直接返回 Unavailable(防 Envoy 拿空配置)
// :214 限流 WaitForRequestLimit
// :219 认证 s.authenticate(ctx)
// :231 newConnection(peerAddr, stream) 后进入通用流循环 xds.Stream(con)
ADS = "一条流管所有配置" 回想 Envoy 课 Day 12:Envoy 用 ADS(聚合发现服务)把所有 xDS 类型复用一条 gRPC 流。istiod 这边的 StreamAggregatedResources 就是这条流的服务端入口——Envoy 连上来,一条双向 gRPC 流承载所有类型的请求和响应。连接建立前有三道关:ready 检查(Day 02 的门禁)、限流(防连接风暴)、认证(Day 14 的 mTLS)。过了才建 Connection 开始服务。
L03

收发分离

// pkg/xds/server.go(通用 xDS 流框架,ADS/SDS 复用)
// Stream(:234):
//   :243 起 go Receive(ctx) 单独协程做阻塞 Recv()
//   :250 等 <-con.initialized(首请求完成初始化后才服务)
//   :252 主循环:select 处理 reqChan(请求/ACK/NACK)和 pushChannel(推送)
为什么收和发要分两个协程? 一条 gRPC 流上,既有 Envoy 发来的请求/ACK(收),也有 istiod 主动推的配置(发)。如果一个协程既阻塞等收、又要发,会互相卡住。所以拆开:一个 Receive 协程专门阻塞收请求塞进 reqChan;主循环用 select 同时等"reqChan(有请求来了)"和"pushChannel(有配置要推)",哪个来了处理哪个。这是并发编程里"收发解耦"的经典模式(Envoy 课 Day 04 的事件循环也是类似思路)。
Envoy一条 ADS 流 Receive 协程阻塞 Recv → reqChan 主循环 select等 reqChan / pushChannel 请求/ACK 推配置 reqChan Generators 生成按 typeURL 路由
一条流两个话务员:Receive 只管收(塞进 reqChan),主循环 select 同时等"有请求"和"有配置要推",互不阻塞。
L04

首请求初始化

// pkg/xds/server.go Receive(:295)循环 Recv()
//   首个请求(:322)必须带 Node.Id → ctx.Initialize(req.Node)
//   → initConnection(ads.go:239):解析 node id → model.Proxy
//     addCon 注册进 adsClients(此后才被 push 遍历到)
//     computeProxyState 算 SidecarScope/labels(Day 05)
//     MarkInitialized() 关闭 initialized 通道 → 主循环开始服务
读法:Envoy 连上后第一个请求必须报上自己是谁(Node.Id)。istiod 据此解析成 model.Proxy、算出它的 SidecarScope(Day 05,决定它能看到什么)、注册进活跃连接表。初始化完成才 MarkInitialized,主循环才开始给它发配置——保证配置生成时 proxy 信息完备。
L05

ShouldRespond(协议精髓)

// pkg/xds/server.go ShouldRespond(:352)—— 拆解 Envoy 的三类消息
// :358 request.ErrorDetail != nil → NACK(记录 reject,不响应)
// :381 ResponseNonce=="" 或 previousInfo==nil → 初始请求/重连(响应)
// :390 nonce 与 NonceSent 不匹配 → 陈旧 nonce(忽略)
// :406 nonce 匹配 + 无资源增删 → ACK(不响应)
// :438 有新增订阅 → 响应
ACK / NACK / 请求:三合一的判定 回想 Envoy 课 Day 11 的 ACK/NACK。问题:Envoy 发来的消息在同一个 API 里不区分"这是新请求、还是对上次下发的确认(ACK)、还是拒绝(NACK)"。ShouldRespond 就是那个拆分器:带 error → NACK(配置有问题,记下来);nonce 匹配且无新订阅 → ACK(收到了,啥也不用做);nonce 空/不匹配或有新订阅 → 真请求(要生成并响应)。这是 xDS 最终一致性协议的服务端核心——搞懂它就理解了"istiod 和 Envoy 怎么对话"。nonce(一次性随机数)用来对上"这个确认针对哪次下发"。
📝 单步走查:一条消息进 ShouldRespond 怎么判
Envoy 发来的消息ShouldRespond 判定istiod 动作
首个 CDS 请求(nonce 空)初始请求生成并响应 CDS
nonce=abc,无 error,无新订阅ACK什么都不做(收到了)
nonce=abc,带 ErrorDetailNACK记录 reject,不重发
nonce=xyz(早已过期)陈旧 nonce忽略
nonce=abc,新增订阅一个 route真请求生成新增部分并响应
💬 对话体 👶 小白:为什么 ACK 要"什么都不做",不回一句"好的"吗?
👨‍🏫 老师:回"好的"又得对方再确认,没完没了。xDS 约定:ACK 只是让 istiod 知道"这份配置生效了、记下当前 nonce",无需回话。只有真正有新东西要发(新订阅/配置变了)才发下一条。沉默即"一切正常"。
L06

通配 vs 非通配

IsWildcardTypeURLserver.go:114):CDS/LDS 是通配类型(缺失即删除,必须全量),SecretType/EndpointType/RouteType/ECDS 是非通配(可只发订阅的子集)。

这就是 EDS 能增量的协议基础 回想 Envoy 课 Day 15:端点(EDS)变化最频繁。如果每次一个 Pod 增删都全量重发所有集群的所有端点,开销巨大。xDS 协议规定:CDS/LDS 是"根类型"——发送的就是全部,没发的视为删除(所以必须全量);EDS/RDS/SDS 是"非根类型"——可以只发变化的那些,没发的不删除。于是 EDS 能做增量推送(只发变化的服务的端点)。这个"通配 vs 非通配"的区分,是 istiod 高频端点更新优化的协议前提。
L07

请求优先

主循环(server.go:252-292)先用非阻塞 select 优先处理 reqChan(请求/ACK,成本低),再在第二个 select 里同时等请求和推送——注释解释"请求优先、避免推送饿死请求"。

读法:请求(尤其 ACK)处理很快且重要(要及时确认 Envoy 的状态),推送(生成配置)较重。所以设计成"有请求先处理请求,没请求才处理推送"——防止推送任务把请求处理"饿死"。这是并发调度里的优先级设计。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • DiscoveryServer 持有哪些核心结构?
  • ADS 入口连接前的三道关?
  • 为什么收发要分两个协程?
  • 首请求为什么必须带 Node.Id?初始化做什么?
  • ShouldRespond 怎么拆分 ACK/NACK/请求?通配 vs 非通配的意义?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/istio
grep -n 'type DiscoveryServer struct\|func.*StreamAggregatedResources' pilot/pkg/xds/discovery.go pilot/pkg/xds/ads.go
grep -n 'func.*ShouldRespond\|func IsWildcardTypeURL' pkg/xds/server.go
明天预告 · Day 07配置生成总览——istiod 怎么把 CRD 翻译成 Envoy xDS?ConfigGenerator 接口、XdsResourceGenerator 生成器、CDS 生成器范例。
← Day 05 PushContext Day 07 · 配置生成总览 →