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 插件,生产环境可以按需组合:

插件阶段功能
ClusterAffinityFilter按集群名/标签过滤
TaintTolerationFilter污点容忍度过滤
ClusterPortsFilter端口可用性检查
APIEnablementFilterAPI 资源支持检查
SpreadConstraintFilter最小/最大分布组约束
ClusterLocalityScore按资源可用量打分
ClusterEvictionPreFilter自动驱逐不健康集群

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 中可清晰看到跨集群资源分布热力图、调度延迟趋势、故障转移事件时间线,为核心业务提供数据支撑。

六、与其他方案的对比

特性KarmadaOpen 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 值得列入技术选型的首选清单。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部