土法炼钢兴趣小组的算法知识备份

【Istio 控制面】生产侧 SotW 与 Delta:debounce、全量推与端点抖动

文章导航

分类入口
networkkubernetes
标签入口
#istio#istiod#xds#sotw#delta#debounce#eds#push-economics

目录

Envoy 第 10 篇 把消费侧讲清楚了:SotW 与 Incremental 是 xDS 协议的两个变体,由 Envoy 拨的是 StreamAggregatedResources 还是 DeltaAggregatedResources 决定。生产侧的问题不是”该用哪种协议”——istiod 两种都支持,由客户端选——而是:同一条 ADS 连接上,istiod 什么时候认为需要重新计算全局状态(Full),什么时候只做一次廉价的定向更新(Incremental);成千上万次 K8s watch 事件如何被合并成一次推送;端点抖动下这套机制会不会把控制面自己拖垮。

本文只钉生产侧:model.PushRequest.Full 与 debounce 合并逻辑,以及它们与 Envoy 消费侧协议变体的正交关系。控制面推送顺序与 NACK 责任见下一篇

本文是「Istio 控制面内核」系列第 10 篇(共 16 篇)。→ 系列目录

版本锚定:Istio 1.30.3istio/istio tag 1.30.3);数据面消费侧协议对齐 Envoy v1.39.0 xDS protocol。无本机 istiod 环境,不粘贴伪造 istioctl / config_dump 输出;命令与日志格式引用源码原文,不编造运行结果。


一、两个容易混淆的”SotW/Delta”

Envoy 系列讲的 SotW/Delta 是协议变体:客户端与服务端之间某个 xDS 类型走全量快照还是差量增删。Istio 生产侧还有另一套看起来同名、实则正交的区分:model.PushRequest.Full 字段——它决定的是istiod 这一次要不要重算全局 push context,与走哪条 gRPC 方法无关。

pilot/pkg/xds/delta.gopushDeltaXds)证明了这个正交性:即便代理走的是 DeltaAggregatedResources(协议层 Delta),生成器如果没实现 GenerateDeltas(只有 Generate),istiod 依然会做一次全量生成,再用当前 WatchedResource.ResourceNames 与生成结果做差集,手工拼出 RemovedResources,模拟出 Delta 语义:

// pilot/pkg/xds/delta.go(Istio 1.30.3,节选,pushDeltaXds)
switch g := gen.(type) {
case model.XdsDeltaResourceGenerator:
    res, deletedRes, logdata, usedDelta, err = g.GenerateDeltas(con.proxy, req, w)
case model.XdsResourceGenerator:
    res, logdata, err = g.Generate(con.proxy, w, req)
}
...
if usedDelta {
    resp.RemovedResources = deletedRes
} else if req.Full {
    // similar to sotw
    removed := w.ResourceNames.Copy()
    for _, r := range res {
        removed.Delete(r.Name)
    }
    resp.RemovedResources = sets.SortedList(removed)
}

结论:协议变体(走哪条 RPC)是代理的选择;Full/Incremental 是 istiod 每次推送时对”要不要重算”的判断。一个走 Delta 协议的代理,仍可能持续收到”全量生成再做差集”的响应体——只是包在 DeltaDiscoveryResponse 里。反过来,SotW 连接上的 EDS 也常年走 req.Full == false 的轻量路径。四个变体(Envoy 第 10 篇)与 Full/Incremental 是两根不同的轴,叠加在一起才是生产环境的真实矩阵。

flowchart LR
  subgraph transport ["Transport axis (client choice)"]
    sotw["StreamAggregatedResources"]
    delta["DeltaAggregatedResources"]
  end
  subgraph scope ["Push scope axis (istiod decision)"]
    full["PushRequest.Full = true"]
    inc["PushRequest.Full = false"]
  end
  sotw --> full
  sotw --> inc
  delta --> full
  delta --> inc

二、Full 推送:什么触发重算

pilot/pkg/xds/ads.go 里几处显式构造 Full: true 的地方,覆盖了三类触发源:

触发源 代码位置 语义
代理主动请求(如首次连接、重连) processRequest 构造 request := &model.PushRequest{Full: true, ...} 新建 watch,需要全量视角
调试接口全量重推 AdsPushAll(s *DiscoveryServer) 运维手动触发,Reason: DebugTrigger
单实例定向刷新(如 xds proxy 请求) ProxyUpdate 构造 Full: true, Forced: true 针对某个已知连接强制重算

AdsPushAll(req *model.PushRequest) 是分流点:

// pilot/pkg/xds/ads.go(Istio 1.30.3,节选)
func (s *DiscoveryServer) AdsPushAll(req *model.PushRequest) {
    if !req.Full {
        log.Infof("XDS: Incremental Pushing ConnectedEndpoints:%d Version:%s",
            s.adsClientCount(), req.Push.PushVersion)
    } else {
        totalService := len(req.Push.GetAllServices())
        log.Infof("XDS: Pushing Services:%d ConnectedEndpoints:%d Version:%s",
            totalService, s.adsClientCount(), req.Push.PushVersion)
        monServices.Record(float64(totalService))
    }
    s.StartPush(req)
}

运维可读的信号在这里XDS: Pushing Services:%d 只在 Full 分支打,日志里能看到当前 push_context 认为的 Service 总数;XDS: Incremental Pushing 分支没有 Service 计数,因为它压根没有重算 push context——这条分支通常对应纯 EDS 端点更新。


三、Debounce:把一堆 watch 事件折成一次推送

pilot/pkg/xds/discovery.godebounce 函数是所有 K8s watch 事件(Service、Endpoint、VirtualService……)进入 pushChannel 后的第一道闸:

// pilot/pkg/xds/discovery.go(Istio 1.30.3,节选,debounce)
type DebounceOptions struct {
    DebounceAfter time.Duration // 静默期:距上次变更多久才推
    debounceMax   time.Duration // 强制上限:即便一直有新事件也要推
    enableEDSDebounce bool
}

核心行为:每来一个事件,重置一个 DebounceAfter 计时器;只要事件持续到达且未超过 debounceMax,推送就被无限期推迟、事件被合并成一个 PushRequestConfigsUpdated 是个 set,天然去重)。稳定窗口打开时打一条日志:

log.Infof("Push debounce stable[%d] %d for reason %s: %v since last change, %v since last push, full=%v",
    pushCounter, debouncedEvents, reasonsUpdated(req), ...)

full=%v 直接把本节的判断结果打进日志——这是排障时判断”这一次是不是全量重算”最直接的入口,不需要猜。

有一个显式的例外分支:

if !opts.enableEDSDebounce && !r.Full {
    // 立即触发,不等 debounce 窗口
}

即:如果关闭了 EDS debounce(enableEDSDebounce=false),纯端点更新(!r.Full)会跳过合并窗口直接触发推送。这是一个吞吐 vs 延迟的显式开关——默认打开 EDS debounce 是为了在端点抖动时不把每一次 Pod 就绪/退出都单独推一次,代价是端点收敛多了一段 DebounceAfter 的延迟。

sequenceDiagram
  participant K8s as K8s watch events
  participant DB as debounce()
  participant Q as pushQueue
  K8s->>DB: Service/Endpoint/VS changed
  DB->>DB: reset DebounceAfter timer, merge ConfigsUpdated
  Note over DB: quietTime >= DebounceAfter or eventDelay >= debounceMax
  DB->>Q: emit merged PushRequest (full=?)
  Q->>Q: fan out to AllClients via pushQueue.Enqueue

四、端点抖动:EDS 压力落在哪一层

生产上”端点抖动”(滚动发布、HPA 伸缩、探针抖动导致 Pod 频繁 Ready/NotReady)对控制面的压力分两段:

  1. debounce 层enableEDSDebounce 打开时,短时间内多次端点变化被合并成一次 PushRequestConfigsUpdated 里塞进多个 kind.Endpoints key,不重复推送。
  2. push 层:即便合并后只推一次,AdsPushAll 仍要对每一个已连接客户端调用 pushConnection / pushConnectionDeltaProxyNeedsPush 会先判断这个代理是否真的关心这批变更(例如 Sidecar 作用域外的 Service 端点变化不需要推给它)——这是把”全网抖动”收窄成”关心该 Service 的代理集合”的关键过滤点,避免每次端点变化都对全网所有代理做无差别推送。

日志可见性上有个容易被忽略的盲区:pushXdspilot/pkg/xds/xdsgen.go)里,非 Full 推送打的是 log.Debugf,Full 推送才打 log.Infof

// pilot/pkg/xds/xdsgen.go(Istio 1.30.3,节选)
switch {
case !req.Full:
    if log.DebugEnabled() {
        log.Debugf("%s: %s%s for node:%s resources:%d size:%s%s", ...)
    }
default:
    log.Infof("%s: %s%s for node:%s resources:%d size:%v%s%s", ...)
}

也就是说:默认日志级别下,生产环境大量的纯 EDS 增量推送在 istiod 日志里几乎不可见,只有打开 debug 日志才能看到逐条 PUSH INC。运维照着默认 Info 日志判断”控制面是不是在疯狂推送”,在端点抖动场景下会得到错误的平静印象——这不是 bug,是日志分级的显式设计取舍,但排障时必须知道这个盲区存在。


五、争论与开放问题

争论 A B
EDS debounce 默认打开 减少端点抖动下的推送风暴,保护控制面 CPU 端点收敛多了一段可观的延迟,快速伸缩场景下”新 Pod 已 Ready 但还没吃到流量”的窗口变长
Full 推送重算 push context 的代价 语义简单,一次重算全网状态一致 Service 数量大时 GetAllServices() 与依赖图重算本身是 CPU 热点,Full 触发越频繁越明显
用 Info/Debug 分级隐藏 Incremental 日志 降低默认日志噪音 排障时低估真实推送频率,容易误判”控制面没做事”

开放问题(回指 Envoy 第 16 篇 §5.1):Envoy 侧的开放问题是”大规模 EDS 抖动下 SotW 带宽与 Incremental 复杂度谁先成为瓶颈”;Istio 生产侧的对应问题是debounce 窗口大小、enableEDSDebounce 开关与 ProxyNeedsPush 过滤精度三者如何联合调参,才能在”端点收敛延迟”与”控制面 CPU/带宽”之间找到一个可测量、可复现的最优点——这需要按具体集群的 Service 规模、Pod churn rate 与代理数实测,本篇不给未跑过的数字。


六、参考资料

源码(A)

规范 / 官方文档(A)

站内对照

实验台账


七、小结

  1. 协议变体(SotW/Delta)与推送范围(Full/Incremental)是两根正交轴:走 Delta 协议不代表每次都收到真正的差量语义,req.Full 才决定 istiod 是否重算全局状态。
  2. debounce 是端点抖动的第一道减震器DebounceAfter/debounceMax/enableEDSDebounce 三个参数共同决定”合并”还是”立即推”。
  3. full=%v 日志字段是排障时判断本次推送性质的最短路径,不需要靠猜。
  4. 默认日志级别下 Incremental 推送几乎不可见——这是设计取舍,不是故障,但会让运维低估真实推送频率。

系列目录 · 下一篇:NACK / Warming 责任

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .