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 的事件循环也是类似思路)。一条流两个话务员: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,带 ErrorDetail | NACK | 记录 reject,不重发 |
| nonce=xyz(早已过期) | 陈旧 nonce | 忽略 |
| nonce=abc,新增订阅一个 route | 真请求 | 生成新增部分并响应 |
💬 对话体
👶 小白:为什么 ACK 要"什么都不做",不回一句"好的"吗?
👨🏫 老师:回"好的"又得对方再确认,没完没了。xDS 约定:ACK 只是让 istiod 知道"这份配置生效了、记下当前 nonce",无需回话。只有真正有新东西要发(新订阅/配置变了)才发下一条。沉默即"一切正常"。
👨🏫 老师:回"好的"又得对方再确认,没完没了。xDS 约定:ACK 只是让 istiod 知道"这份配置生效了、记下当前 nonce",无需回话。只有真正有新东西要发(新订阅/配置变了)才发下一条。沉默即"一切正常"。
L06
通配 vs 非通配
IsWildcardTypeURL(server.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 生成器范例。