跳转至

多集群编排:Karmada

一个集群能承载的规模有上限,容灾、地域合规和数据隔离又常常要求「不把鸡蛋放在一个篮子里」。Karmada 让你继续用 Deployment、Service 这些原生 API 描述应用,由一份策略决定副本落在哪些集群、每个集群长什么样、某个集群挂掉之后怎么办。

难度 · 进阶 预计 55 分钟 前置:Deployment、Service、RBAC

你将学到

  • 多集群要解决的四个问题:容灾、地域、隔离、规模,以及它们对调度策略的影响
  • 控制面组件分工,资源模板、PropagationPolicy、ResourceBinding、Work 的关系
  • Duplicated 与 Divided 两种副本模式,以及 OverridePolicy 的差异化改写
  • 集群故障时副本如何被判定失联、驱逐,并在其它集群重建

为什么需要它

单集群的边界很实在:etcd 与 apiserver 的写入能力、单集群 5000 节点的推荐上限、一次误操作的影响半径,以及「同城双活 / 异地多活」这类合规要求。当业务跨可用区甚至跨云部署时,问题从「怎么调度 Pod」变成了「怎么调度集群」。主流做法有三种:单集群多命名空间(最简单,但故障域没变)、Cluster API + GitOps(每集群一份清单,成本随集群数线性增长)、多集群联邦控制面(一套 API 和策略管理所有集群,Karmada 属于这一类)。前两种是「分别管理」,第三种是「统一编排」,而且不修改应用:你写的仍是标准 Deployment,只是提交到 karmada-apiserver。

方案 统一 API 跨集群重调度 差异化配置 复杂度
单集群多命名空间 是 不涉及 Namespace 级 低
Cluster API + Argo CD 否,每集群一份 需人工改 Git Kustomize overlay 中
Karmada 是 内置 failover OverridePolicy 高

核心概念

控制面与成员集群

控制面是一套「迷你 apiserver + etcd + 控制器」,通常跑在某个 host cluster 上,自己不运行业务 Pod,只保存资源模板和策略。

flowchart TB
  U["kubectl / GitOps"] --> A["karmada-apiserver + etcd"]
  A --> CM["controller-manager"] --> B["ResourceBinding / Work"]
  B --> SCH["scheduler"]
  SCH --> M1["成员集群 kube02"]
  SCH --> M2["成员集群 kube03"]
  M1 -->|"状态回流"| CM

图:模板与策略进 karmada-apiserver,scheduler 决定落到哪些集群,Work 才是真正下发到成员集群的对象。

controller-manager 把策略翻译成 ResourceBinding(命名空间级)或 ClusterResourceBinding(集群级),再拆成每个集群一份 Work;scheduler 决定放置;webhook 做准入校验。成员集群默认 Push 模式(控制面直接写成员集群),跨网络边界可用 karmada-agent 走 Pull 模式。

资源模板与传播策略

提交到 karmada-apiserver 的 Deployment 只是模板,不会自动出现在任何成员集群。让它流动起来的是 PropagationPolicy:resourceSelectors 选中资源(支持 apiVersion、kind、namespace、name、labelSelector),placement 决定去哪,propagateDeps 决定是否连带分发 Service、ConfigMap 等依赖。

02-demoapp-propergation-policy.yaml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata: {name: propagationpolicy-demo}
spec:
  resourceSelectors:
    - {apiVersion: apps/v1, kind: Deployment, name: demoapp}
  placement:
    clusterAffinity: {clusterNames: [kube02, kube03]}   # 目标成员集群
  propagateDeps: true

策略与资源是多对多匹配:一个策略可选中多个资源,一个资源也可能被多个策略选中——后者按 spec.priority 仲裁,同优先级下最早创建的生效。实验时「改了策略却没效果」,十有八九是旧策略还在。

Duplicated 与 Divided

replicaSchedulingType 决定副本怎么分:

  • Duplicated:每个目标集群拿到完整副本数。模板 replicas: 4 + 两个集群 = 8 个 Pod,适合「两地都全量服务」的容灾形态。
  • Divided:把 replicas 拆开分给各集群。4 个副本按权重 3:1 分成 3 和 1,适合「总量固定、按容量分摊」。

replicaDivisionPreference: Weighted 配合 weightPreference.staticWeightList 指定静态权重,不写则平均分配。spreadConstraints 进一步约束拓扑:spreadByField 可选 region、zone、provider、cluster,minGroups / maxGroups 控制铺开几组;cluster 维度是其它维度的前提,不按集群铺开就无从谈「每个 region 里选几个集群」。

OverridePolicy:同一份模板,不同集群不同样子

OverridePolicy 在分发前改写资源,常用于「测试集群用小 tag」「边缘集群加 nodeSelector」「某集群副本减半」。它支持 imageOverrider、plaintextOverrider(按 JSONPath 改任意字段)、commandOverrider、labelsOverrider 等,targetCluster 限定生效范围。注意 resourceSelectors 里同时写 name 和 labelSelector 时后者被忽略,仓库 YAML 的注释就是提醒这一点。

故障转移

Karmada 给不健康的成员集群打上 cluster.karmada.io/not-ready 与 cluster.karmada.io/unreachable 污点,工作负载通过 clusterTolerations 声明能忍多久:

机制 位置 作用
集群污点 成员集群对象 控制面按心跳自动打上
clusterTolerations placement 容忍时长,决定多久后开始驱逐
failover.application 策略 触发条件、宽限期、purgeMode 驱逐方式

purgeMode: Graciously 是「等新副本就绪或宽限期超时再删旧的」,Directly 直接删。生产上几乎总该选前者,否则故障瞬间会出现容量缺口。

与 Cluster API、Argo CD 的分工

三者不是竞品:Cluster API 创建、升级、伸缩集群本身;Karmada 决定应用放在哪些集群、每个集群长什么样、集群挂了怎么办;Argo CD / Flux 把 Git 里的期望状态同步到集群,负责交付而非放置。常见组合是 Argo CD 只对 karmada-apiserver 同步、Karmada 向成员集群扇出;ApplicationSet 也能扇出,但没有重调度与故障转移能力,集群失联时只能报 Unknown。

动手实验

实验:把 demoapp 分发到两个成员集群并演练故障转移

目标:用 Karmada 控制面创建 Deployment,分发到 kube02 / kube03,做差异化,最后模拟集群故障观察副本迁移。

前置:一个 host 集群 + 两个成员集群;karmadactl 已安装;能访问仓库 karmada/ 下的 YAML。

步骤 1 · 初始化控制面并接入成员集群

karmadactl init          # 在 host 集群部署 karmada-apiserver、etcd、控制器
karmadactl join kube02 --cluster-kubeconfig=$HOME/.kube/kube02.config
karmadactl join kube03 --cluster-kubeconfig=$HOME/.kube/kube03.config
kubectl --kubeconfig /etc/karmada/karmada-apiserver.config get clusters
NAME     VERSION   MODE   READY   AGE
kube02   v1.31.0   Push   True    3m
kube03   v1.31.0   Push   True    2m

后面所有命令都要带 --kubeconfig=/etc/karmada/karmada-apiserver.config(仓库 README 的写法,下文用变量 K 指代)。用成员集群的 kubeconfig 会报 no matches for kind "PropagationPolicy",因为那些 CRD 只存在于控制面。

步骤 2 · 创建模板并分发

01-demoapp-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: demoapp, namespace: default}
spec:
  replicas: 4
  selector: {matchLabels: {app: demoapp}}
  template:
    metadata: {labels: {app: demoapp}}
    spec:
      containers:
        - {name: demoapp, image: "ikubernetes/demoapp:v1.0"}

只提交 Deployment 时,kubectl get deploy 会显示 READY 0/0——模板存在但没有被分发;成员集群里根本查不到这个 Deployment。加上策略后才真正下发。下面的 04 是带 spreadConstraints 的完整版,前面 02 只是最小示例:

K="--kubeconfig=/etc/karmada/karmada-apiserver.config"
kubectl apply -f 01-demoapp-deployment.yaml $K
kubectl apply -f 04-propagationpolicy-ha.yaml $K   # 完整版:Duplicated + spreadConstraints
karmadactl get deployment $K
NAME      CLUSTER   READY   UP-TO-DATE   AVAILABLE   AGE
demoapp   kube02    4/4     4            4           45s
demoapp   kube03    4/4     4            4           45s

karmadactl get 的 CLUSTER 列把同一份模板在各成员集群的副本摊开显示,是验证分发结果最快的方式。04 里的 spreadConstraints 做了两件事:spreadByField: region 要求铺到 2 个 region,spreadByField: cluster 限定每个 region 只选 1 个集群。

步骤 3 · 用 OverridePolicy 做差异化

03-demoapp-override-policy.yaml
apiVersion: policy.karmada.io/v1alpha1
kind: OverridePolicy
metadata: {name: overridepolicy-demo}
spec:
  resourceSelectors:
    - {apiVersion: apps/v1, kind: Deployment, name: demoapp}   # 写了 name 就不看 labelSelector
  overrideRules:
    - targetCluster: {clusterNames: [kube03]}
      overriders:
        imageOverrider:
          - {component: Tag, operator: replace, value: "v1.1"}   # 只在 kube03 换 tag
kubectl apply -f 03-demoapp-override-policy.yaml $K
kubectl --kubeconfig ~/.kube/kube02.config -n default get deploy demoapp -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl --kubeconfig ~/.kube/kube03.config -n default get deploy demoapp -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

kube02 上镜像 tag 仍是 v1.0,kube03 上已经变成 v1.1——同一份模板、两个镜像,这就是「不做多套 Git 分支也能做环境差异化」;想按集群改副本数,用 plaintextOverrider 指定 path: /spec/replicas。

步骤 4 · Divided 拆分与故障转移

06-propagationpolicy-failover.yaml
apiVersion: policy.karmada.io/v1alpha1
kind: PropagationPolicy
metadata: {name: propagationpolicy-failover-demo}
spec:
  failover:
    # Graciously 表示先建后删:等新副本就绪或宽限期超时才驱逐
    application: {decisionConditions: {tolerationSeconds: 60}, gracePeriodSeconds: 120, purgeMode: Graciously}
  resourceSelectors:
    - {apiVersion: apps/v1, kind: Deployment, name: demoapp}
  placement:
    clusterTolerations:
      - {key: cluster.karmada.io/not-ready, operator: Exists, effect: NoExecute, tolerationSeconds: 60}
      - {key: cluster.karmada.io/unreachable, operator: Exists, effect: NoExecute, tolerationSeconds: 60}
    replicaScheduling:
      replicaSchedulingType: Divided
      replicaDivisionPreference: Weighted
      weightPreference:
        staticWeightList:
          - {targetCluster: {clusterNames: [kube02]}, weight: 3}
          - {targetCluster: {clusterNames: [kube03]}, weight: 1}
  propagateDeps: true

这条策略把 replicaSchedulingType 换成 Divided 并给了 3:1 的静态权重,所以 4 个副本会拆成 3 和 1;同时用两条 clusterTolerations 声明对失联污点的容忍时长。

kubectl apply -f 06-propagationpolicy-failover.yaml $K
ssh kube03-node 'systemctl stop kubelet'   # 模拟 kube03 彻底失联
kubectl --kubeconfig /etc/karmada/karmada-apiserver.config get clusters
karmadactl get deployment $K
NAME      CLUSTER   READY   UP-TO-DATE   AVAILABLE   AGE
demoapp   kube02    4/4     4            4           6m

kube03 的 READY 变成 False 后并不会立刻驱逐:tolerationSeconds: 60 到期、decisionConditions 满足,副本才被重新调度到 kube02 补齐到 4。failover.application 是较新版本引入的字段,用 kubectl explain propagationpolicy.spec.failover 确认你的 CRD 支持哪一版。

验证清单

  • 两个成员集群 READY=True,kubectl get clusters 显示 MODE=Push
  • Duplicated 下两个集群各 4 副本,总数是模板的 2 倍
  • Divided 3:1 下两个集群分别是 3 和 1,总数等于模板 replicas
  • OverridePolicy 生效后 kube03 是 v1.1,kube02 仍是 v1.0
  • kube03 失联后副本在 kube02 补齐到 4

常见问题与排错

现象 可能原因 处理方式
资源创建成功但成员集群没有 没有匹配的 PropagationPolicy kubectl get pp、kubectl get rb 检查绑定;resourceSelectors 的 apiVersion 必须是 apps/v1
报 no matches for kind PropagationPolicy 用了成员集群的 kubeconfig 所有策略操作加 --kubeconfig=/etc/karmada/karmada-apiserver.config
Work 一直 Pending 成员集群 NotReady 或接入凭据过期 kubectl -n karmada-es-kube02 get work -o yaml 看 conditions,必要时重跑 karmadactl join
改了策略但副本数没变 多条策略同时选中同一资源 kubectl get pp -o yaml 查 priority,删掉不用的策略
OverridePolicy 不生效 name 与 labelSelector 并存 二选一;按名字选资源时删掉 labelSelector
故障转移不触发 集群只是短暂抖动,未到 tolerationSeconds 加大故障注入强度(停 kubelet);核对两条 taint 拼写

进阶挑战

  1. 用 clusterAffinity.labelSelector(按 region=beijing)替代 clusterNames,再接入一个同标签集群,观察它是否被自动纳入分发。
  2. 用 plaintextOverrider 给 kube03 注入 nodeSelector 与 tolerations,让它在只有污点的边缘节点上运行。
  3. 写一个 ClusterPropagationPolicy 分发 Namespace,比较它与命名空间级策略的差别。

小结

  • Karmada 用一套 API 管理多集群:模板进 karmada-apiserver,策略决定放置,Work 负责下发。
  • Duplicated 是「每地全量」,Divided 是「总量拆分」;spreadConstraints 控制拓扑铺开的组数。
  • 故障转移靠集群污点、clusterTolerations、failover.application 三者配合,Graciously 才能先建后删。

延伸阅读