Day 09 / 共 20 天 · 第 2 周 xDS 生成
Push 与事件模型
你改了一条 VirtualService,几秒后所有相关 Envoy 就更新了——中间发生了什么?今天追这条"神经反射弧":ConfigUpdate → debounce → Push → PushQueue → 各 proxy。
📍 你在控制面 istiod 之旅的位置(配置会翻译了,今天看"配置一变,怎么自动送到 Envoy")
D1 全景·
D2 启动·
D3 配置CRD·
D4 服务注册·
D5 快照·
D6 xDS服务·
D7 生成·
D8 Builder·
D9 推送·
D10 连接
L01
完整链路
🤔 痛点:几千个 Envoy,配置一变全部无脑重推会怎样
你
kubectl apply 改一条路由。若 istiod 立刻给全网 3000 个 Envoy 各自全量重算重推一遍——CPU 打满、网络风暴、Envoy 集体抖动。可你又希望"改完几秒就生效"。既要快,又要稳,中间必须有套精巧的机制。💡 本质:一条"神经反射弧" + 三道减负闸门
这套推送机制像人的神经反射:感知(K8s 事件)→ 传导(debounce 合并)→ 中枢处理(重建 PushContext)→ 指挥(PushQueue 分发)→ 动作(生成下发 xDS)。为了不被刺激淹没,路上设三道闸门——debounce(把连按的电梯合成一趟)、PushContext 增量(只重算变的)、proxy 级裁剪(只通知相关的保安)。快与稳,全靠这三道闸门平衡。
registry/config 事件(K8s CRD/服务变化)
DiscoveryServer.ConfigUpdate(discovery.go:318)投递到 pushChannelhandleUpdates → debounce(防抖合并,100ms)Push(构建/复用 PushContext,Day 05)AdsPushAll → StartPush:遍历所有连接入 pushQueuesendPushes:并发从 pushQueue 出队pushConnection → pushXds:生成并下发 xDS(Day 07-08)读法:这条链是控制面的"神经系统"——从"配置变了"到"Envoy 收到新配置"的完整路径。两级缓冲:
pushChannel(防抖前)和 pushQueue(防抖后、下发前)。L02
事件源
// pilot/pkg/bootstrap/server.go:893 initRegistryEventHandlers
// 服务变更(:896):serviceHandler → PushRequest{Full:true, ConfigsUpdated:{ServiceEntry...}} → ConfigUpdate
// 配置(CRD)变更(:908):configHandler,EventUpdate 时若 spec 未变则跳过(needsPush)
// EDS 增量:Full:false,由 EDSUpdate 触发(不经这里,Day 07)
读法:Day 04 的服务注册表、Day 03 的 ConfigStore 检测到变化,就调
ConfigUpdate 触发推送。关键优化:配置更新时若 spec 没实质变化(只是 resourceVersion 变了)就跳过(needsPush)——避免无谓推送。L03
PushRequest
// pilot/pkg/model/push_context.go:366
type PushRequest struct {
Full bool // true=全量重算 PushContext;false=增量(一般仅 EDS)
ConfigsUpdated sets.Set[ConfigKey] // 变更配置集合;空=推所有;非空=只推依赖它的 proxy
Push *PushContext // 本次用的快照
Reason ReasonStats // 触发原因(受控枚举,防 metrics 基数爆炸)
Forced bool // 强制生成下发
}
// Merge()(:500):debounce 时合并 —— Full 取或、ConfigsUpdated 求并
读法:
PushRequest 描述"一次推送请求"。ConfigsUpdated 是关键——记录"哪些配置变了",用于 proxy 级裁剪(L07)。Merge 让 debounce 时能把多个请求合并成一个(Full 求或、ConfigsUpdated 求并)。配置变更绝不能是增量(只有 EDS 会 Full=false)。L04
debounce 防抖
// pilot/pkg/xds/discovery.go:357 debounce
// 默认:DebounceAfter=100ms(每事件后静默期)、DebounceMax=10s(累计最长等待)
// pushWorker(:378):quietTime >= DebounceAfter || eventDelay >= debounceMax 才真推
// 期间的事件用 req = req.Merge(r) 累积合并
debounce(防抖)= "等一等再一起处理"
想象你批量
kubectl apply 了 50 个 CRD——如果每个都立即触发一次全量推送,istiod 会被打爆、Envoy 收到 50 次抖动的配置。debounce 的策略:收到事件后不立即推,而是等一个"静默期"(100ms)。如果 100ms 内又来事件,合并(Merge)并重置计时;直到 100ms 内没有新事件(或累计等了 10s),才把合并后的一个 PushRequest 真正推出去。好比电梯——不是每按一次就走,而是等一会儿把同时按的人一起送。这把"50 次抖动"合并成"1 次推送",是控制面稳定性的关键。Higress 还加了 ShouldBlockPush 全局节流。debounce 时间线:事件不断来就不断重置计时,直到静默 100ms(或累计满 10s),才把合并后的请求推一次。
📝 单步走查:批量 apply 50 个 CRD
| 时刻 | 发生什么 | debounce 状态 |
|---|---|---|
| t=0ms | 第 1 个 CRD 事件到 | 启动 100ms 计时,req = e1 |
| t=0~40ms | 第 2~50 个陆续到 | 每来一个 Merge 并重置计时 |
| t=140ms | 最后一个后静默满 100ms | 触发!推 1 个合并后的 PushRequest |
| 结果 | 50 次事件 | 只产生 1 次全量推送 |
L05
两级缓冲
// Push(discovery.go:277):
// Full → initPushContext(重算/复用 PushContext,Day 05)→ AdsPushAll
// 非 Full → 直接 AdsPushAll(不重建 PushContext)
// AdsPushAll → StartPush(ads.go:584):for 每个连接 pushQueue.Enqueue(p, req)
读法:debounce 后进入
Push:全量则重建/复用 PushContext(Day 05 的增量复用优化),然后 AdsPushAll 把请求投给每个连接入 pushQueue。两级缓冲:pushChannel(防抖前,吸收突发)→ debounce → pushQueue(防抖后,控制并发下发)。L06
PushQueue 合并
// pilot/pkg/xds/pushqueue.go:23 PushQueue
// pending map[*Connection]*PushRequest // 排队中,重复入队会 merge
// processing map[*Connection]*PushRequest // 已出队未完成
// Enqueue(:51):已在 pending/processing → CopyMerge(合并,不重复排队)
// sendPushes → doSendPushes(:491):semaphore 限并发 + Dequeue
PushQueue 保证"每个 proxy 至多一个待推请求"
如果推送队列里对同一个 Envoy 堆了 10 个待推请求,纯属浪费(只需推最新的)。PushQueue 的巧思:每个 proxy 在队列里至多一个请求——重复入队时不新增,而是
Merge 进已有的那个。还有并发控制(concurrentPushLimit 信号量):同时最多推 N 个 proxy(按核数),防止一次性给几千个 Envoy 推送打爆 istiod。这是"合并 + 限流"双重保护——大规模网格下 istiod 稳定的关键。L07
ProxyNeedsPush
// pilot/pkg/xds/proxy_dependencies.go:114 DefaultProxyNeedsPush
if req.Forced { return req, true }
req = filterRelevantUpdates(proxy, req) // 按 Sidecar 依赖过滤(Day 05)
return req, len(req.ConfigsUpdated) > 0 // 过滤后无关联则跳过该代理
三级优化的完整拼图(
① debounce(本课 L04)——把突发事件合并成少数几次推送
② PushContext 增量复用(Day 05
③ proxy 级裁剪(本课,
层层收窄:从"哪些事件"→"重算哪些"→"推给谁"。这套优化让 istiod 能管理数千 Envoy 而不被推送风暴压垮。
pilot.md:166-181):① debounce(本课 L04)——把突发事件合并成少数几次推送
② PushContext 增量复用(Day 05
updateContext)——即便全量推送也只重算变化的索引③ proxy 级裁剪(本课,
ProxyNeedsPush)——只推给依赖变更配置的 proxy,其余跳过层层收窄:从"哪些事件"→"重算哪些"→"推给谁"。这套优化让 istiod 能管理数千 Envoy 而不被推送风暴压垮。
💬 对话体
👶 小白:改一条 payment 服务的路由,为什么不是所有 Envoy 都要更新?
👨🏫 老师:因为大部分 Envoy 根本不调用 payment(它们的 SidecarScope 不依赖 payment 的配置)。
👶 小白:那 EDS 端点变化也走这套吗?
👨🏫 老师:不,端点高频变化走
👨🏫 老师:因为大部分 Envoy 根本不调用 payment(它们的 SidecarScope 不依赖 payment 的配置)。
ProxyNeedsPush 用 filterRelevantUpdates 一过滤,发现这次变更和某个 proxy 无关(ConfigsUpdated 过滤后为空),就直接跳过它,连生成都省了。只有"会调 payment"的那批 Envoy 才真收到推送。👶 小白:那 EDS 端点变化也走这套吗?
👨🏫 老师:不,端点高频变化走
Full=false 的独立快路径(Day 07),不重建 PushContext,直接增量推 EDS。L08
今日小结 + 动手
🧠 今天你应该能回答
- Push 完整链路的七步?两级缓冲是哪两级?
- PushRequest 的 Full/ConfigsUpdated 各是什么?Merge 干嘛?
- debounce 防抖解决什么问题?100ms/10s 各是什么?
- PushQueue 怎么保证"每 proxy 至多一个请求"?
- 三级优化分别是什么?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/istio
grep -n 'func.*ConfigUpdate\|func debounce\|func (s \*DiscoveryServer) Push' pilot/pkg/xds/discovery.go
grep -n 'type PushQueue struct\|func.*Enqueue' pilot/pkg/xds/pushqueue.go
grep -n 'DefaultProxyNeedsPush' pilot/pkg/xds/proxy_dependencies.go
明天预告 · Day 10(第2周收官):代理连接生命周期——把第 2 周串起来:一个 Envoy 从连上 istiod、拿初始配置、到收增量更新、到断开的完整故事。