Kubernetes 多集群联邦调度 Karmada 深度实战——从架构原理到生产落地
引言:为什么需要多集群联邦
2026 年,几乎所有中大型企业都运行着多个 Kubernetes 集群——可能是跨可用区的高可用部署,可能是 AWS + 阿里云的混合云架构,也可能是按业务线划分的独立集群。然而,Kubernetes 本身是一个单集群编排系统,并没有原生的多集群管理能力。这带来了一系列令人头疼的问题:应用需要手动分发到每个集群;故障转移依赖外部 DNS 切换;策略配置无法统一管控;可观测性碎片化……
Kubernetes Federation v1 因架构问题已被废弃;Federation v2(KubeFed)也基本停止维护。在这个真空中,Karmada(Kubernetes Armada,舰队)应运而生。作为 CNCF 孵化项目,Karmada 秉承"先扩展后抽象"的理念,不修改上游 Kubernetes 代码,而是通过 API Server + Controller 的模式,为多集群提供统一的控制平面。
本文将深入 Karmada 的架构设计、核心调度机制,并结合完整的生产级实战案例,展示如何构建真正的多集群联邦系统。
一、Karmada 架构解析
1.1 核心组件
Karmada 的控制平面完全独立运行,不需要改动子集群的任何配置:
┌─────────────────────────────────┐
│ Karmada Control Plane │
├─────────────────────────────────┤
│ Karmada API Server │
│ Karmada Controller Manager │
│ ├── Cluster Controller │
│ ├── Execution Controller │
│ ├── Binding Controller │
│ ├── Work Status Controller │
│ └── ...
┌─────────┐ watch │ Karmada Scheduler │ execute ┌─────────┐
│Member │◄───────►│ ├── Scheduling Framework │──────────►│Member │
│Cluster A│ │ │ ├── ClusterAffinity │ │Cluster B│
└─────────┘ │ │ ├── SpreadConstraint │ └─────────┘
│ │ ├── ReplicasScheduling │
┌─────────┐ │ │ ├── DividedReclaim │ ┌─────────┐
│Member │◄─────────┘ └── ... └─────────►│Member │
│Cluster C│ │Cluster D│
└─────────┘ └─────────┘
关键设计亮点:
- 不劫持 etcd:Karmada 有自己的轻量 etcd,但只存储多集群联邦资源(PropagationPolicy、ResourceBinding 等),不触碰子集群的任何数据
- 控制器驱动协调:通过 Karmada Agent 或 Push 模式连接子集群,子集群无需主动连回控制平面
- 原生兼容:普通的 Deployment、Service、ConfigMap 等对象直接作为联邦资源声明,无需 CRD 包装
1.2 资源生命周期
Karmada 的多集群资源流转过程完全遵循声明式 API 哲学:
graph LR
A[用户创建 Deployment] --> B[PropagationPolicy 匹配]
B --> C[ResourceBinding 生成]
C --> D[Scheduler 选择目标 Cluster]
D --> E[cluster-affinity binding 拆分为 ClusterResourceBinding]
E --> E[Execution Controller 下发到 Member Cluster]
E --> F[Work Status 采集回 Karmada]
F --> G[ServiceExport/Import 跨集群服务发现]
二、核心调度机制深度解析
Karmada 的调度器并非单一决策点,而是一个多阶段决策框架。
2.1 Propagation Policy 资源过滤
每个 Workload(Deployment/StatefulSet)通过 Label Selector 绑定到 Propagation Policy:
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: nginx-propagation
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: Deployment
name: nginx
placement:
clusterAffinity:
clusterNames:
- member-shanghai
- member-beijing
- member-singapore
spreadConstraints:
- spreadByField: Cluster
maxGroups: 3
minGroups: 2
replicaScheduling:
replicaDivisionPreference: Weighted
replicaSchedulingType: Divided
weightPreference:
staticWeightList:
- targetCluster:
clusterNames: [member-shanghai]
weight: 40
- targetCluster:
clusterNames: [member-beijing]
weight: 40
- targetCluster:
clusterNames: [member-singapore]
weight: 20
2.2 调度器框架插件
Karmada Scheduler 内置多个 Filter 和 Score 插件,生产环境可以按需组合:
| 插件 | 阶段 | 功能 |
|---|
| ClusterAffinity | Filter | 按集群名/标签过滤 |
|---|
| TaintToleration | Filter | 污点容忍度过滤 |
|---|
| ClusterPorts | Filter | 端口可用性检查 |
|---|
| APIEnablement | Filter | API 资源支持检查 |
|---|
| SpreadConstraint | Filter | 最小/最大分布组约束 |
|---|
| ClusterLocality | Score | 按资源可用量打分 |
|---|
| ClusterEviction | PreFilter | 自动驱逐不健康集群 |
|---|
2.3 副本分配算法
Divided 模式下的副本分配是一个经典的多维度约束求解问题。Karmada 内部采用贪心 + 回填算法:
// Karmada 调度器副本分配简化逻辑 (conceptual)
func staticWeightScheduler(desiredReplicas int32, clusters []ClusterWeight) map[string]int32 {
var totalWeight int64
for _, c := range clusters {
totalWeight += c.Weight
}
allocated := make(map[string]int32)
var allocatedTotal int32
// 第一遍:按比例取整分配
for i, c := range clusters {
if i == len(clusters)-1 {
// 最后一个集群:补足剩余副本,避免舍入误差
allocated[c.Name] = desiredReplicas - allocatedTotal
} else {
share := int32(int64(desiredReplicas) * c.Weight / totalWeight)
allocated[c.Name] = share
allocatedTotal += share
}
}
// 第二遍:处理资源不足集群的副本重平衡
for name, replicas := range allocated {
capacity := getClusterAvailableReplicas(name)
if replicas > capacity {
excess := replicas - capacity
allocated[name] = capacity
rebalance(excess, allocated, name)
}
}
return allocated
}
三、生产级实战:跨地域服务高可用部署
3.1 环境准备
子集群注册(使用 Push 模式):
# 安装 Karmada 控制平面
hack/install-cli.sh
karmadactl init
# 注册子集群(每个 Member Cluster 需安装 karmada-agent)
karmadactl join member-shanghai --cluster-kubeconfig=$HOME/.kube/sh-config
karmadactl join member-beijing --cluster-kubeconfig=$HOME/.kube/bj-config
karmadactl join member-singapore --cluster-kubeconfig=$HOME/.kube/sg-config
# 验证集群注册状态
kubectl get clusters
3.2 应用定义与策略配置
以下实战案例:部署一个 Redis Cluster + Node.js API 微服务到三个地域集群,实现地域亲和 + 自动故障转移。
Step 1:创建 Namespace Propagation
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: microservices-propagation
spec:
resourceSelectors:
- apiVersion: v1
kind: Namespace
name: microservices
placement:
clusterAffinity:
clusterNames:
- member-shanghai
- member-beijing
- member-singapore
Step 2:部署 Redis Cluster(StatefulSet + PVC)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
namespace: microservices
spec:
serviceName: redis-cluster
replicas: 6
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values: [redis-cluster]
topologyKey: topology.kubernetes.io/zone
containers:
- name: redis
image: redis:7.4-alpine
ports:
- containerPort: 6379
name: client
- containerPort: 16379
name: gossip
command: ["redis-server", "--cluster-enabled yes"]
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: redis-propagation
namespace: microservices
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: StatefulSet
name: redis-cluster
placement:
clusterAffinity:
clusterNames:
- member-shanghai
- member-beijing
- member-singapore
replicaScheduling:
replicaDivisionPreference: Weighted
replicaSchedulingType: Divided
weightPreference:
staticWeightList:
- targetCluster:
clusterNames: [member-shanghai]
weight: 33
- targetCluster:
clusterNames: [member-beijing]
weight: 33
- targetCluster:
clusterNames: [member-singapore]
weight: 34
spreadConstraints:
- spreadByField: Cluster
maxGroups: 3
minGroups: 3
Step 3:API 服务部署(Deployment + HPA)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: microservices
spec:
replicas: 9
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: registry.ybb.press/api-server:v2.3.1
ports:
- containerPort: 8080
env:
- name: REDIS_ADDR
value: "redis-cluster:6379"
- name: REGION
valueFrom:
fieldRef:
fieldPath: metadata.labels['topology.kubernetes.io/zone']
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
---
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: api-server-propagation
namespace: microservices
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: Deployment
name: api-server
- apiVersion: v1
kind: Service
name: api-server
placement:
clusterAffinity:
clusterNames:
- member-shanghai
- member-beijing
- member-singapore
replicaScheduling:
replicaDivisionPreference: Weighted
replicaSchedulingType: Divided
weightPreference:
staticWeightList:
- targetCluster:
clusterNames: [member-shanghai]
weight: 40
- targetCluster:
clusterNames: [member-beijing]
weight: 35
- targetCluster:
clusterNames: [member-singapore]
weight: 25
3.3 跨集群服务发现(Multi-Cluster Service)
Karmada 通过 ServiceExport 和 ServiceImport 资源实现跨集群的服务访问:
# 在所有 Member Cluster 导出 Service
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceExport
metadata:
name: api-server
namespace: microservices
---
# 在消费方集群创建 ServiceImport
apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ServiceImport
metadata:
name: api-server
namespace: microservices
spec:
type: ClusterSetIP
ports:
- name: http
protocol: TCP
port: 8080
这解决了多集群场景下的服务发现问题——客户端无需知道后端集群分布,统一通过虚拟 DNS 名 api-server.microservices.svc.clusterset.local 访问。
3.4 故障转移实战
Karmada 的故障转移是基于集群污点和控制器协作实现的:
Cluster Unhealthy ──► Taint Update ──► Karmada Detects ──► Reschedule Resources ──► Evict to Healthy Clusters
│ │
└── (kube-apiserver 不可达超过 cluster-failure-threshold) └── PropagationPolicy Override 生效
Karmada Descheduler 实现自动重调度:
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata:
name: api-server-propagation
spec:
resourceSelectors:
- apiVersion: apps/v1
kind: Deployment
name: api-server
placement:
replicaScheduling:
replicaDivisionPreference: Weighted
replicaSchedulingType: Divided
weightPreference:
dynamicWeight: AvailableReplicas # 按可用副本数动态调整
failover:
application:
decisionConditions:
- type: Healthy
status: "False"
- type: Timeout
value: "10s"
purgeMode: Immediately # 故障集群副本立即迁移
四、调度器高阶配置
4.1 Profile + Plugin 定制
Karmada 调度器支持多 Profile 模式,针对不同应用类型使用不同插件组合:
apiVersion: scheduling.karmada.io/v1alpha1
kind: SchedulerProfile
metadata:
name: latency-sensitive
spec:
plugins:
schedule:
enabled:
- name: ClusterAffinity
weight: 100
- name: SpreadConstraint
- name: ClusterLocality
weight: 50
disabled:
- name: ClusterEviction
pluginConfig:
- name: ClusterLocality
args:
apiVersion: config.karmada.io/v1alpha1
kind: ClusterLocalityArgs
availablePercentage: 70
4.2 自定义调度插件
当内置插件不满足需求时(如 GPU 利用率感知、跨集群网络拓扑感知),可以编写自定义插件,实现 Filter 或 Score 接口:
package clusterlatency
import (
"context"
"github.com/karmada-io/karmada/pkg/scheduler/framework"
)
const Name = "ClusterLatency"
type ClusterLatency struct{}
func (c *ClusterLatency) Name() string { return Name }
func (c *ClusterLatency) Filter(ctx context.Context, state *framework.CycleState, pod *v1.Pod, cluster string, up framework.ClusterClient) *framework.Result {
latency := state.clusterLatencies[cluster]
if latency > 100*time.Millisecond {
return framework.NewResult(framework.Unschedulable, "cluster latency too high:", latency)
}
return framework.NewResult(framework.Success)
}
func (c *ClusterLatency) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, cluster string) (int64, *framework.Result) {
latency := state.clusterLatencies[cluster]
// 延迟越低分数越高
score := int64(1000 / latency.Milliseconds())
return score, framework.NewResult(framework.Success)
}
五、可观测性体系
Karmada 提供完整的监控指标,直接对接 Prometheus + Grafana:
# 启用 metrics
kubectl apply -f components/karmada-metrics-adapter/
关键 SLI/SLO 指标:
| 指标 | 含义 | 告警阈值 |
|---|
| `karmada_scheduler_schedule_attempts_total` | 调度尝试次数 | - |
|---|
| `karmada_execution_apply_duration_seconds` | 资源下发耗时 | P99 > 30s |
|---|
| `karmada_cluster_ready` | 集群就绪状态 | != 1 (持续 > 5min) |
|---|
| `karmada_binding_replicas` | 当前绑定副本数 | 偏离声明值 > 20% |
|---|
| `karmada_deschedule_evict_total` | 驱逐次数 | > 3/hour |
|---|
Grafana Dashboard 中可清晰看到跨集群资源分布热力图、调度延迟趋势、故障转移事件时间线,为核心业务提供数据支撑。
六、与其他方案的对比
| 特性 | Karmada | Open Cluster Management (OCM) | KubeFed v2(已废弃) |
|---|
| 资源分发模式 | 控制器+Webhook | 控制器+Registration | 资源模板覆盖 |
|---|
| 调度框架 | 原生 Scheduling Framework | 外部Placement | 简单过滤 |
|---|
| 故障转移 | 内置 Descheduler | 需外部实现 | 不支持 |
|---|
| 服务发现 | 原生 ServiceImport/Export | 有 | 有 |
|---|
| GC 机制 | 自动清理孤儿资源 | 有 | 不完整 |
|---|
| 社区活跃度 | CNCF 孵化项目,活跃 | CNCF 沙箱项目 | 已归档 |
|---|
| 侵入性 | 无侵入,子集群无感知 | 需要安装 klusterlet | 资源包装 |
|---|
七、生产实践总结
根据我们的部署经验,以下是多集群联邦的关键最佳实践:
网络前提:Member 集群与 Karmada 控制平面之间网络延迟建议 < 100ms,否则 Execution Controller 的状态同步会严重滞后。必要时使用 Pull 模式(Karmada Agent 部署在 Member 侧主动连回)。
资源亲和策略:AI/GPU 工作负载必须结合 clusterAffinity.clusterNames 手动指定有 GPU 的集群,不要依赖自动评分。
灰度发布:利用 Karmada 的 Override Policy 实现集群级灰度——先发布到 staging 集群,验证后逐步推到 prod。
etcd 备份:Karmada 控制平面的 etcd 是所有联邦资源的状态源,必须纳入定期备份,恢复难度远高于单集群。
GitOps 集成:PropagationPolicy、Override Policy 声明纳入 Git 仓库,通过 ArgoCD 实现联邦层面的 GitOps 流水线。
结语
Karmada 代表了一种务实的多集群架构哲学:不发明新 API、不绑架基础设施、不在子集群安装侵入式组件。它站在 Kubernetes 原生生态的肩膀上,用控制器和声明式 API 解决了企业多云场景下的核心痛点。随着 Karmada 1.x 版本成熟和 MultiClusterService 标准逐步成为社区共识,多集群联邦正在从"高级特性"变成"生产标配"。如果你正在规划多云或混合云战略,Karmada 值得列入技术选型的首选清单。

发表评论 取消回复