17. 多集群管理
一个集群不够用的那一天
前面 16 章都在讲单个 K8S 集群内部的事。但迟早你会遇到这些场景:
- 多地域部署。同样的应用,新加坡一套、东京一套。每个地域一个独立集群,公网隔离,互相不依赖。
- 开发/测试/生产环境隔离。你不能让测试集群的 Pod 碰到生产数据。独立的集群是最好的硬隔离。
- 集群太大管不过来。单个 K8S 集群官方支持 5000 个 Node。超了这个数字不是不能跑,而是 Scheduler 和 etcd 的压力曲线陡增——所有的 Pod 变动都在同一个 Watch 池子里。
- 算力异构。你的 GPU 集群在机房 A,边缘 K3S 集群在全国几百个网点。这些集群的 Node 类型、网络环境完全不同,没法融进一个集群里。
多集群不是你"先做一个大的、拆成几个小的",而是从一开始你就有不同物理位置的资源需要统一纳管。
多集群的两层:分配 + 调度
拆成多个集群之后,一个 Pod 要跑起来多了一层决策:
用户声明:"我要 10 个 GPU Pod"
│
▼
第一层:哪个集群有空闲 GPU + 网络延迟最低?
│ 选集群 (Cluster Selection)
│
▼
第二层:在这个集群的哪个 Node 上跑?
│ 调 Scheduler(第 14 章的内容,完全不用改)
│
▼
Pod Running
第一层是多集群特有的——第 14 章的 Scheduler 管不了"选哪个集群"。你需要额外的东西来管理集群间的资源视图和调度。
Karmada:完整的多集群控制面
Karmada(CNCF Incubating)是目前最成熟的多集群方案。它在成员集群之上建了一整套控制面——有自己的 API Server、Scheduler、Controller Manager。
Cluster Registry:把所有集群注册进来
首先你得知道有几个集群、每个集群剩多少资源。Karmada 把每个成员集群注册成一个 Cluster 对象:
apiVersion: cluster.karmada.io/v1alpha1
kind: Cluster
metadata:
name: member1
spec:
syncMode: Push # push 还是 pull
apiEndpoint: https://192.168.1.10:6443
secretRef:
name: member1-kubeconfig
taints:
- key: gpu-type
value: v100
effect: NoSchedule
Karmada Control Plane 通过这个 Cluster 对象的 kubeconfig 连接成员集群。然后它 Watch 成员集群的 API Server,把"这个集群现在有多少 CPU/内存/GPU 空闲"同步到 Control Plane 上。
思考题:Control Plane 连成员集群用的是它的 kubeconfig——这个 kubeconfig 里的证书是怎么跨集群认证的?Control Plane 的 API Server 跟成员集群的 API Server 是两套完全独立的 CA——怎么互信?
// karmada/pkg/cluster/cluster.go 简化
func (c *ClusterController) syncCluster(key string) error {
cluster := c.clusterLister.Get(key)
client := c.getClient(cluster.Name)
// 抓取成员集群的总资源和分配情况
nodes, _ := client.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
totalCPU := sum(nodes.Items, func(n v1.Node) int64 { return n.Status.Allocatable.Cpu() })
// 更新 Cluster 对象上的资源状态(供后续调度使用)
updateClusterStatus(cluster, totalCPU, totalMemory)
}
调度:先选集群,再下发
Karmada Scheduler 不调 Pod 去哪个 Node,而是调 PodTemplate 去哪个集群。
# 用户在 Karmada Control Plane 上写
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 10
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: nginx-propagation
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: Deployment
name: nginx
placement:
clusterAffinity:
clusterNames: [member1, member2, member3] # 只有这三个集群
spreadConstraints:
- spreadByField: cluster
maxGroups: 3
minGroups: 3
replicaScheduling:
replicaDivisionPreference: Weighted
replicaSchedulingType: Divided
weightPreference:
staticWeightList:
- targetCluster:
clusterNames: [member1]
weight: 60 # 60% 副本下到 member1
- targetCluster:
clusterNames: [member2]
weight: 40 # 40% 下到 member2
Karmada Scheduler 接收到 PropagationPolicy,算出每个集群该分到的副本数,然后把 Deployment(带上裁剪过的 replicas)下发到对应的成员集群 API Server。每个成员集群收到的是这样一个对象:
# karmada 下发到 member1 的 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
annotations:
resourcetemplate.karmada.io/uid: "original-123"
spec:
replicas: 6 # 原来的 60% = 6 个
template:
...
余下的 4 个副本下到 member2。每个成员集群收到的是一个普通的 Deployment YAML,它们根本不感知自己被"多集群纳管"了——本地 Scheduler 照常工作。
思考题:Karmada 把 Deployment 拆成 6 + 4 个副本下到两个成员集群——如果成员集群 1 挂了,那 6 个 Pod 就没了。Control Plane 会感知到并把这 6 个副本重新分配到集群 2 吗?还是需要手工介入?
K3S 在 Karmada 中的角色
K3S 经常出现在多集群的边缘层:
Karmada Control Plane (K8S 或 K3S)
│
├── Cluster: beijing-dc (K8S, 50 Node, GPU 集群)
├── Cluster: singapore-dc (K8S, 20 Node)
├── Cluster: edge-shenzhen (K3S, 6 Node, Jetson 边缘 AI)
├── Cluster: edge-shanghai (K3S, 3 Node, 轻量推理)
└── Cluster: edge-hangzhou (K3S, 2 Node)
K3S 的轻量特性让它在边缘多集群场景几乎不可替代:
- 单个 K3S 二进制 100MB,启动一键完成
- 支持 ARM(树莓派、Jetson),内存占用 300MB 起步
- SQLite 模式不需要额外的 etcd 部署
Karmada 通过 Pull 模式(而不是 Push)管理远端 K3S 集群——K3S 集群里的 karmada-agent 主动连接 Control Plane,不需要 Control Plane 直连边缘集群。这对于 NAT 后面的边缘设备至关重要。
Pull 模式:NAT 后面的集群也能纳管
Pull 模式由 Karmada 的 karmada-agent 组件在成员集群上手实现:
Control Plane 成员集群 (NAT 后面)
┌──────────┐ ┌─────────────────┐
│ karmada │ ←──── gRPC stream ──── │ karmada-agent │
│ scheduler│ │ ┌───────────┐ │
│ │ │ │ K3S │ │
└──────────┘ │ │ + 你的应用 │ │
│ └───────────┘ │
└─────────────────┘
karmada-agent 用 gRPC 反向通道连接 Control Plane,它扮演两个角色:
- Watch Control Plane 下发到自己的 Deployment 等资源,
kubectl apply到本地集群 - 把本地 Pod 的运行状态上报给 Control Plane
// karmada/pkg/agent/agent.go 简化
func (a *Agent) Run(ctx context.Context) error {
// 1. 连接 Control Plane
conn := grpc.Dial(a.controlPlaneAddr, ...)
// 2. 上报集群状态
a.registerCluster(conn)
// 3. Watch 下发到本集群的资源
go a.watchResources(conn)
// 4. 上报 Pod 状态到 Control Plane
go a.reportStatus(conn)
}
Control Plane 不关心 agent 后面是 K8S 还是 K3S——kubectl apply 对二者是一样的 API。
Clusternet:最轻的多集群粘合层
Clusternet(CNCF Sandbox)选了跟 Karmada 完全不同的路——不需要独立控制面,所有组件跑在父集群里,成员集群映射为一个 namespace。
架构:三个组件,全跑在父集群
父集群 namespace: clusternet-system 每个成员集群各占一个
┌──────────────────────┐ ┌─────────────────────┐
│ clusternet-hub │ ←─ gRPC ──→ ┌─│ clusternet-cls-abc123 │
│ clusternet-scheduler │ │ │ │ Description │
│ Subscription CRD │ │ │ │ Localization │
└──────────────────────┘ │ │ └─────────────────────┘
│→│ clusternet-cls-def456 │
│ │ ... │
└─└─────────────────────┘
| 组件 | 跑在哪 | 干什么 |
|---|---|---|
clusternet-hub | 父集群 | 所有 agent 的连接点,gRPC 双向流推送 Description、接收状态上报 |
clusternet-scheduler | 父集群 | 监听 Subscription,选出匹配集群,按策略算副本,生成 Description |
clusternet-agent | 每个成员集群 | 连 hub、收 Description、本地 kubectl apply、上报状态 |
不需要独立的 etcd、独立的 API Server——所有东西复用父集群。
资源模型:Subscription + Description
Subscription——你声明"这些资源要下到 label region=east 的集群"。你写的是标准 K8S YAML,apply 到父集群,不需要学新的语法:
apiVersion: apps.clusternet.io/v1alpha1
kind: Subscription
metadata:
name: nginx-sub
spec:
feeds:
- apiVersion: apps/v1
kind: Deployment
name: nginx
subscribers:
- clusterAffinity:
matchLabels:
region: east
schedulingStrategy: Dividing # 分散到多个集群
Description——Scheduler 把原始 Deployment 序列化后包进 Description,存到目标集群对应的 namespace(clusternet-<cluster-id>/)。hub 通过 gRPC 推给 agent,agent 解码 → kubectl apply → Pod 诞生:
apiVersion: apps.clusternet.io/v1alpha1
kind: Description
metadata:
name: nginx-deployment
namespace: clusternet-cls-abc123 # 只属于这个集群
spec:
raw: # 原始 Deployment 的完整序列化
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
Override:每个集群可以有自己的差异
同一个 Deployment 下到三个集群——北京 replicas=10、新加坡=5、边缘 K3S=2。用 Localization(JSON Patch):
apiVersion: apps.clusternet.io/v1alpha1
kind: Localization
metadata:
name: nginx-edge-override
namespace: clusternet-cls-edge-001 # 只对这个集群生效
spec:
feed:
apiVersion: apps/v1
kind: Deployment
name: nginx
overrides:
- override: '{"spec":{"replicas":2}}'
type: JSONPatch
Scheduler 生成 Description 时先合并全局 Globalization → 再合并 per-cluster Localization → 塞进 spec.raw。agent 拿到的是已差异化完成的 Deployment。
调度策略:Dividing vs Replicating
- Dividing——副本分散到多个集群。20 个 Nginx 副本,4 个集群各 5 个。适合业务 Pod 负载分散
- Replicating——每个匹配集群都拿到完整的一份。监控/日志基础设施每个集群各装一套
Push 模式:可免 agent
Clusternet 默认 Pull(agent 主动连 hub,NAT 友好)。也支持 Push——hub 直连成员集群 API Server,不需要 agent:
spec:
syncMode: Push
apiEndpoint: https://10.0.1.10:6443
适合所有集群在同一 VPC 的场景。
一句话:Karmada 是"完整控制面",适合复杂调度。Clusternet 是"最轻粘合层",适合"我就想把几个集群管起来"。
Karmada vs Clusternet:怎么选
| 维度 | Karmada | Clusternet |
|---|---|---|
| 控制面 | 独立的 etcd + API Server | 复用父集群 API Server,全部跑在 Pod 里 |
| 集群映射 | 自定义 Cluster 对象 | 一个 namespace(clusternet-<id>) |
| 调度 | 丰富的 affinity/taint/拓扑/权重 | Dividing/Replicating,不支持亲和性 |
| Override | OverridePolicy | Localization(per-cluster)+ Globalization(全局) |
| 学习成本 | 需学 PropagationPolicy、OverridePolicy 等 CRD | Subscription + Description + JSON Patch |
| 侵入度 | 需要 agent | Push 模式可免 agent |
| CNCF | Incubating | Sandbox |
选 Karmada——多集群间复杂调度(GPU 多的集群跑训练、CPU 多的跑推理)、需要跨集群拓扑感知、企业级多集群治理。
选 Clusternet——快速纳管几十个边缘集群、不想学新 CRD、所有集群在同一 VPC、团队只懂标准 K8S。
多集群网络:是分开还是打通
多集群管理的一个大坑是网络。
如果你的应用不需要跨集群通信(每个集群是一个独立的"单元",只对公网暴露 API)——那最简单。每个集群自己的 Service 和 Gateway,互不影响。
但如果一个集群的 Pod 需要调另一个集群的 Service——你就有跨集群网络的需求了。主流方案:
| 方案 | 实现 | 复杂度 |
|---|---|---|
| Istio / Linkerd 多集群 Mesh | 通过 mTLS + Gateway 打通服务网 | 高 |
| Submariner | 基于 IPsec 隧道的跨集群 Pod 网络 | 中 |
| DNS + 公网 Gateway | 用域名路由到各集群 Gateway | 低 |
大部分场景走到第三个就够了——"跨集群通信"很大程度上可以通过统一 API Gateway + DNS 路由来绕开 Pod 级别的直接连通性。
思考题:一个 Deployment 在两个成员集群各跑了副本——前端怎么让用户流量去最近的集群?DNS 能做 GeoDNS,但你怎么保证 DNS 解析到的那个集群的后端 IP 是健康的?
小结
多集群是 K8S 的终极问题——当集群数量、地域、算力类型都需要分别管理时,你需要一层更高级的调度。
- Cluster Registry 汇总所有集群的资源余量和状态
- Karmada Scheduler 做"选集群 + 分配副本"的第一层决策
- 成员集群(无论 K8S 还是 K3S)收到的仍然是标准 YAML,本地逻辑不变
- Pull 模式(karmada-agent)让 NAT 后面的边缘集群能主动连到 Control Plane
- Clusternet 作为替代方案,用 namespace 映射集群、Subscription 替代 PropagationPolicy,学习成本更低
- K3S 的轻量 + ARM 支持让它在多集群边缘场景下几乎不可替代
- 跨集群网络从简单到复杂有多种方案可选,DNS + Gateway 覆盖大部分需求
解答
1. 跨集群认证——两套 CA 怎么互信?
Control Plane 连成员集群用的是标准 kubeconfig——跟你本机 ~/.kube/config 完全一样的格式。成员集群的 Cluster 对象里存着 apiEndpoint + secretRef(指向包含 kubeconfig 的 Secret)。
认证走的是成员集群的 CA——不是 Control Plane 的 CA。两套 CA 完全独立,不需要互信:
Control Plane 成员集群
┌──────────────┐ ┌──────────────┐
│ CA_cp 签发 │ kubeconfig │ CA_member │
│ control-plane│ ─── 带着 ──→ │ 签发 │
│ 组件证书 │ 成员集群证书 │ kube-apiserver│
└──────────────┘ └──────────────┘
kubeconfig 里的 certificate-authority-data 是成员集群的 CA 证书——Control Plane 用这个 CA 验证成员集群 API Server 的身份。Control Plane 的 CA 不参与这个过程。
2. 成员集群挂了——副本会自动漂移吗?
Karmada 默认不会自动重新分配。 但可以配 PropagationPolicy 的 reschedulePolicy 来启用:
spec:
placement:
reschedulePolicy:
replicaRescheduling:
type: Divided # 挂了的集群的副本重新分给其他正常集群
不加这个配置,Karmada 默认行为是"看到集群挂了就等它活过来"——Pod 数(期望值)不变。加上 Divided 后,Scheduler 会重新计算剩余正常集群的副本分配。
Clusternet 没有这个自动恢复机制。它的调度是"指定多少就是多少",不管目标集群挂不挂。
3. 前端如何让流量去最近的集群?
三种递进方案:
GeoDNS——最基础。同一个域名,北京的 DNS 解析到北京集群的 LB IP,新加坡解析到新加坡集群。问题:DNS 有 TTL 缓存,切换要等 TTL 过期;无法检测后端健康则继续返回 IP。
Global Load Balancer + Health Check——云厂商的全局 LB(AWS Global Accelerator、GCP Cloud LB)能健康检查后端 LB,健康则路由,挂了自动切。比纯 DNS 多了健康感知。
Multi-Cluster Gateway(Istio / Linkerd)——最完整。Gateway 分布在每个集群,mTLS 互通,Control Plane 感知后端健康状况。流量路由分两步:GeoDNS 选集群 → 集群内 Gateway 选具体 Pod。如果集群 B 的后端全挂了,Gateway 能自动把流量转到集群 A。代价是整个控制面的复杂度最高。
大部分场景走到第二步就够了——全局 LB + 健康检查覆盖了 99% 的多集群流量需求。
到这里,17 章走完了。从 docker run 一路走到几百个集群的调度——希望你对"K8S 不是魔法,它是一堆接口拼起来的"这件事,有了更清晰的认识。