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

【Istio 控制面】Service / Endpoint / Workload → CDS/EDS:istiod 如何拼出一个 Cluster

文章导航

分类入口
networkkubernetes
标签入口
#istio#istiod#cds#eds#endpointslice#workloadentry#service-registry#xds

目录

Envoy 数据面系列 已经讲清 Cluster 内部怎么按 priority、locality、subset 收窄主机集合;但那一篇的前提是「CDS 已经给出一个 Cluster,EDS 已经给出一份 ClusterLoadAssignment」。这两份资源从哪来——一个 K8s Service、一批 EndpointSlice、可能还有几条 WorkloadEntry,如何变成 sidecar 上按 outbound|9080||reviews.default.svc.cluster.local 命名的 Cluster?本文只钉这条翻译链路,不展开 subset、outlier、TLS context 的挂载方式——那是 DestinationRule 篇 的范围。

本文是「Istio / 网格控制面内核」系列第 5 篇。→ 系列目录

上接第 1–4 篇建立的 istiod 进程模型与推送图坐标系;下接 第 6 篇 VirtualService/HTTPRoute → RDS

版本锚定:Istio 1.30.3,源码路径对应 istio/istio 仓库 pilot/pkg/serviceregistry/kube/controller/pilot/pkg/xds/cds.gopilot/pkg/xds/eds.gopilot/pkg/networking/core/(函数签名以 master 分支验证,1.30.x 语义一致;未逐行核对 1.30.3 具体行号)。


一、两份资源,两条生成路径

CDS 与 EDS 是两个独立的 xDS 类型,各有生成器,但共享同一份底层服务模型:

资源 生成器 回答的问题
CDS(Cluster pilot/pkg/xds/cds.goCdsGenerator 有哪些上游逻辑组,用什么协议、LB 策略、发现类型
EDS(ClusterLoadAssignment pilot/pkg/xds/eds.goEdsGenerator 某个 Cluster 具体有哪些 endpoint

两者都不直接读 K8s API,而是读 istiod 内部维护的 model.PushContext / 服务注册聚合层——这一层才是本文重点:K8s Service、EndpointSlice、WorkloadEntry 如何先变成统一的 model.Servicemodel.IstioEndpoint,再喂给两个生成器。


二、聚合层:Service Registry 把三种来源拍平

istiod 支持多种服务注册来源(Kubernetes、ServiceEntry、外部注册中心),pilot/pkg/serviceregistry/aggregate 把它们聚合成统一视图。Kubernetes 来源的核心实现在 pilot/pkg/serviceregistry/kube/controller/controller.go

flowchart TD
  svc["K8s Service"] --> modelSvc["model.Service"]
  slice["EndpointSlice"] --> epCache["endpointSliceCache"]
  we["WorkloadEntry"] --> wiIndex["workloadInstancesIndex"]
  epCache --> pushEDS["endpointSliceController.pushEDS"]
  wiIndex -->|"collectWorkloadInstanceEndpoints"| pushEDS
  pushEDS -->|"EDSUpdate"| ds["DiscoveryServer"]
  modelSvc --> ds
  ds --> cds["CdsGenerator"]
  ds --> eds["EdsGenerator"]

关键函数是 endpointSliceController.pushEDSpilot/pkg/serviceregistry/kube/controller/endpointslice.go):它先从 endpointSliceCache 取出某个 hostname 的常规端点,若开启特性开关 EnableK8SServiceSelectWorkloadEntries,再调用 Controller.collectWorkloadInstanceEndpoints 把匹配该 Service selector 的 WorkloadEntry 端点追加进同一批 endpoint,一并调用 XDSUpdater.EDSUpdate 上报。也就是说:Service 选择器同时决定了「哪些 Pod」和「哪些 WorkloadEntry」进入同一个 Cluster 的候选端点集合,这一步是在聚合层完成的,CDS/EDS 生成器看到的已经是拍平后的端点列表,不区分来源。

这里有一处顺序约束:注释明确指出 pushEDS 要在持锁状态下串行执行,是为了避免 EDS 更新与 WorkloadEntry 更新乱序导致端点列表短暂不一致——这是「聚合多个来源」天然要付的代价,而不是 bug。


三、CDS:一个 Service 端口通常对应一个 Cluster

CdsGenerator.Generatepilot/pkg/xds/cds.go)先判断 cdsNeedsPush,需要推送时调用 ConfigGenerator.BuildClusters,逻辑在 pilot/pkg/networking/core/cluster.go / cluster_builder.go。核心规则:

outbound|9080||reviews.default.svc.cluster.local     # 无 subset 主 cluster
outbound|9080|v1|reviews.default.svc.cluster.local   # DestinationRule 派生的 subset cluster

CDS 增量:CdsGenerator.GenerateDeltas 目前只对 Service 变化计算增量(源码注释 // todo implement changes for DestinationRule, etc),DestinationRule 变化时仍走全量重建该服务相关的 Cluster——这是 1.30.3 时点上的已知实现边界,不是文档承诺的稳定行为。


四、EDS:为什么是「按 Cluster 拆」而不是「一次全推」

EdsGeneratorpilot/pkg/xds/eds.go)用注释里明确的设计取舍:用 hostname 为键的内存索引代替「每次遍历全部 endpoint 再转换」,端点在第一次发现时就转换成 model.IstioEndpoint,之后复用。buildEndpoints 会区分:

EDS 与 CDS 的订阅关系靠名字对齐:Cluster 的 eds_cluster_config.service_name 就是 EDS 请求里的 resource name;聚合层产出的 hostname 经过命名规则映射到这个名字上,任何一边名字算法不一致,都会表现成「Cluster 存在但 EDS 查不到端点」——这类问题排障时要先确认 CDS 里的 service_name 字段和 EDS 响应的资源名是否逐字相同,而不是先怀疑网络。


五、命名契约小结

由谁决定命名 后果
Service hostname K8s Service 的 <name>.<namespace>.svc.<domain> 拼错 namespace/域名会导致找不到对应 Cluster
Cluster 名 CDS:direction\|port\|subset\|hostname RDS 路由若引用了错误的 port/subset,会指向不存在的 Cluster(见第 6 篇资源顺序)
EDS 订阅名 Cluster 的 service_name,默认等于 Cluster 名去掉 subset 段 与聚合层 hostname 索引对不齐时端点为空

这条命名链和 Envoy xDS 资源树篇 讲的「LDS→RDS→CDS→EDS 靠字符串名字引用,不是外键级联」完全一致——Istio 只是在生产侧把这些名字算法钉死成上表的格式,消费侧(Envoy)不关心名字是谁生成的。


六、WorkloadEntry 的两处特殊性

WorkloadEntry 代表「没有 K8s Pod 生命周期,但要挂进某个 Service」的工作负载(典型场景:迁移到网格中的 VM)。它在这条链路里有两个不同于 Pod 的地方:

  1. 进入 EDS 的路径不同:Pod 端点来自 EndpointSlice watch;WorkloadEntry 端点来自 workloadInstancesIndex,需要显式开启 EnableK8SServiceSelectWorkloadEntries 才会被 collectWorkloadInstanceEndpoints 拉进同一个 Service 的候选池。关闭该开关时,WorkloadEntry 只能通过 ServiceEntryworkloadSelector 路径接入,而不是被 K8s Service selector 直接选中。
  2. 健康状态来源不同:Pod 依赖 kubelet 探活与 EndpointSlice 的 ready condition;WorkloadEntry 没有等价的探针基础设施,健康与否更依赖运维显式维护该资源,或配合 WorkloadGroup 的自动注册流程。生产上常见的故障模式是「WorkloadEntry 长期残留、进程已下线但端点仍在 EDS 里」——这是聚合层信任的输入本身过期,不是 CDS/EDS 生成逻辑的 bug。

七、争论与开放问题

谱系:Istio 的服务注册聚合层沿用了「多注册中心统一成一份内部模型再翻译」的思路,与 Envoy 自身把 xDS 拆成独立类型资源(Envoy 09)是同一代设计哲学在控制面侧的延伸:把「发现」这件事情做成可插拔的输入源,而不是让数据面直接感知 K8s API

争论:K8s Endpoints/EndpointSlice 默认每片最多 100 个端点(kube-controller-manager --max-endpoints-per-slice,最大可调到 1000),这是 Kubernetes 为了权衡「对象数量」与「单次 watch 事件体量」做的取舍。对 istiod 而言,一次滚动发布可能触发多个 EndpointSlice 连续变化,每次都会调用 pushEDS;istiod 用 PILOT_DEBOUNCE_AFTER / PILOT_DEBOUNCE_MAX 这两个特性开关把短时间内的多次变化合并成更少的推送批次。但去抖策略是否应该按 EndpointSlice 粒度感知,还是无差别对待所有配置变化,源码层面仍是统一的 debounce 队列,没有为端点抖动单独调参的一等入口——这一权衡与后续推送经济学直接相关,留给 生产侧 SotW/Delta 一篇 展开,本篇只指出输入侧的形状。

开放问题:多个注册来源(K8s、ServiceEntry、未来可能的其他 registry)对同一 hostname 给出冲突信息时(例如 K8s Service 存在但同名 ServiceEntry 也声明了该 host),聚合层的优先级规则决定了最终 Cluster/EDS 的内容,但这类冲突没有像 Gateway API 双轨 那样有官方文档明确排序——多数团队靠约定「不要对同一 hostname 同时用两种注册方式」规避,而不是依赖工具告警。


八、参考资料

源码(A)

官方文档(A)

站内

实验台账


九、小结

  1. K8s Service、EndpointSlice、WorkloadEntry 先在服务注册聚合层拍平成 model.Service / model.IstioEndpoint,CDS/EDS 生成器不直接感知来源差异。
  2. CDS 按「Service × 端口」生成 Cluster,命名遵循 direction|port|subset|FQDNEDS 按 hostname 维护内存索引,支持全量与部分推送两条路径。
  3. 两者靠 service_name 字符串对齐,不是外键约束——命名算法不一致是「有 Cluster 无端点」的高频原因。
  4. WorkloadEntry 走独立的索引与开关(EnableK8SServiceSelectWorkloadEntries),健康状态不像 Pod 一样有 kubelet 兜底。
  5. subset、outlier、TLS 挂载留给下一篇 DestinationRule;路由如何选中这里的 Cluster 名,见下一篇 VirtualService/HTTPRoute。

系列目录 · 下一篇:VirtualService/HTTPRoute → RDS

同主题继续阅读

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


By .