Cilium ClusterMesh 中的 Multi-Cluster Services API(MCS-API)实现深度解析
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
Multi-Cluster Services API(MCS-API)是 Kubernetes 社区定义的、将单一集群的 Service 概念扩展到多集群场景的标准 API。本文以 pkg/clustermesh/mcsapi/README.md 为骨架,结合 Cilium 仓库中pkg/clustermesh/mcsapi目录下的控制器源码、类型定义与测试,深入讲解 Cilium 如何通过三个 reconciler 控制器、一个集群间共享 store 以及派生 Service 机制,把 MCS-API 的ServiceExport/ServiceImport资源无缝接入既有 ClusterMesh 数据面。读完本文,你将掌握 MCS-API 在 Cilium 中的完整数据流、冲突解决策略、派生 Service 命名规则、启用方式与可观测手段,能够独立排查多集群服务导入/导出问题。
MCS-API 是什么:从 Service 到多集群服务
MCS-API(Multi-Cluster Services API)是一套标准 API,用于将 Kubernetes 的 Service 概念扩展至多个集群。它建立在两个新的 CRD 之上:
- ServiceExport:一个轻量资源,标记同命名空间、同名的 Service 需要被导出到其他集群。它本身不包含任何服务规格,仅仅是一个"导出意图"声明。
- ServiceImport:完全由实现方(在 Cilium 中由一个 controller 管理)维护的资源,表示所有被导出 Service 的聚合视图,通常由导入方集群(本地集群)消费。
在 Cilium 的代码库中,这两个 CRD 的启用、安装与校验集中在 cell.go:控制器启动时会检查serviceimports与serviceexports两个 GroupVersionKind 对应的 CRD 是否已存在,若未安装且配置要求自动安装,则通过 crdinstall.go 中基于 mcs-api 官方预生成 CRD 二进制(mcsapicrd.ServiceImportCRD/mcsapicrd.ServiceExportCRD)的createCustomResourceDefinitions完成创建与版本升级。
注:KEP-1645(Multi-Cluster Services API 的增强提案)定义了该 API 的语义与约束,包括下文会提到的冲突解决规则;本仓库的
conformance子目录还提供了对 MCS-API 的符合性测试入口(见 conformance_test.go 的TestConformance)。
Cilium 中的实现架构:共享 Store + 三个控制器
Cilium 对 MCS-API 的实现并不像某些实现那样在各集群之间直接读写对方 API Server,而是复用了 ClusterMesh 自身的跨集群同步通道:集群间通过 kvstore(ClusterMesh API Server)交换信息。
整体架构由以下几部分组成:
MCSAPIServiceSpec结构体:承载被导出服务(同名的 Service + ServiceExport)的字段,定义在 types/mcsapiservicespec.go。- 集群间同步:
MCSAPIServiceSpec像其他 ClusterMesh 结构体一样被同步到 ClusterMesh API Server,各集群再从中拉取远端信息。 mcsAPIServiceImportReconciler控制器:从所有集群拉取 Service 导出信息,重构出本地的ServiceImport资源。mcsAPIServiceReconciler控制器:根据ServiceImport创建名为derived-$hash的内部 Service,以触发普通 global Service 的既有机制(ClusterIP 生成、远端 Endpoint 同步到 BPF Map、EndpointSliceSync 等),并把派生 Service 的 IP 回写到ServiceImport。mcsAPIEndpointSliceMirrorReconciler控制器:把本地 Service 的 EndpointSlice 镜像到派生 Service,确保本地端点能被远程集群访问。- DNS 支持:依赖 multicluster CoreDNS 插件,该插件查询
ServiceImport与 EndpointSlice 资源来完成跨集群 DNS 解析。
下图完整展示了这一数据流(源自 README 中的流程图):
核心数据结构:MCSAPIServiceSpec与 kvstore 同步
MCSAPIServiceSpec是 Cilium 内部用于跨集群传递"被导出服务"信息的载体,定义在 types/mcsapiservicespec.go,其字段完整覆盖了 MCS-API 关心的服务属性:
| 字段 | 类型 | 说明 |
|---|---|---|
Cluster | string | 服务所在集群名称 |
Name/Namespace | string | 对应 ServiceExport / ServiceImport 资源的名称与命名空间 |
Annotations/Labels | map | 从 ServiceExport 的spec.exportedAnnotations/spec.exportedLabels复制而来 |
ExportCreationTimestamp | metav1.Time | ServiceExport 的创建时间,用于冲突解决(谁更早谁优先) |
Ports | []mcsapiv1beta1.ServicePort | 以 MCS API 格式表示的 Service 端口列表 |
Type | ServiceImportType | 只能是ClusterSetIP或Headless |
SessionAffinity | corev1.ServiceAffinity | 仅支持ClientIP与None(默认),Headless 类型时忽略 |
SessionAffinityConfig | *corev1.SessionAffinityConfig | 会话保持配置(如 ClientIP 超时时间) |
IPFamilies | []corev1.IPFamily | ServiceImport 分配的 IP 协议族 |
InternalTrafficPolicy | *ServiceInternalTrafficPolicy | Local或Cluster(默认),控制节点上 ClusterIP 流量的分发范围 |
TrafficDistribution | *string | 流量分发偏好,如PreferClose、PreferSameZone、PreferSameNode |
该结构体通过 kvstore 共享 store 跨集群同步,store 前缀为ServiceExportStorePrefix,即kvstore.BaseKeyPrefix/state/serviceexports/v1(见 mcsapiservicespec.go),且被标记为STABLE API——修改其结构或 key 格式会破坏向后兼容性。每个对象的 kvstore key 由GetKeyName()生成,格式为cluster/namespace/name。
值得注意的细节:
- 反序列化校验:
Unmarshal会先做 JSON 解析,再调用validate()检查各必填字段与枚举合法性(如 Type 必须为ClusterSetIP或Headless、SessionAffinity 必须为ClientIP或None、InternalTrafficPolicy / TrafficDistribution 取值必须合法),见 mcsapiservicespec.go。 - 额外校验器:
ValidatingMCSAPIServiceSpec支持注入ClusterNameValidator(校验 cluster 字段与预期一致)和NamespacedNameValidator(校验 key 与 NamespacedName 匹配),防止跨集群数据串扰,见 mcsapiservicespec.go。 - 本地服务转换:
FromCiliumServiceToMCSAPIServiceSpec负责把本地的 slim Service + ServiceExport 转换为MCSAPIServiceSpec;其中ClusterIP: None的服务会被判定为Headless类型,否则为ClusterSetIP(见 mcsapiservicespec.go)。 - 导出合法性检查:
CheckLocalSvcValidForExport明确拒绝ExternalName类型的 Service 导出,返回ServiceExportReasonInvalidServiceType;其 slim 版本CheckLocalSlimSvcValidForExport逻辑必须与其保持一致(源码注释中明确要求两者同步维护),见 mcsapiservicespec.go。
远端数据的订阅由serviceExportObserver完成(见 observer.go):它会根据远端集群的CiliumClusterConfig能力声明决定是否启用——只有远端集群声明支持ServiceExportsEnabled且本地启用了 MCS-API 时才注册 watch;否则会 Drain 掉已有数据并记录警告"Remote cluster does not support MCS-API service export resources"。每个远端服务的更新/删除事件都会同步写入本地的globalServiceExports缓存,并通过RemoteObjectSource触发控制器。
控制器一:mcsAPIServiceImportReconciler——导入与冲突解决
这是 MCS-API 实现中最核心的控制器,实现在 serviceimport_controller.go。它的职责是:根据来自远端集群(经 kvstore)与本地的全部 ServiceExport(及对应 Service),自动创建/更新ServiceImport资源,同时维护本地ServiceExport的状态条件。
触发源
控制器通过SetupWithManager注册对以下资源的 watch(见 serviceimport_controller.go):
ServiceImport(主资源,For);ServiceExport的变化;Service的变化;Namespace的变化(用于在命名空间标签/配置变化时重排队导入导出);- 远端服务的 raw source(来自共享 store 的远端事件)。
命名空间门控
每个命名空间是否"可导出/可导入"取决于isNamespaceGlobal判断(见 serviceimport_controller.go),其依据是cmnamespace.IsGlobalNamespace与GlobalNamespacesByDefault配置。若命名空间不是 global 而本地存在 ServiceExport,控制器会写入Valid=False(reason 为NamespaceNotGlobal)与Ready=False条件;若命名空间不是 global,ServiceImport的Ready条件也会被置为False(reason 同样为NamespaceNotGlobal),见 serviceimport_controller.go。
冲突解决策略:导入时处理
README 特别强调了一个与部分其他实现不同的设计决策:Cilium 在导入时(import time)处理冲突,而不是在导出方检查。原因在于 Cilium 的导出路径是"把自己的信息更新到本地 ClusterMesh API Server",而导入路径是"聚合所有 ClusterMesh API Server 的信息"——冲突只有在聚合时才能被完整发现。不过冲突信息仍会以条件形式回写到导出方的ServiceExport对象上。
冲突解决的基准规则是:按ExportCreationTimestamp从旧到新排序,以最旧(最早创建)的 ServiceExport 为准。排序函数orderSvcExportByPriority在时间戳相同时还会按集群名排序,保证结果确定性(见 serviceimport_controller.go)。
具体冲突检查分三个层次:
- 端口合并
mergePorts(serviceimport_controller.go):按端口号+协议合并所有导出的端口;若出现同名端口定义不一致、同名端口冲突(appProtocol不一致)或某集群端口与最旧导出的端口集合不匹配,则记录ServiceExportReasonPortConflict,并采用最旧导出方的端口定义。 - IP 协议族交集
intersectIPFamilies(serviceimport_controller.go):对所有导出的 IPFamilies 取交集而非并集——因为期望所有 Pod 在导出的每个 IP 族都有端点,交集能保证客户端无论用哪个 IP 协议都能到达全部 Pod;若取并集,可能只能到达 Pod 的一个子集。若某集群与现有交集无公共 IP 族,会跳过该集群并报告ServiceExportReasonIPFamilyConflict。为空 IPFamilies 的导出会被跳过,以兼容 Cilium 1.18 及更早版本。 - 其余字段一致性
checkConflictExport(serviceimport_controller.go):对type、sessionAffinity、sessionAffinityConfig.clientIP、annotations、labels、internalTrafficPolicy、trafficDistribution七个字段逐一比对,任一字段有集群与最旧导出不一致,即返回对应 reason(如TypeConflict、SessionAffinityConflict、AnnotationsConflict等),消息形如Conflicting <field>. N/M clusters disagree. Using "<value>" from oldest service export in cluster "<cluster>".
ServiceImport 的生成与状态回写
合并后的结果会写入ServiceImport.Spec(Ports、IPFamilies、Type、SessionAffinity、InternalTrafficPolicy、TrafficDistribution),并以最旧导出方的 Labels/Annotations 为准;同时会设置annotation.SupportedIPFamilies内部注解,记录本地集群实际支持的 IP 族(filterSupportedIPFamilies会根据本地是否启用 IPv4/IPv6 过滤,见 serviceimport_controller.go)。随后通过createOrUpdateServiceImport(基于controllerutil.CreateOrUpdate)幂等地创建或更新资源。
ServiceImport.Status会记录:
status.clusters:当前支撑该 ServiceImport 的所有集群列表;status.endpointSliceObjects:根据annotation.GlobalServiceSyncEndpointSlices注解或 Headless 类型判断 EndpointSlice 对象是否存在(getEndpointSliceObjectsStatus,见 serviceimport_controller.go);Ready条件:本地不支持任何导入的 IP 族时为False(reasonIPFamilyNotSupported);已存在派生 Service 注解时为True(reasonReady);否则为False(reasonPending,等待派生 Service 创建)。
本地ServiceExport的Valid、Ready、Conflict三个条件也在该控制器内维护:合法则Valid=True、Ready=True;有冲突则Conflict=True并携带具体 reason 与描述,否则Conflict=False(reasonNoConflicts)。
控制器二:mcsAPIServiceReconciler——派生 Service 与 IP 回写
ServiceImport本身不会直接驱动数据面,真正的接入点是派生 Service(derived Service)。mcsAPIServiceReconciler(实现在 service_controller.go)负责把ServiceImport转成一个普通的、带 Cilium global 注解的 Service,从而"免费"继承 ClusterMesh 既有的全部机制。
命名规则:derived-$hash
派生 Service 的名字由derivedName函数生成(service_controller.go):
func derivedName(name types.NamespacedName) string { hash := sha256.New() hash.Write([]byte(name.String())) return "derived-" + strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(hash.Sum(nil)))[:10] }即对namespace/name做 SHA-256,再用无填充的 Base32(HEX 编码、小写)取前 10 个字符。该函数来自 mcs-api 官方仓库的common.go,保证了与生态实现的一致性。
派生 Service 的关键属性
在Reconcile中(service_controller.go),派生 Service 会被构造为:
Type: ClusterIP,IPFamilyPolicy: PreferDualStack(始终偏好双栈,实际 IP 族以supported-ip-families注解为准);若 ServiceImport 为Headless类型则ClusterIP: None;Selector置空(map[string]string{}),端口从 ServiceImport 继承;- OwnerReference 指向 ServiceImport(
ctrl.SetControllerReference),保证 ServiceImport 删除时派生 Service 被级联清理; - 注解复制自 ServiceImport,并强制追加
service.cilium.io/global: "true"(annotation.GlobalService),正是这个注解让它进入 ClusterMesh 的 global Service 处理流程,触发远端 Endpoint 到 BPF Map 的同步、EndpointSliceSync 等既有能力(见 pkg/annotation/k8s.go); - 标签复制自 ServiceImport,并追加
multicluster.x-k8s.io/service-name(mcsapiv1beta1.LabelServiceName)指向原始服务名。
双栈与 Headless 迁移
getBaseDerivedService(service_controller.go)会处理两类边界情况:
- 强制双栈迁移:对于 Cilium 1.18 或更早版本创建的旧派生 Service,强制把
IPFamilyPolicy改为PreferDualStack; - Headless 切换:如果派生 Service 已存在,但它的 headless 属性与当前 ServiceImport 类型不一致(例如从 ClusterSetIP 切换为 Headless),会先删除旧 Service 再以新的形态重建,避免直接更新被 API Server 拒绝。
IP 回写
创建/更新派生 Service 后,patchServiceImport(service_controller.go)会把两件事写回 ServiceImport:
- 注解
multicluster.x-k8s.io/derived-service(DerivedServiceAnnotation)指向派生 Service 名; spec.ips更新为派生 Service 的 ClusterIPs——IP 的选取顺序严格跟随supported-ip-families注解(getDesiredIPs会按 IP 族顺序从 ClusterIPs 中挑 IPv4/IPv6,见 service_controller.go)。
这样,消费ServiceImport的 DNS 插件等组件就能拿到最终的虚拟 IP。
控制器三:mcsAPIEndpointSliceMirrorReconciler——本地 EndpointSlice 镜像
多集群服务不仅要"有 IP",还要把每个集群的端点(Endpoints)彼此可见。远端端点由 ClusterMesh 既有流程负责,而本地端点的对外可见性则由mcsAPIEndpointSliceMirrorReconciler承担(实现在 endpointslice_mirror_controller.go)。
该控制器把本地 Service 的每个 EndpointSlice"镜像"到派生 Service 对应的 EndpointSlice,命名规则为derivedServiceName + "-" + 本地EndpointsSlice后缀(getLocalDerivedEndpointSliceKey,见 endpointslice_mirror_controller.go),并打上endpointslice-local-mcsapi-controller.cilium.io的endpointslice.kubernetes.io/managed-by标签。
镜像过程包含三层过滤:
- IP 族过滤:只镜像与派生 Service 支持的 IP 族(
supported-ip-families注解)匹配的 EndpointSlice(shouldMirrorLocalEndpointSlice,见 endpointslice_mirror_controller.go); - 端口过滤:
getFilteredPorts(endpointslice_mirror_controller.go)按端口名把本地 Service 端口与派生 Service 端口对齐,若某端口发生冲突(端口号或协议不一致),则该端口会被跳过——既不会导给本地也不会导给远端; - 镜像产物维护:若全部端口被过滤掉、或本地 EndpointSlice 被删除/不再可镜像,控制器会清理对应的派生 EndpointSlice;对无法回溯到源 EndpointSlice 的"被篡改"派生 EndpointSlice,会走
reconcileMalformedDerivedEndpointSlice路径(带 UID/ResourceVersion 前置条件)删除之(见 endpointslice_mirror_controller.go)。
派生 EndpointSlice 同样以派生 Service 为 Owner,并带有multicluster.x-k8s.io/service-name、multicluster.x-k8s.io/source-cluster等标签,便于下游识别其来源。
如何启用:配置项与 CRD 安装
MCS-API 支持在 Cilium Operator 中通过两个布尔 flag 控制,定义于 types/config.go:
| Flag | 默认值 | 说明 |
|---|---|---|
--clustermesh-enable-mcs-api | false | 启用 ClusterMesh MCS-API 支持 |
--clustermesh-mcs-api-install-crds | true | 是否自动安装并管理 MCS-API CRD(仅在启用 MCS-API 时生效) |
只有当EnableMCSAPI && InstallCRDs都为真时,Operator 才会通过newMCSAPICRDs注册 CRD 安装函数(见 cell.go);若未启用自动安装,则控制器启动时会主动检查所需 CRD 是否存在,缺失即报错退出(见 cell.go)。
从模块组织看,整个 MCS-API 功能被封装为 Hive 的一个 cell("mcsapi",见 cell.go),依赖 Operator 的 ClusterMesh 组件、控制器运行时 Manager、kvstore store 工厂等,并在 bootstrap 时等待 ClusterMesh 的 observer 同步完成后才注册 ServiceImport 控制器(job.OneShot("mcsapi-main"),见 cell.go)。因此启用前提还包括:已部署 ClusterMesh(含 kvstoremesh)、命名空间已标记为 global(或配置了GlobalNamespacesByDefault),以及按官方文档安装 multicluster CoreDNS 插件用于 DNS 解析。
相关注解速查
MCS-API 实现依赖或写入以下 Cilium 注解(常量定义见 pkg/annotation/k8s.go):
| 注解 | 值 | 作用 |
|---|---|---|
service.cilium.io/global | true | 标记派生 Service 为 global Service,接入 ClusterMesh 数据面 |
service.cilium.io/global-sync-endpoint-slices | true | 是否把远端集群的 EndpointSlice 同步到本地 Kubernetes API(HEADLESS 类型默认同步) |
service.cilium.io/shared | true/false | 是否分享本地端点(false时仅暴露远端端点) |
service.cilium.io/affinity | local/remote/none | 端点亲和偏好,默认none |
cilium.io/supported-ip-families | IP 族列表 | MCS-API 内部注解,记录本地集群支持并使用的 IP 族 |
multicluster.x-k8s.io/derived-service | 派生 Service 名 | 由 mcs-api 控制器写入 ServiceImport,指向派生 Service |
可观测性与验证
Prometheus 指标
MCS-API 提供两类指标(见 metrics.go):
- 按远端集群统计的导出数量:
cilium_operator_clustermesh_remote_cluster_service_exports{target_cluster="..."}(Metrics.TotalServiceExports,由 observer 在 kvstore watch 时维护); - 由
mcsAPICollector收集的本地集群资源指标(subsystemmcsapi):cilium_operator_mcsapi_serviceexport_info{serviceexport,namespace}cilium_operator_mcsapi_serviceexport_status_condition{serviceexport,namespace,condition,status,reason}cilium_operator_mcsapi_serviceimport_info{serviceimport,namespace}cilium_operator_mcsapi_serviceimport_status_condition{serviceimport,namespace,condition,status,reason}cilium_operator_mcsapi_serviceimport_status_clusters{serviceimport,namespace}(当前支撑该 ServiceImport 的集群数)
这些指标与ServiceExport/ServiceImport的status.conditions一一对应,可直接用于告警(例如Conflict条件为 True、Ready条件长期为 False)。
符合性测试
仓库在 conformance/conformance_test.go 中通过testutils.MCSAPIConformanceTest运行 mcs-api 官方的 MCS-API 符合性测试;此外,serviceimport_controller_test.go、service_controller_test.go 与 endpointslice_mirror_controller_test.go 分别覆盖了端口合并冲突、IP 族交集、派生 Service 创建与镜像等关键逻辑,是理解各控制器行为的绝佳样例。
小结与排障线索
回顾 Cilium 中 MCS-API 的完整闭环:
- 本地/远端集群的用户创建
Service+ServiceExport; - Cilium 把它转换为
MCSAPIServiceSpec写入各自 ClusterMesh API Server(kvstore); - 导入方集群通过 observer 拉取所有集群的导出信息,
mcsAPIServiceImportReconciler按"最旧优先"原则做冲突解决,聚合生成ServiceImport; mcsAPIServiceReconciler依据ServiceImport创建derived-$hash派生 Service(带service.cilium.io/global: true),使其进入既有 ClusterMesh global Service 流程,并把 IP 回写ServiceImport;mcsAPIEndpointSliceMirrorReconciler把本地 EndpointSlice 镜像到派生 Service,完成端点互通;- multicluster CoreDNS 插件消费
ServiceImport与 EndpointSlice,提供跨集群 DNS 名称解析。
排查问题时,可按此链路逐层检查:先看ServiceExport的Valid/Conflict条件(确认是否被ExternalName、命名空间非 global 或端口/字段冲突阻断),再看ServiceImport的Ready条件与status.clusters(确认是否已聚合到预期集群、IP 族是否被本地支持),最后确认派生 Service 的service.cilium.io/global注解与multicluster.x-k8s.io/derived-service注解是否就位,并结合上文列出的 Prometheus 指标判断异常阶段。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考