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 能做地域感知路由、健康过滤等高级功能。
📝 举个例子:三层的具体值 Service = reviews.default.svc.cluster.local:9080(一个逻辑名)
→ 它当前有 3 个 IstioEndpoint10.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 上,或者要调用外部的 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,通过 XDSUpdaterSvcUpdate/EDSUpdate)通知 xDS 层触发推送(Day 09)。ConvertServiceconversion.go:48)是 K8s Service → model.Service 的核心翻译(ClusterIP=None → Passthrough headless 等)。
K8s APISvc/EndpointSlice informerwatch 到变化 ConvertService→ Service/Endpoint XDSUpdaterSvcUpdate/EDSUpdate → 触发推送(Day 09)
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 看到哪些配置)。
← Day 03 配置模型 Day 05 · PushContext →