Day 07 / 共 20 天 · 第 2 周 xDS 生成
配置生成总览
istiod 最核心的工作:把 CRD + 服务翻译成 Envoy xDS。今天看翻译层的骨架:ConfigGenerator 顶层接口、XdsResourceGenerator 生成器接口、以及 CDS 生成器怎么"薄封装 + 委托"。
📍 你在控制面 istiod 之旅的位置(专线通了,今天看总调度室怎么把工单"翻译"成保安听得懂的指令)
D1 全景·
D2 启动·
D3 配置CRD·
D4 服务注册·
D5 快照·
D6 xDS服务·
D7 生成·
D8 Builder·
D9 推送·
D10 连接
L01
ConfigGenerator
🤔 痛点:你写的是 VirtualService,Envoy 只认自己的 xDS
你递交的工单是 Istio 的 VirtualService/DestinationRule(人类友好),可 Envoy 只认 Listener/Route/Cluster 这套 xDS 结构。中间必须有个"翻译官",把工单语言逐条译成 Envoy 语言——这就是配置生成层。
💡 本质:一支分工明确的翻译科
配置生成层像总调度室的翻译科,按译文种类分窗口:BuildListeners 译"监听哪些端口"、BuildClusters 译"有哪些后端集群"、BuildHTTPRoutes 译"怎么路由"。上层的 xDS 生成器(Day 02 的 CdsGenerator 等)是前台接待——先判断"这单要不要重译",要才把活派给对应翻译窗口(core 层 Build 方法)。前台薄、翻译科重,各司其职。
// pilot/pkg/networking/core/configgen.go:29
type ConfigGenerator interface {
BuildListeners(node, req) []*listener.Listener // :33 LDS
BuildClusters(node, req) ([]*discovery.Resource, ...) // :36 CDS
BuildHTTPRoutes(node, req, routeNames) (...) // :44 RDS
BuildNameTable(node, push) *NameTable // :51 DNS
BuildExtensionConfiguration(...) [...] // :54 ECDS
}
读法:
ConfigGenerator 是"翻译引擎"的顶层接口——每个 Build* 方法负责生成一类 Envoy 资源。BuildScopedRoutes(:47)是 Higress 定制的 SRDS(大规模路由优化)。实现体 ConfigGeneratorImpl 只持有一个缓存。这些 Build 方法就是"CRD → xDS"翻译的入口。L02
XdsResourceGenerator
// pilot/pkg/model/context.go:288
type XdsResourceGenerator interface {
Generate(proxy *Proxy, w *WatchedResource, req *PushRequest) (Resources, ...)
}
type XdsDeltaResourceGenerator interface { // 支持增量
XdsResourceGenerator
GenerateDeltas(...) (Resources, DeletedResources, ...)
}
两层生成器:xDS 层 vs core 层
有两组"生成器",别混:①xDS 层的
XdsResourceGenerator(Day 02 注册的 CdsGenerator/LdsGenerator…)——每个 typeURL 一个,是"路由入口";②core 层的 ConfigGenerator(BuildClusters 等)——真正的翻译逻辑。xDS 层生成器是薄封装,收到请求后判断要不要推、然后委托给 core 层的 Build 方法干活。这种"入口层 + 逻辑层"分离,让 xDS 协议处理和配置翻译逻辑解耦。前台(xDS 层)判断"要不要重译",要才委托给翻译科(core 层 Build 方法),产出 Envoy 认的 xDS 资源。
L03
CDS 生成器范例
// pilot/pkg/xds/cds.go:122 CdsGenerator.Generate
func (c CdsGenerator) Generate(proxy, w, req) (...) {
req, needsPush := cdsNeedsPush(req, proxy) // 判断要不要推
if !needsPush { return nil, ... } // 不推直接返回
clusters, logs := c.ConfigGenerator.BuildClusters(proxy, req) // 委托 core 层
return clusters, logs, nil
}
读法:典型的薄封装:先
cdsNeedsPush 判断(这个 proxy 需要重新生成 CDS 吗),需要才调 core 层的 BuildClusters。LDS/RDS 生成器结构完全一样。RDS 特殊:是"点名订阅"(Envoy 只订阅它需要的 route),所以只生成订阅的那些。L04
薄封装 + 委托
为什么要"判断要不要推"?
Day 05 讲了"依赖跟踪"——不是每次配置变更都要给每个 proxy 重发所有类型。
cdsNeedsPush 就是判断"这次变更,这个 proxy 的 CDS 需要重新生成吗"。比如只改了某个 RDS 相关配置,CDS 没变,就跳过 CDS 生成。这避免了无谓的重新翻译(翻译是有成本的)。这是三级优化(Day 05/09)在生成层的体现——连"要不要生成"都精打细算。不需要就返回 nil,Envoy 那边就保持现有 CDS 不变。L05
增量生成
支持增量的生成器实现 GenerateDeltas。CDS 的 GenerateDeltas(cds.go:132)调 BuildDeltaClusters——解析 cluster 名(outbound|port|subset|hostname),据 ConfigsUpdated 只算增删的集群。
读法:Delta(增量)模式下,只生成/删除"变化的"资源,不重发全部。
BuildDeltaClusters 根据"哪些 ServiceEntry/DestinationRule 变了"精确算出要增删哪些集群。Envoy 课讲的 Delta xDS 就是消费这个。全量模式(SotW)则用 Generate。📝 举个例子:一个 cluster 名字里藏着什么
istiod 生成的集群名形如
当你新增一个
outbound|9080|v2|reviews.default.svc.cluster.local,四段分别是方向|端口|subset|host。当你新增一个
DestinationRule 给 reviews 加了 subset v3 → BuildDeltaClusters 只需增一个 outbound|9080|v3|reviews… 集群,其余集群纹丝不动,也不用重发。这就是靠解析集群名做到的精准增量。L06
EDS 独立路径
// pilot/pkg/xds/eds.go:84 EdsGenerator —— 不走 ConfigGeneratorImpl!
// 直接用内存 endpoint 索引 EndpointIndex
// Generate(:117)→ buildEndpoints:查缓存命中直接返回,
// 否则 builder.BuildClusterLoadAssignment(EndpointIndex) 现算
// EDSUpdate(:47):端点变化直接触发 push,不重算 PushContext
EDS 为什么"特殊对待"?
回想 Day 01/05:端点是最高频变更资源(Pod 扩缩容随时发生)。如果每次端点变化都重建整张 PushContext(Day 05),开销巨大。所以 EDS 走独立优化路径:不经过 PushContext、不重算快照,而是直接用一个内存里的
EndpointIndex,端点变了就增量更新这个索引并推送。其他 xDS(CDS/LDS/RDS)依赖 PushContext,EDS 绕开它。这是"把最高频的操作抽出来单独优化"的工程智慧——针对热点做专门的快路径。💬 对话体
👶 小白:既然 EDS 不走 PushContext,那它和 CDS 会不会对不上?比如 CDS 里的集群,EDS 里没端点?
👨🏫 老师:好问题。它们最终一致:新集群通过 CDS 下发后,Envoy 会订阅它的 EDS,EdsGenerator 再从 EndpointIndex 现算端点补上。EDS 走快路径只是"端点变化不必惊动整张快照",集群拓扑仍由 CDS/PushContext 主导。分工是:CDS 定"有哪些集群"(低频),EDS 填"每个集群此刻有哪些机器"(高频)。
👨🏫 老师:好问题。它们最终一致:新集群通过 CDS 下发后,Envoy 会订阅它的 EDS,EdsGenerator 再从 EndpointIndex 现算端点补上。EDS 走快路径只是"端点变化不必惊动整张快照",集群拓扑仍由 CDS/PushContext 主导。分工是:CDS 定"有哪些集群"(低频),EDS 填"每个集群此刻有哪些机器"(高频)。
L07
缓存
生成结果会缓存(DiscoveryServer.Cache)。因为"带 Proxy 翻译"(Day 01),很多 proxy 的配置其实相同(同命名空间、同 SidecarScope),缓存能大幅减少重复翻译。EDS 有专门的缓存(eds.Cache.Get(&builder))。
读法:缓存 key 要包含"所有影响生成结果的因素"(
cluster_builder.go 强调这点)。同样输入 → 同样输出 → 缓存命中直接返回,不重新翻译。这是"带 Proxy 翻译"成本高的必要补偿——用缓存把重复翻译省下来。L08
今日小结 + 动手
🧠 今天你应该能回答
- ConfigGenerator 的 Build* 方法各生成什么?
- xDS 层生成器和 core 层生成器怎么分工?
- CDS 生成器的"薄封装 + 委托"模式?
- 为什么要判断"要不要推"?增量生成怎么做?
- EDS 为什么走独立路径?缓存为什么重要?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/istio
grep -n 'type ConfigGenerator interface\|BuildClusters\|BuildListeners' pilot/pkg/networking/core/configgen.go
sed -n '122,141p' pilot/pkg/xds/cds.go
grep -n 'type EdsGenerator\|func.*EDSUpdate' pilot/pkg/xds/eds.go
明天预告 · Day 08:Listener/Route/Cluster Builder——深入 core 层的三大 Builder,看它们怎么把 Istio 配置一步步构造成 Envoy 的 Listener、Route、Cluster 对象。