Day 04 / 共 20 天 · 第 1 周 控制面全景
服务注册
istiod 要生成"路由到哪个后端"的配置,前提是知道集群里有哪些服务、每个服务有哪些实例(端点)。今天看 ServiceDiscovery 抽象、Service/Endpoint 模型,以及 K8s 和 ServiceEntry 两个注册表。
📍 你在控制面 istiod 之旅的位置(工单看懂了,还得知道"后端有哪些机器可派"——服务发现)
D1 全景·
D2 启动·
D3 配置CRD·
D4 服务注册·
D5 快照·
D6 xDS服务·
D7 生成·
D8 Builder·
D9 推送·
D10 连接
L01
为什么要服务发现
🤔 痛点:Pod 的 IP 天天变,路由怎么写死?
你想把流量导到 reviews 服务,可它的 3 个 Pod 昨天是
10.1.1.5/6/7,今天扩容+重启后变成 10.1.2.9/11/…。IP 一直漂,配置里写死任何 IP 明天就失效。💡 本质:服务发现是一本"实时更新的员工通讯录"
好比公司通讯录:员工(Pod)入职离职调岗天天变,但你只要记住"部门名(服务名 reviews)",前台(服务发现)随时告诉你这个部门现在有哪些人、分机是多少。istiod 让前台盯着 K8s,人一变就更新通讯录,再把最新名单(端点)通过 EDS 发给各门店保安(Envoy)。
服务发现 = "维护一张实时的服务电话簿"
集群里服务的实例(Pod)随时在增删(扩缩容、发布、故障)。istiod 要给 Envoy 下发"reviews 服务当前有哪些 IP:Port",就必须实时知道这张"电话簿"。服务发现(ServiceDiscovery)就是维护这张簿的机制——watch K8s,服务/Pod 一变就更新。这是 Day 01 三段式里"配置摄取"的另一半(配置摄取 = ConfigStore 读 CRD + ServiceDiscovery 读服务)。没有准确的服务发现,Envoy 就会把流量发到不存在的实例(黑洞)。
L02
Service 模型
// pilot/pkg/model/service.go:74 —— 平台无关的服务抽象
type Service struct {
Hostname host.Name // :90 FQDN 主键(如 reviews.default.svc.cluster.local)
Ports PortList // :81 服务端口
ClusterVIPs AddressMap // :94 多集群:同 host 各集群不同 ClusterIP
Resolution Resolution // :120 ClientSideLB(EDS)/DNSLB/Passthrough…
Attributes ServiceAttributes // :77 registry/labels/exportTo/selector…
}
读法:
Service 是"平台无关"的服务抽象——K8s Service、ServiceEntry 都转成它。Hostname 是主键。Resolution 决定端点怎么解析(呼应 Envoy 课 Day 15 的服务发现类型)。多集群支持体现在 ClusterVIPs(同一 host 在不同集群有不同 VIP)。L03
IstioEndpoint
// pilot/pkg/model/service.go:522 —— 最细粒度的端点
type IstioEndpoint struct {
Addresses []string // :541 双栈 IP
ServiceAccount string // :551 mTLS 身份
Network string // :554 网络(多网络支持)
Locality Locality // :557 地域/可用区(就近路由)
EndpointPort uint32 // :561 工作负载实际端口
TLSMode string // :567 istio/disabled
HealthStatus HealthStatus // :585 Healthy/Draining/Terminating
}
Service vs Endpoint vs Instance
Service = 逻辑服务(reviews);IstioEndpoint = 一个具体实例(某个 Pod 的 IP:Port + 元数据);ServiceInstance = "一个 endpoint × 一个服务 × 一个端口"的组合。Envoy 课的 EDS 下发的就是这些 endpoint(转成 ClusterLoadAssignment)。注意 endpoint 带了丰富元数据:ServiceAccount(用于 mTLS 身份,Day 14)、Locality(就近路由)、HealthStatus(剔除不健康的)。这些元数据让 Istio 能做地域感知路由、健康过滤等高级功能。📝 举个例子:三层的具体值
→ 它当前有 3 个
→ istiod 生成 EDS 时会把 Draining 那个剔除,只把两个 Healthy 端点塞进
Service = reviews.default.svc.cluster.local:9080(一个逻辑名)→ 它当前有 3 个
IstioEndpoint:10.1.2.9:9080(v1,az-a,Healthy)、10.1.2.11:9080(v2,az-b,Healthy)、10.1.2.14:9080(v1,az-a,Draining)→ istiod 生成 EDS 时会把 Draining 那个剔除,只把两个 Healthy 端点塞进
ClusterLoadAssignment 发给 Envoy。L04
ServiceDiscovery 接口
// pilot/pkg/model/service.go:917
type ServiceDiscovery interface {
Services() []*Service // :921 所有服务
GetService(hostname) *Service // :924
GetProxyServiceTargets(*Proxy) []ServiceTarget // :943 某 proxy 同机的目标
GetProxyWorkloadLabels(*Proxy) labels.Instance // :944
}
读法:又是"接口 + 多实现":
ServiceDiscovery 抽象"怎么发现服务",K8s/ServiceEntry 各是一个实现。GetProxyServiceTargets 很关键——它返回"和某个 Envoy 同机的服务",用于生成入站配置(这个 Envoy 代理哪些本地服务)。Day 01 说的 istiod 两大数据接口之一(另一个是 ConfigStore)。L05
聚合注册表
pilot/pkg/serviceregistry/aggregate/controller.go:42 把多个注册表聚合:
// addRegistry(:199):K8s 注册表插到非 K8s 之前(K8s 服务优先)
// Services(:296):非 K8s 直接追加;K8s 按 hostname 去重、跨集群合并 ClusterVIPs(mergeService :358)
// HasSynced(:461):全部注册表都同步才 true
为什么要"聚合"多个注册表?
服务不一定都在 K8s 里——有的是 VM、有的是外部服务(通过 ServiceEntry 手工登记)、多集群场景还有多个 K8s。聚合注册表(aggregate)把这些来源"合并成一张统一的电话簿":查服务时它扇出到所有注册表、合并结果。K8s 注册表优先级最高(防外部登记抢占域名)。istiod 上层只跟聚合注册表打交道,不用管服务到底来自哪。这又是"统一抽象"——用一个聚合层屏蔽多来源的差异。
💬 对话体
👶 小白:K8s 里已经有服务了,为什么还需要 ServiceEntry 注册表?
👨🏫 老师:因为不是所有后端都在 K8s 里。比如你的数据库跑在集群外的一台 VM 上,或者要调用外部的
👶 小白:万一两边有同名服务呢?
👨🏫 老师:K8s 优先(
👨🏫 老师:因为不是所有后端都在 K8s 里。比如你的数据库跑在集群外的一台 VM 上,或者要调用外部的
api.stripe.com——K8s 根本不认识它们。ServiceEntry 就是"手工往通讯录里加一条外部联系人"。聚合注册表把 K8s 的自动名单 + ServiceEntry 的手工名单合成一本,上层查询时感觉不到区别。👶 小白:万一两边有同名服务呢?
👨🏫 老师:K8s 优先(
addRegistry 把它插在前面),防止外部登记的域名"冒名顶替"集群内真实服务。L06
K8s 注册表
// pilot/pkg/serviceregistry/kube/controller/controller.go:217
// 用 informer watch Service/EndpointSlice/Pod
// onServiceEvent(:461)→ ConvertService(conversion.go:48)→ XDSUpdater.SvcUpdate
// endpointslice.go:269 updateEndpointCacheForSlice:EndpointSlice → IstioEndpoint
// → pushEDS(:451)→ XDSUpdater.EDSUpdate
读法:K8s 注册表用 informer(K8s 的 watch 机制)监听 Service/EndpointSlice/Pod 变化,转成 Istio 的 Service/IstioEndpoint,通过
XDSUpdater(SvcUpdate/EDSUpdate)通知 xDS 层触发推送(Day 09)。ConvertService(conversion.go:48)是 K8s Service → model.Service 的核心翻译(ClusterIP=None → Passthrough headless 等)。K8s 里服务/Pod 一变,informer 感知 → 翻译成 Istio 模型 → 通过 XDSUpdater 通知 xDS 层去推送。
L07
ServiceEntry 注册表
pilot/pkg/serviceregistry/serviceentry/controller.go:101:把 ServiceEntry CRD(Day 03)转成服务/端点。一个 SE → 多个 Service(每 host 一个)。
两个注册表怎么"互喂"?
有个巧妙设计:K8s 注册表和 ServiceEntry 注册表互相通知对方的工作负载——K8s 的 Pod 派生出 WorkloadInstance 通知 ServiceEntry(让带 WorkloadSelector 的 SE 能选中 Pod);SE 的 WorkloadEntry(VM)通知 K8s(让 K8s Service 能选中 VM)。于是"K8s Service 选中 VM""ServiceEntry 选中 Pod"这种混合场景都能工作。这是 Istio 支持"K8s + VM 混合网格"的关键——服务发现不局限于 K8s。IP 自动分配(
autoAllocateIPs)给无地址的外部服务分配确定性虚拟 IP。L08
今日小结 + 动手
🧠 今天你应该能回答
- 服务发现解决什么?和 ConfigStore 什么关系?
- Service / IstioEndpoint / ServiceInstance 的区别?
- ServiceDiscovery 接口的核心方法?
- 聚合注册表为什么必要?K8s 为什么优先?
- K8s 注册表怎么工作?两个注册表怎么互喂?
✋ 动手
cd /Users/bitmart/work/codes/github/higress-group/istio
grep -n 'type Service struct\|type IstioEndpoint struct\|type ServiceDiscovery interface' pilot/pkg/model/service.go
grep -n 'func ConvertService' pilot/pkg/serviceregistry/kube/controller/conversion.go
ls pilot/pkg/serviceregistry/
明天预告 · Day 05(第1周收官):PushContext——istiod 把所有配置和服务预建成一个"全量快照"(PushContext),供给每个 proxy 快速生成 xDS。以及 Sidecar 作用域(决定 proxy 看到哪些配置)。