Karmada 应用故障转移(Application Failover)机制全解析:从设计提案到源码实现
【免费下载链接】karmadaOpen, Multi-Cloud, Multi-Cluster Kubernetes Orchestration项目地址: https://gitcode.com/GitHub_Trending/ka/karmada
导读
本篇文章围绕 Karmada 多集群编排中的应用级故障转移(Application Failover)机制展开,该机制基于 docs/proposals/failover/application-failover.md 设计提案落地实现。与 Karmada 已有的"集群故障视角"的 failover 不同,它关注的是控制面健康但应用本身不可用的场景,允许用户通过 PropagationPolicy 声明应用不可用时的跨集群迁移行为。读完本文,你将掌握应用 failover 的触发条件、PurgeMode/TolerationSeconds等关键配置项的语义、ResourceBinding上的状态记录方式,以及该能力在karmada-controller-manager、karmada-webhook、karmada-scheduler三个组件中的实现原理。
背景:单集群自愈为何不够用
Kubernetes 本身具备容错与自愈能力:节点故障时工作负载会被迁移到其他正常节点;kubelet检测到节点资源不足并达到驱逐阈值时,会驱逐低优先级 Pod 并重新调度;节点异常或用户停机维护时,也可以基于污点(taint)驱逐机制保证 Pod 的 HA。
但上述机制无法覆盖以下场景:
- 集群资源不足导致长时间悬挂:集群内没有足够资源运行应用时,被驱逐的应用无法被重新调度,长期处于悬挂(suspended)状态。
- 资源抢占导致低优先级应用频繁被挤掉:抢占逻辑会尝试驱逐低优先级 Pod 以让高优先级 Pod 调度,若集群始终资源不足,低优先级应用会反复被抢占、长期无法正常运行。抢占的原因可能是资源短缺,也可能是 Pod 间的亲和性约束。
- 集群控制面健康但应用不可用:在集群视角的 failover 检测中,只要集群控制面正常就认为"无故障",但应用本身可能因为资源被回收、配置错误等原因持续不可用。
此时单集群 failover 无法有效解决问题,用户希望 Karmada 提供一种从应用视角出发的故障迁移手段:当应用不可用且无法在单集群内自愈时,把应用重新调度到其他集群,并避开此前运行失败的集群。
该提案的明确目标(Goals)包括:
- 扩展 PropagationPolicy API,为用户提供重调度(rescheduled)配置;
- 扩展 ResourceBinding API,记录此前被驱逐的集群;
- 给出涉及
karmada-controller-manager、karmada-webhook、karmada-scheduler三个组件的实现思路:例如 controller-manager 需要新增观察应用健康状态的组件,scheduler 需要新增过滤被驱逐集群的插件。
同时该设计保持向后兼容:旧版本 Karmada 部署的系统可以无缝迁移到新版本,旧配置 YAML 可直接应用于新版本且行为不变。
典型用户场景
场景一:抢占式调度下的低优先级应用
假设应用部署在启用了抢占式调度的集群中。当业务高峰、集群资源紧张时,原本正常运行的低优先级应用被抢占后长期无法运行,且单集群内无法自愈。用户希望尝试把它调度到另一个集群,保证服务持续可用。
场景二:云厂商竞价/Spot 实例资源被回收
Spot 实例(竞价实例)以折扣价出售但带有系统中断机制:系统可能随时收回实例。当应用部署在 Spot 实例上时,可能因资源被回收而运行失败。此场景下调度器感知到的资源量是资源配额(quota)的大小,而非实际可用资源,因此资源探测手段(如 estimator)失效。用户希望把应用调度到此前失败集群之外的其他集群。
依赖:Resource Interpreter Framework 提供健康语义
应用 failover 依赖 Karmada 的Resource Interpreter Framework:它负责解释资源结构,通过解释器(interpreter)操作告诉 Karmada 如何判断某个对象(object)的健康状态。是否触发重调度、何时触发,最终由用户通过健康定义来决定。
典型示例如下(判断 Deployment 的 readyReplicas 是否等于 spec.replicas):
apiVersion: config.karmada.io/v1alpha1 kind: ResourceInterpreterCustomization metadata: name: declarative-configuration-example spec: target: apiVersion: apps/v1 kind: Deployment healthInterpretation: luaScript: > function InterpretHealth(observedObj) return observedObj.status.readyReplicas == observedObj.spec.replicas end注意:在应用 failover 场景中,
Health字段不仅表示应用的健康状态,也是判断应用是否应被迁移的条件之一。使用 failover 时请谨慎配置该字段——健康语义定义得不准确,可能导致迁移决策错误。
设计总览:failover 行为的三个配置维度
用户通过定义策略(policy)来规定 failover 行为,配置维度包括:
- 何时触发应用 failover(触发前置条件);
- 如何从一个集群迁移到另一个集群(迁移过程中的旧应用清理方式);
- 被迁移集群何时可以恢复调度(冷却时间)。
此外还需要一些**前置条件(PreConditions)**来避免无效的 failover,例如:
- 健康状态前置:只迁移那些曾经成功运行过的应用;对于一开始就失败的应用,倾向于在单集群内解决;
- 时间前置:等待一段时间后再启动 failover,以兼容首次初始化需要额外启动时间的遗留应用,避免因误配置导致无效迁移。
同时还有执行决策条件(DecisionConditions):对于短时异常(一段时间后可自愈),不希望立刻把应用迁到其他集群导致迁移过于频繁,因此需要一个参数来容忍不健康的应用。
API 变更:PropagationPolicy 与 ResourceBinding
PropagationPolicy:新增 Failover 配置
提案中的核心 Go 类型定义如下:
// PurgeMode represents that how to deal with the legacy applications on the // cluster from which the application is migrated. type PurgeMode string const ( // Immediately represents that Karmada will immediately evict the legacy application. Immediately PurgeMode = "Immediately" // Graciously represents that Karmada will wait for the application to // come back to healthy on the new cluster or after a timeout is reached // before evicting the application. Graciously PurgeMode = "Graciously" // Never represents that Karmada will not evict the application and // users manually confirms how to clean up redundant copies. Never PurgeMode = "Never" ) // PropagationSpec represents the desired behavior of PropagationPolicy. type PropagationSpec struct { // Failover indicates how Karmada migrates applications in case of failures. // If this value is nil, failover is disabled. // +optional Failover *FailoverBehavior `json:"failover,omitempty"` // ... } // FailoverBehavior indicates failover behaviors in case of an application or cluster failure. type FailoverBehavior struct { // Application indicates failover behaviors in case of application failure. // If this value is nil, failover is disabled. // If set, the PropagateDeps should be true so that the dependencies could // be migrated along with the application. // +optional Application *ApplicationFailoverBehavior `json:"application,omitempty"` // Cluster indicates failover behaviors in case of cluster failure. // If this value is nil, failover is disabled. // +optional // Cluster *ClusterFailoverBehavior `json:"cluster,omitempty"` } // ApplicationFailoverBehavior indicates application failover behaviors. type ApplicationFailoverBehavior struct { // PreConditions indicates the preconditions of the failover process. // Currently, PreConditions includes several conditions: // - DelaySeconds (optional) // - HealthyState (optional) // +optional PreConditions *PreConditions `json:"preConditions,omitempty"` // DecisionConditions indicates the decision conditions of performing the failover process. // Only when all conditions are met can the failover process be performed. // Currently, DecisionConditions includes several conditions: // - TolerationSeconds (optional) // +required DecisionConditions DecisionConditions `json:"decisionConditions,omitempty"` // PurgeMode represents how to deal with the legacy applications on the // cluster from which the application is migrated. // Valid options are "Immediately", "Graciously" and "Never". // Defaults to "Graciously". // +kubebuilder:default=Graciously // +optional PurgeMode PurgeMode `json:"purgeMode,omitempty"` // GracePeriodSeconds is the maximum waiting duration in seconds before // application on the migrated cluster should be deleted. // Required only when PurgeMode is "Graciously" and defaults to 600s. // If the application on the new cluster cannot reach a Healthy state, // Karmada will delete the application after GracePeriodSeconds is reached. // Value must be positive integer. // +optional GracePeriodSeconds *int32 `json:"gracePeriodSeconds,omitempty"` // BlockPredecessorSeconds represents the period of time the cluster from which the // application was migrated from can be schedulable again. // During the period of BlockPredecessorSeconds, clusters are forcibly filtered out by the scheduler. // Defaults to 600s. Zero means the cluster will never be schedulable. // +kubebuilder:default=600 // +optional BlockPredecessorSeconds *int32 `json:"blockPredecessorSeconds,omitempty"` } // PreConditions represents the preconditions of the failover process. type PreConditions struct { // DelaySeconds refers to a period of time after the control plane collects // the status of the application for the first time. // If specified, the failover process will be started after DelaySeconds is reached. // +optional DelaySeconds *int32 `json:"delaySeconds,omitempty"` // HealthyState refers to the healthy status reported by the Karmada resource interpreter. // Valid options are "Healthy". // If specified, the failover process will be started when the application reaches the healthy state. // +optional HealthyState ResourceHealth `json:"healthyState,omitempty"` } // DecisionConditions represents the decision conditions of performing the failover process. type DecisionConditions struct { // TolerationSeconds represents the period of time Karmada should wait // after reaching the desired state before performing failover process. // If not specified, Karmada will immediately perform failover process. // Defaults to 10s. // +kubebuilder:default=10 // +optional TolerationSeconds *int32 `json:"tolerationSeconds,omitempty"` }关键配置项语义汇总:
| 配置项 | 所属 | 默认值 | 作用 |
|---|---|---|---|
preConditions.delaySeconds | 前置条件 | 无 | 控制面首次收集到应用状态后,等待该时长再启动 failover;可与 HealthyState 同时使用、互不影响 |
preConditions.healthyState | 前置条件 | 无 | 仅当应用达到指定健康状态(如Healthy)后才启动 failover |
decisionConditions.tolerationSeconds | 决策条件 | 10s | 达到期望状态后 Karmada 等待的时长,用于容忍短时异常,避免频繁迁移;不设置则立即执行 |
purgeMode | 迁移方式 | Graciously | 旧集群上的遗留应用如何处理,可选Immediately/Graciously/Never |
gracePeriodSeconds | 迁移方式 | 600s | 仅在Graciously模式下生效:等待新集群应用达到 Healthy 的最大时长,超时后删除旧应用,必须为正整数 |
blockPredecessorSeconds | 冷却 | 600s | 被迁移的原集群在多长时间内不可再次调度;0 表示该集群永远不可调度 |
提案中的实现说明:
PreConditions和BlockPredecessorSeconds在 release-1.6 中暂不实现,因为 ResourceBinding 针对这些字段的 API 变更尚未就绪。
对应的 PropagationPolicy 配置示例:
apiVersion: policy.karmada.io/v1alpha1 kind: PropagationPolicy metadata: name: nginx-propagation spec: resourceSelectors: - apiVersion: apps/v1 kind: Deployment name: nginx - apiVersion: apps/v1 kind: StatefulSet name: mysql failover: application: preConditions: delaySeconds: 5000 decisionConditions: tolerationSeconds: 200 healthyState: - Unhealthy placement: clusterAffinity: clusterNames: - member1 - member2ResourceBinding:记录驱逐历史与 failover 配置
ResourceBinding 是调度决策的直接载体,需要记录"哪些集群被驱逐过"以及"继承自策略的 failover 配置":
type ResourceBindingSpec struct { ... // ActiveEvictionHistory represents the eviction history which might affect the scheduler's // scheduling decisions. // The scheduler tends not to schedule applications on clusters with a history of eviction. // +optional ActiveEvictionHistory []ActiveEvictionHistory `json:"activeEvictionHistory,omitempty"` // FailoverBehavior represents the failover config for the referencing resource. // It inherits directly from the associated PropagationPolicy(or ClusterPropagationPolicy). // +optional FailoverBehavior *policyv1alpha1.FailoverBehavior `json:"failoverBehavior,omitempty"` ... } // AggregatedStatusItem represents status of the resource running in a member cluster. type AggregatedStatusItem struct { // ClusterName represents the member cluster name which the resource deployed on. // +required ClusterName string `json:"clusterName"` // Status reflects running status of current manifest. // +optional Status *runtime.RawExtension `json:"status,omitempty"` // Applied represents if the resource referencing by ResourceBinding or ClusterResourceBinding // is successfully applied on the cluster. // +optional Applied bool `json:"applied,omitempty"` // Settled represents if the resource referencing by ResourceBinding or ClusterResourceBinding // is once Healthy on the cluster. // +optional Settled bool `json:"settled,omitempty"` // AppliedMessage is a human readable message indicating details about the applied status. // +optional AppliedMessage string `json:"appliedMessage,omitempty"` // Health represents the healthy state of the current resource. // There maybe different rules for different resources to achieve health status. // +kubebuilder:validation:Enum=Healthy;Unhealthy;Unknown // +optional Health ResourceHealth `json:"health,omitempty"` // CreationTimestamp is a timestamp representing the server time when this AggregatedStatusItem was created. // It represents the timestamp when the control plane first collects status of the resource running in a member cluster. // Populated by the system. Read-only. // +optional CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"` } type ActiveEvictionHistory struct { // ClusterName represents the evicted cluster name. // +required ClusterName string `json:"clusterName,omitempty"` // CreationTimestamp is a timestamp representing the server time when this cluster was // evicted. After specific reset timeout, the evicted cluster will become schedulable again. // Populated by the system. Read-only. // +optional CreationTimestamp metav1.Time `json:"creationTimestamp,omitempty"` }对应的 ResourceBinding 示例(可见activeEvictionHistory记录成员集群 member2 曾在2023-04-03T01:44:31Z被驱逐,调度器据此避开该集群):
apiVersion: work.karmada.io/v1alpha2 kind: ResourceBinding metadata: name: nginx-pod spec: clusters: - name: member1 activeEvictionHistory: - clusterName: member2 createTimestamp: "2023-04-03T01:44:31Z" failover: application: preConditions: delaySeconds: 5000 decisionConditions: tolerationSeconds: 200 healthyState: - Unhealthy placement: clusterAffinity: clusterNames: - member1 - member2 resource: apiVersion: v1 kind: Pod name: nginx namespace: default schedulerName: default-scheduler status: aggregatedStatus: - applied: true settled: true clusterName: member1 health: Healthy status: phase: Running createTimestamp: "2023-04-03T02:44:31Z"组件变更与源码实现
karmada-controller-manager:新增应用 failover 控制器
控制器需要监听ResourceBindingStatus中Health字段的变化,并按照容忍时间(toleration time)重新入队(requeue)。当检测到应用连续两次不健康(意味着在一段时间内持续不健康)时,触发原有驱逐逻辑。
这一设计在源码中落地为独立的 failover 控制器,相关实现见 pkg/controllers/applicationfailover:
rb_application_failover_controller.go:ResourceBinding的应用 failover 控制器,控制器名为resource-binding-application-failover-controller;crb_application_failover_controller.go:ClusterResourceBinding的对应控制器;common.go:共享的驱逐任务构建逻辑。
其核心工作原理(从源码结构看):
- 控制器通过
workloadUnhealthyMap记录"某个资源在哪些集群上处于不健康状态"及其起始时间戳; detectFailure遍历不健康集群:若某集群上的工作负载不健康持续时间超过tolerationSeconds,且该集群不在GracefulEvictionTasks中,则将该集群加入待驱逐列表;否则计算还需等待多久并返回RequeueAfter以便到时再检查;Reconcile根据返回值决定立即驱逐或RequeueAfter定时重试(见 rb_application_failover_controller.go)。
驱逐任务通过buildTaskOptions构造(见 common.go):携带驱逐原因EvictionReasonApplicationFailure、PurgeMode以及(在Graciously/Gracefully模式下)GracePeriodSeconds;当PurgeMode要求优雅驱逐但GracefulEviction特性门控未开启时,会直接报错提示用户启用该特性门控。
karmada-webhook:校验误导性配置
由于 API 新增了字段,webhook 需要执行额外的校验工作,防止用户配置出误导性的组合(例如配置了PurgeMode=Graciously却未开启相应特性门控等)。
karmada-scheduler:新增过滤被驱逐集群的插件
调度器需要新增 filter 插件,过滤掉处于驱逐历史中的集群,避免重调度时再次调度到这些集群。该插件在源码中即为 pkg/scheduler/framework/plugins/clustereviction/cluster_eviction.go 中的ClusterEviction插件:它在Filter阶段检查目标集群是否处于GracefulEvictionTasks(即正在被驱逐),若是则返回Unschedulable,拒绝将工作负载调度到该集群。
FAQ 与设计取舍
与 karmada-descheduler 的区别是什么?
两者关注点不同:
- karmada-descheduler 关注
Replicas(副本数):它根据 estimator 探测到的集群资源余量,把挂起的副本重新调度到其他集群,保留健康副本。 - 本提案关注整个
Application(应用):当应用在某集群整体不可用时,把全部副本移除并整体迁移到新集群。
举例:某 Deployment 部署在多个集群,状态如下(member1 不健康、member2 健康):
status: aggregatedStatus: - applied: true clusterName: member1 health: Unhealthy status: replicas: 2 availableReplicas: 1 - applied: true clusterName: member2 health: healthy status: replicas: 1 availableReplicas: 1karmada-descheduler 倾向于保留健康副本、把挂起的副本调度到其他集群:
status: aggregatedStatus: - applied: true clusterName: member1 health: healthy status: replicas: 1 availableReplicas: 1 - applied: true clusterName: member2 health: healthy status: replicas: 2 availableReplicas: 2而应用 failover 倾向于移除全部副本并把应用整体迁移到新集群:
status: aggregatedStatus: - applied: true clusterName: member3 health: healthy status: replicas: 2 availableReplicas: 2 - applied: true clusterName: member2 health: healthy status: replicas: 1 availableReplicas: 1更重要的是:karmada-descheduler 依赖 estimator 探测集群资源余量,因此无法解决"非资源不足引起的错误",也无法应对资源无法被正确探测的场景(如 serverless 计算)。本提案基于"过去运行失败的尝试"进行重调度,不依赖 estimator,因此没有上述限制。
如何判断失败能否通过重调度解决?
只有用户能判断failover 能否通过重调度解决。用户可以通过 Resource Interpreter Framework 自定义资源对象的健康定义,决定何时触发重调度。极端情况下,如果应用因配置错误而不可用、且其状态无法识别该错误,迁移到任何集群可能都无法解决问题——用户应通过策略配置来决定是否重调度以及相应的重调度配置。
替代方案:基于 Condition 的 Gate 触发
在设计过程中曾考虑用资源 Condition 作为触发条件(InitiateGates/ActiveGates)。但对于同时命中多个资源的策略,期望行为应作用于所有命中资源,而不同资源有不同的 Condition。因此当使用 Condition 作为触发条件时,无法解决多应用场景——需要在策略中增加apiVersion/kind来区分不同的资源对象。
该替代方案的示意结构:
type PropagationSpec struct { // Failover []FailoverBehavior `json:"failover,omitempty"` // 变为列表,每个元素绑定具体资源类型 // ... } type FailoverBehavior struct { APIVersion string `json:"apiVersion,omitempty"` Kind string `json:"kind,omitempty"` Behavior *Behavior `json:"behavior,omitempty"` } type Behavior struct { Application *ApplicationFailoverBehavior `json:"application,omitempty"` Cluster *ClusterFailoverBehavior `json:"cluster,omitempty"` } type ApplicationFailoverBehavior struct { InitiateGates []FailoverInitiateGate `json:"initiateGates,omitempty"` // 前置条件门 ActiveGates []FailoverActiveGate `json:"activeGates"` // 执行条件门(MinItems=1) TolerationSeconds int32 `json:"tolerationSeconds,omitempty"` // 默认 10s PurgeMode string `json:"purgeMode,omitempty"` // Immediately/Graciously/Never EscapeSeconds int32 `json:"escapeSeconds,omitempty"` // 默认 600s } type FailoverInitiateGate struct { ConditionType string `json:"conditionType,omitempty"` HealthyState string `json:"healthyState,omitempty"` // Healthy/Unknown } type FailoverActiveGate struct { ConditionType string `json:"conditionType,omitempty"` HealthyState string `json:"healthyState,omitempty"` // Unhealthy/Unknown }对应的示例配置:
apiVersion: policy.karmada.io/v1alpha1 kind: PropagationPolicy metadata: name: nginx-propagation spec: resourceSelectors: - apiVersion: apps/v1 kind: Deployment name: nginx - apiVersion: apps/v1 kind: StatefulSet name: mysql failover: - apiVersion: apps/v1 kind: Deployment behavior: application: activeGates: - healthyState: Unhealthy - apiVersion: apps/v1 kind: StatefulSet behavior: application: activeGates: - healthyState: Unhealthy placement: clusterAffinity: clusterNames: - member1 - member2最终提案选择了更简洁的"全局 failover 配置 + 健康状态门控"方案,避免了在策略中为每种资源重复声明配置的复杂度。
当前仓库中的实现演进
对照 pkg/apis/policy/v1alpha1/propagation_types.go,可以发现实现相对提案已发生如下演进(可作为理解代码现状的参考):
PurgeMode的枚举值从提案初期的Immediately/Graciously演进为当前的Directly/Gracefully/Never,其中PurgeModeDirectly直接驱逐旧应用,PurgeModeGracefully等待新集群应用恢复健康或超时后再驱逐(见 propagation_types.go);ApplicationFailoverBehavior当前保留DecisionConditions(必填)、PurgeMode、GracePeriodSeconds,并新增了StatePreservation——在启用StatefulFailoverInjection(alpha)特性门控时,可在 failover 期间提取并恢复有状态应用的状态数据(见 propagation_types.go);DecisionConditions.TolerationSeconds默认值从提案的 10s 调整为300s,且明确"设置为 0 表示立即执行 failover"(见 propagation_types.go);- 同时补充了
ClusterFailoverBehavior,用于定义集群故障时的 failover 行为;若不设置,则由 controller 的no-execute-taint-eviction-purge-mode参数控制; - ResourceBinding 侧,
Failover字段(继承自 PropagationPolicy/ClusterPropagationPolicy)已落地于 pkg/apis/work/v1alpha2/binding_types.go。
测试计划与总结
该功能按照 Karmada 的标准质量要求覆盖Unit Test与E2E Test:单元测试覆盖applicationfailover控制器(如 rb_application_failover_controller_test.go)、ClusterEviction调度插件(如 cluster_eviction_test.go)等;E2E 测试则验证完整的"应用不健康 → 容忍期结束 → 驱逐并迁移 → 原集群冷却"链路。
总结来说,Karmada 的应用 failover 机制为"集群健康但应用不可用"的跨集群场景提供了完整解决方案:通过 PropagationPolicy 声明式配置触发前置条件、容忍时长、旧应用清理模式与集群冷却时间;通过 ResourceBinding 记录驱逐历史并继承 failover 配置;由 controller-manager 的健康观察控制器、webhook 的配置校验和 scheduler 的ClusterEviction过滤插件协作完成整个迁移闭环。它与 karmada-descheduler 形成互补:前者处理副本级资源再平衡,后者处理应用级整体迁移,且不依赖 estimator 的资源探测能力。
【免费下载链接】karmadaOpen, Multi-Cloud, Multi-Cluster Kubernetes Orchestration项目地址: https://gitcode.com/GitHub_Trending/ka/karmada
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考