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.go、pilot/pkg/xds/eds.go、pilot/pkg/networking/core/(函数签名以 master 分支验证,1.30.x 语义一致;未逐行核对 1.30.3 具体行号)。
一、两份资源,两条生成路径
CDS 与 EDS 是两个独立的 xDS 类型,各有生成器,但共享同一份底层服务模型:
| 资源 | 生成器 | 回答的问题 |
|---|---|---|
CDS(Cluster) |
pilot/pkg/xds/cds.go 的
CdsGenerator |
有哪些上游逻辑组,用什么协议、LB 策略、发现类型 |
EDS(ClusterLoadAssignment) |
pilot/pkg/xds/eds.go 的
EdsGenerator |
某个 Cluster 具体有哪些 endpoint |
两者都不直接读 K8s API,而是读 istiod 内部维护的
model.PushContext /
服务注册聚合层——这一层才是本文重点:K8s
Service、EndpointSlice、WorkloadEntry 如何先变成统一的
model.Service 和
model.IstioEndpoint,再喂给两个生成器。
二、聚合层:Service Registry 把三种来源拍平
istiod
支持多种服务注册来源(Kubernetes、ServiceEntry、外部注册中心),pilot/pkg/serviceregistry/aggregate
把它们聚合成统一视图。Kubernetes 来源的核心实现在
pilot/pkg/serviceregistry/kube/controller/controller.go:
Controllerwatchv1.Service,产出model.Service(hostname、端口、ExportTo、ServiceRegistry等属性)。endpointSliceController(endpointslice.go)watchv1.EndpointSlice,按 hostname 维护endpointSliceCache,转成[]*model.IstioEndpoint。WorkloadEntry不走 EndpointSlice:Controller维护workloadInstancesIndex,在WorkloadInstanceHandler里按事件插入/删除,代表手工登记的非 K8s 工作负载(VM、裸机进程)。
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.pushEDS(pilot/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.Generate(pilot/pkg/xds/cds.go)先判断
cdsNeedsPush,需要推送时调用
ConfigGenerator.BuildClusters,逻辑在
pilot/pkg/networking/core/cluster.go /
cluster_builder.go。核心规则:
- 默认按「每个 Service 的每个端口」生成一个 outbound
Cluster,命名遵循
direction|port|subset|FQDN契约(无 subset 时该字段为空),例如outbound|9080||reviews.default.svc.cluster.local。 - Cluster
的发现类型(
type)取决于服务的解析模式:K8s ClusterIP 服务默认走 EDS(具体端点由第四节的 EDS 单独下发);ServiceEntry若声明了静态地址则可能是STATIC/STRICT_DNS;ORIGINAL_DST用于 passthrough/透传场景。 - 若该 Service 有
DestinationRule,applyDestinationRule还会派生出额外的 subset Cluster(outbound|9080|v1|...)——但那是第 7 篇的内容,本篇只需知道主 Cluster 与 subset Cluster 都在这一步一起产出,共享同一份服务与端口输入。
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 拆」而不是「一次全推」
EdsGenerator(pilot/pkg/xds/eds.go)用注释里明确的设计取舍:用
hostname 为键的内存索引代替「每次遍历全部 endpoint
再转换」,端点在第一次发现时就转换成
model.IstioEndpoint,之后复用。buildEndpoints
会区分:
- 全量 EDS:
req.Full且没有可用的部分推送信号时,按w.ResourceNames(该代理订阅的 Cluster 名集合)逐个查内存索引出ClusterLoadAssignment。 - 部分 EDS(partial push):当这次推送的
ConfigsUpdated只包含ServiceEntry/Endpoints类变化时,canSendPartialFullPushes允许只重算受影响的 hostname,而不触碰其余 Cluster——源码注释特别强调:这个前提是「只有 Service 变了,才能保证只有对应的 CLA 变了」;多网络场景下一次 Service 变化会触发ConfigsUpdated=ALL,这时就不能走这条捷径。
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 的地方:
- 进入 EDS 的路径不同:Pod 端点来自
EndpointSlicewatch;WorkloadEntry 端点来自workloadInstancesIndex,需要显式开启EnableK8SServiceSelectWorkloadEntries才会被collectWorkloadInstanceEndpoints拉进同一个 Service 的候选池。关闭该开关时,WorkloadEntry 只能通过ServiceEntry的workloadSelector路径接入,而不是被 K8s Service selector 直接选中。 - 健康状态来源不同: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)
istio/istio:pilot/pkg/xds/cds.go(CdsGenerator.Generate/GenerateDeltas)、pilot/pkg/xds/eds.go(EdsGenerator、EDSUpdate、buildEndpoints)。istio/istio:pilot/pkg/serviceregistry/kube/controller/controller.go(Controller、WorkloadInstanceHandler)、endpointslice.go(endpointSliceController.pushEDS、collectWorkloadInstanceEndpoints)。istio/istio:pilot/pkg/networking/core/cluster.go、cluster_builder.go(BuildClusters、applyDestinationRule的服务/端口枚举部分)。
官方文档(A)
- Istio 1.30 文档,Traffic Management 概览(Service 与 DestinationRule 的关系边界)。
- Istio 1.30 文档,Debugging Envoy and
Istiod(
istioctl proxy-config clusters输出格式与direction|port|subset|FQDN命名)。 - Kubernetes 文档,EndpointSlices(默认每片 100
端点上限与
--max-endpoints-per-slice)。
站内
- Envoy / Cluster 与负载均衡 · Envoy / xDS 资源树
- 第 6 篇:VirtualService/HTTPRoute → RDS · 第 7 篇:DestinationRule
- 系列目录
实验台账
- 本篇不粘贴
istioctl proxy-config实跑输出;Cluster 命名格式与字段引用官方文档与源码函数签名,未在本机集群核对具体行号。
九、小结
- K8s Service、EndpointSlice、WorkloadEntry
先在服务注册聚合层拍平成
model.Service/model.IstioEndpoint,CDS/EDS 生成器不直接感知来源差异。 - CDS 按「Service × 端口」生成
Cluster,命名遵循
direction|port|subset|FQDN;EDS 按 hostname 维护内存索引,支持全量与部分推送两条路径。 - 两者靠
service_name字符串对齐,不是外键约束——命名算法不一致是「有 Cluster 无端点」的高频原因。 - WorkloadEntry
走独立的索引与开关(
EnableK8SServiceSelectWorkloadEntries),健康状态不像 Pod 一样有 kubelet 兜底。 - subset、outlier、TLS 挂载留给下一篇 DestinationRule;路由如何选中这里的 Cluster 名,见下一篇 VirtualService/HTTPRoute。
→ 系列目录 · 下一篇:VirtualService/HTTPRoute → RDS
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Istio 控制面】生产侧 SotW 与 Delta:debounce、全量推与端点抖动
拆解 istiod 生产侧的 Full/Incremental 推送决策与 debounce 合并逻辑,说明它与 Envoy 消费侧 SotW/Delta 协议变体是两个正交问题;用 pilot/pkg/xds 源码钉住端点抖动下的日志可见性盲区。
【Istio 控制面】控制面全景:从 CRD 到 xDS 的翻译与推送内核
定位 Istio 控制面内核相对 Envoy 消费侧、Service Mesh 税文与 Gateway API 资源模型的缺口;给出五条坐标系、16 篇地图与本系列明确不写的范围,钉住 istiod 1.30.3 为主线。
【Istio 控制面】DestinationRule:Cluster 之上的 subset、outlier 与 TLS 挂载
拆解 DestinationRule 如何在 CDS 已生成的 Cluster 之上派生 subset、挂载 connectionPool/outlierDetection/loadBalancer/tls,以及 mesh 默认、DestinationRule 级、port 级、subset 级四层设置的真实合并规则——其中 port 级与 subset 级的继承语义并不对称。
【Istio 控制面】NACK 与 Warming 归属:CDS→EDS→LDS→RDS 的顺序责任
用 pilot/pkg/xds 的 PushOrder 与 ShouldRespond 源码钉住 istiod 推送顺序与 NACK 处理边界,说明为什么'istiod 已推送'与'Envoy 在服务'之间的责任划分不在同一层,给出排障用的责任表。