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

【Istio 控制面】NACK 与 Warming 归属:CDS→EDS→LDS→RDS 的顺序责任

文章导航

分类入口
networkkubernetes
标签入口
#istio#istiod#xds#nack#warming#ack#push-order#troubleshooting

目录

Envoy 第 10、11 篇 从消费侧钉死了两件事:NACK 只拒绝这一次协议更新,旧配置继续跑;ACK 之后还要过 warming,Route 本身不 warming,全靠控制面按 CDS→EDS→LDS→RDS 顺序推送来避免黑洞。这些结论都是”Envoy 会怎么做”。生产联调时真正的问题是:istiod 在 NACK 之后做了什么(还是什么都没做);istiod 是否真的按这个顺序推;以及”控制面已推送”这句话在 istiod 内部对应的是哪一行代码、覆盖到状态机的哪一步。

本文只回答控制面这一侧,把责任边界钉在 istiod 代码上,为第 15 篇的排障清单提供一个可复用的心智模型。

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

版本锚定:Istio 1.30.3;消费侧协议语义对齐 Envoy v1.39.0。无本机环境,不粘贴伪造 istioctl / config_dump 输出。


一、istiod 如何”看见”NACK

pkg/xds/server.goShouldRespond 是每条 DiscoveryRequest 进来后第一个判定函数,NACK 分支在最前面:

// pkg/xds/server.go(Istio 1.30.3,节选,ShouldRespond)
if request.ErrorDetail != nil {
    errCode := codes.Code(request.ErrorDetail.Code)
    log.Warnf("ADS:%s: ACK ERROR %s %s:%s", stype, id, errCode.String(), request.ErrorDetail.GetMessage())
    IncrementXDSRejects(request.TypeUrl, w.GetID(), errCode.String())
    w.UpdateWatchedResource(request.TypeUrl, func(wr *WatchedResource) *WatchedResource {
        wr.LastError = request.ErrorDetail.GetMessage()
        return wr
    })
    return false, emptyResourceDelta
}

三件事,逐条对应 Envoy 侧协议承诺:

  1. 打日志ADS:%s: ACK ERROR)并增加一个 Prometheus 计数器 pilot_total_xds_rejects(按 typenodeerr_code 打标签)。这是控制面唯一主动留下的可观测信号。
  2. 把错误信息记到 WatchedResource.LastError——这是每个连接内存里的状态,不落盘、不跨 istiod 重启存活。
  3. 返回 false:istiod 不会因为这次 NACK 而做任何补救推送。函数签名叫 ShouldRespond,NACK 分支的返回值就是”不需要响应”——控制面这一轮什么都不做。

责任边界在这里就分清楚了:istiod 对 NACK 的全部责任是”记录 + 计数”,不包含”重试”或”回滚”。旧配置继续服务,不是因为 istiod 主动保护了什么,而是因为 istiod 从未发起任何撤回动作——这与 Envoy 侧”NACK 不回滚已生效配置”是同一个事实的两个观察角度:没有任何一方在主动做恢复,双方都只是”不动”。要让这个连接吃到修复后的配置,必须等下一次真正的 PushRequest(配置变更触发新的一轮 debounce → push,见第 10 篇),NACK 本身不会触发重推。

sequenceDiagram
  participant Op as Operator
  participant Pilot as istiod
  participant Envoy as Envoy
  Op->>Pilot: apply broken config (e.g. bad DestinationRule)
  Pilot->>Envoy: DiscoveryResponse (new version)
  Envoy-->>Pilot: NACK error_detail
  Pilot->>Pilot: log.Warnf + IncrementXDSRejects + LastError
  Note over Pilot,Envoy: No retry. Envoy keeps old config. Pilot does nothing further.
  Op->>Pilot: fix config (real change)
  Pilot->>Envoy: new PushRequest, new version
  Envoy-->>Pilot: ACK

二、istiod 是否真的按 CDS→EDS→LDS→RDS 推

pilot/pkg/xds/ads.go 里有一个显式常量,回答了这个问题:

// pilot/pkg/xds/ads.go(Istio 1.30.3,节选)
// PushOrder defines the order that updates will be pushed in. Any types not listed here will be pushed in random
// order after the types listed here
var PushOrder = []string{
    v3.ClusterType,
    v3.EndpointType,
    v3.ListenerType,
    v3.RouteType,
    v3.SecretType,
    v3.AddressType,
    v3.WorkloadType,
    v3.WorkloadAuthorizationType,
}

watchedResourcesByOrder 用这个常量把某一个连接当前订阅的类型排序,pushConnection / pushConnectionDelta 再按这个顺序逐个调用 pushXds / pushDeltaXds

// pilot/pkg/xds/ads.go(Istio 1.30.3,节选,pushConnection)
wrl := con.watchedResourcesByOrder()
for _, w := range wrl {
    if err := s.pushXds(con, w, pushRequest); err != nil {
        return err
    }
}

这正是 CDS→EDS→LDS→RDS 顺序的源码落点,并且比 Envoy 协议文档写的四段多了三段:Secret(SDS)→ Address(Ambient waypoint/ztunnel 用的工作负载地址)→ Workload/WorkloadAuthorization。多客户端场景下(sidecar、waypoint、ztunnel)需要这些额外类型,见第 13 篇

要澄清一个容易误解的点:这个顺序是同一条 gRPC 流上依次发送的顺序,不是”等 CDS 被 ACK 之后才发 EDS”的同步等待——pushXds 是连续调用,中间没有等待响应。它保证的是 Envoy 侧收包的到达顺序(ADS 单流保序),而不是”上一类型确认生效”。这与 Envoy 第 10 篇的结论完全对应:ADS 降低多服务端抢序的难度,但顺序保证仅限于”同一控制面按什么顺序发”,不覆盖”对端处理到哪一步”


三、四层责任表:从 istiod 视角重画

Envoy 第 11 篇把”配置生效”拆成协议 / 依赖 / 快照 / 请求四层,全部站在 Envoy 内部状态机的角度。把镜头切到 istiod,会发现控制面只对第一层有观测能力

谁负责 istiod 能看到什么 istiod 看不到什么
协议(ACK/NACK) istiod 生成,Envoy 判定 ShouldRespond 里的 ErrorDetailpilot_total_xds_rejects 计数 无法区分”客户端还没处理”与”客户端已拒绝但还没发 NACK”
依赖(warming) Envoy 独占 无原生信号;只能通过代理下一次行为间接推断 Cluster/Listener 是否 warming 完成、是否卡在等 EDS/SDS
快照(Worker 切换) Envoy 独占,进程内部 具体哪个 Worker 线程用的是哪版快照
请求(长连接绑定旧路由) Envoy + 客户端应用 某个具体请求绑的是新路由还是旧路由

这张表是本篇最重要的结论:istiod 的职责边界在”发出去、且这次协议交互本身是否被接受”为止。“istiod 已推送”这句话最多只能担保到协议层——它甚至不担保 Envoy 已经处理完这次响应,因为 ACK 本身就只是”隔离校验通过并意图应用”(Envoy 第 10 篇)。warming、快照切换、请求绑定这三层,istiod 完全没有原生的可观测通道,只能靠数据面自己暴露的 /config_dump/clusters/stats 反推——这也是为什么第 15 篇的排障清单必须同时看 istioctl(问 istiod 端的推送/ACK 状态)和 Envoy admin(问数据面端的实际生效状态),单看一边都不完整。

flowchart TD
  subgraph pilot ["istiod visibility"]
    ack["Protocol: ACK / NACK<br/>ShouldRespond + pilot_total_xds_rejects"]
  end
  subgraph envoy ["Envoy-only visibility"]
    warm["Dependency: Cluster/Listener warming"]
    snap["Snapshot: Worker TLS config swap"]
    req["Request: in-flight route binding"]
  end
  ack -.->|"opaque boundary"| warm
  warm --> snap
  snap --> req

四、Make-before-break:istiod 侧的落地方式

Envoy 协议文档给出的顺序建议(CDS → EDS → LDS → RDS →(再删陈旧 CDS/EDS))在 istiod 里不是靠”手动排队等确认”实现的,而是靠两个机制的组合:

  1. PushOrder 常量保证同一连接上,新增/更新类的资源总是先发 Cluster/Endpoint,后发 Listener/Route——新 Cluster 大概率先于引用它的 Route 到达 Envoy 侧的 warming 队列。
  2. 删除类操作没有对称的”先后”保证PushOrder 只管发送顺序,不管”哪些资源该在这一批里被删除”。如果一次配置变更让 RDS 指向的 Cluster 在同一批推送里被删除(例如误删 DestinationRule 导致的 subset cluster 消失),PushOrder 里 Cluster 排在 Route 前面并不能阻止”新 Route 引用的 Cluster 恰好没在这批里”这种时序错位——这仍然是 Envoy 第 11 篇说的”控制面自己排序责任”,PushOrder 只解决了”同批内先发哪个类型”,没有解决”跨批次的资源生命周期编排”。

因此,make-before-break 在 Istio 里更准确的表述是:istiod 用固定的类型发送顺序降低了单批推送内的短暂黑洞概率,但跨资源生命周期的一致性(新 Cluster 必须先于引用它的 Route 存在)仍然依赖 istiod 自身的配置翻译逻辑是否正确构造了这一批 ConfigsUpdated,而不是靠 PushOrder 本身兜底。


五、争论与开放问题

争论 A B
NACK 后 istiod 完全被动 简单、无重试风暴风险 排障者容易忽略”配置一直坏着”,需要额外告警在 pilot_total_xds_rejects
PushOrder 用固定顺序而非等待 ACK 再发下一类型 吞吐更高,不被慢客户端拖住整条推送流水线 无法保证”上一类型已被处理”,一致性责任转嫁给配置翻译层
istiod 不感知 warming 职责边界清晰,控制面不需要理解数据面内部状态机 排障时控制面侧和数据面侧的”已完成”定义不一致,容易产生误判发布门禁

开放问题(承接第 10 篇Envoy 第 15 篇):如果要建立一个”配置已真正生效”的 SLI,需要把 pilot_total_xds_rejects(控制面侧)与数据面 warming/ACK 信号做关联,但两者目前分别暴露在 istiod 和 Envoy 各自的指标体系里,没有统一的 trace id 或关联键把同一次配置变更的控制面推送与数据面生效状态串起来——这是否应该由控制面主动生成、数据面透传,还是留给外部可观测性平台拼接,目前没有权威结论。


六、参考资料

源码(A)

规范 / 官方文档(A)

站内对照

实验台账


七、小结

  1. istiod 对 NACK 的响应只有”记录 + 计数”LastError 写进内存,pilot_total_xds_rejects 加一,不重试、不回滚。
  2. PushOrder 是 make-before-break 建议在 istiod 里的真实源码落点,但它只管同批推送内的类型发送顺序,不管跨批次的资源生命周期编排。
  3. istiod 的可观测边界止步于协议层;warming、快照切换、请求绑定三层完全是 Envoy 内部状态,控制面没有原生信号。
  4. 排障必须两端对齐pilot_total_xds_rejects 之类的控制面信号回答”有没有被拒绝”,Envoy admin 回答”有没有真的在服务”——两个问题答案可以不一致,且经常不一致。

系列目录 · 上一篇:生产侧 SotW / Delta · 下一篇:多集群东西向发现扇出

同主题继续阅读

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

2026-08-11 · network / kubernetes

Istio / 网格控制面内核:从 CRD 到 xDS

补齐 Envoy 数据面 xDS 消费侧与 Mesh 选型叙事之间的控制面内核:istiod 推送图、CRD/HTTPRoute 翻译、Ambient 多客户端、NACK/warming 责任与排障对齐。


By .