主备角色漂移与副本一致性检查
当管理面记录的期望主备关系,与两个下游集群里的真实工作负载副本分布不一致时,operator 会写入 RoleDrift condition。对于无法安全解释的漂移,实例会进入 Failed,status.reason=RoleDriftDetected。
典型报错:
expectedPrimary=suse-2 role=Standby workloads=1 nonZeroWorkloads=0 zeroWorkloads=1 desiredReplicas=0;
expectedSecondary=local role=Standby workloads=1 nonZeroWorkloads=0 zeroWorkloads=1 desiredReplicas=0;
both clusters are scaled to zero
这表示管理面认为 suse-2 是当前主集群、local 是当前备集群,但 operator 在两个集群中都只采样到 replicas=0 的工作负载。两边都没有活跃业务副本,因此实例会被判定为 BothStandby。
管理面期望从哪里来
RoleDrift 不使用 DisasterConfig.spec.sourceCluster/targetCluster 直接推导主备,因为 Failover 和 Reprotect 会改变当前保护方向。
当前期望关系固定来自实例状态:
expectedPrimary = DisasterInstance.status.primaryCluster
expectedSecondary = DisasterInstance.status.secondaryCluster
因此,文案里的 expectedPrimary=suse-2 表示 实例 status 当前认为 suse-2 应该承载业务副本;并不一定等于基础配置中的 source cluster。
下游真实角色怎么判断
operator 会分别连接 expectedPrimary 和 expectedSecondary 指向的下游集群,在实例保护范围内采样:
- Deployment
- StatefulSet
采样范围受这些字段限制:
DisasterInstance.spec.namespacesDisasterInstance.spec.labelSelector
每个集群会计算:
| 字段 | 含义 |
|---|---|
workloads | 被采样到的 Deployment/StatefulSet 数量。 |
nonZeroWorkloads | spec.replicas > 0 的工作负载数量。 |
zeroWorkloads | spec.replicas = 0 的工作负载数量。 |
desiredReplicas | 所有采样工作负载的 spec.replicas 总和。 |
真实角色按 spec.replicas 判断:
| 真实角色 | 判定 |
|---|---|
Active | 至少一个采样工作负载的期望副本数大于 0。 |
Standby | 有采样工作负载,但所有期望副本数都是 0。 |
Unknown | 没有采样到工作负载,或下游集群不可达,或 list 失败。 |
如果 spec.replicas 字段为空,按 Kubernetes 默认值 1 处理。
判定矩阵
| expectedPrimary 真实角色 | expectedSecondary 真实角色 | condition | 实例状态 | reason | 说明 |
|---|---|---|---|---|---|
Active | Standby | RoleDrift=False | 保持当前稳态 | ExpectedRoleMatched | 真实副本分布与期望主备一致。 |
Standby | Active | RoleDrift=True | Failed | RoleReversed | 真实业务面看起来在期望备集群上,和管理面期望相反。 |
Standby | Standby | RoleDrift=True | Failed | BothStandby | 两边都缩 0,没有活跃业务面。 |
Active | Active | RoleDrift=False | 保持当前稳态 | BothActiveObserved | 双活只作为观测信号,不直接判错。跳过源端缩容的 Failover 可能产生这种形态。 |
Unknown | 任意 | RoleDrift=Unknown | 保持当前稳态 | CheckFailed / NoWorkloadObserved | 无法可靠判断,不因一次观测失败直接置 Failed。 |
| 任意 | Unknown | RoleDrift=Unknown | 保持当前稳态 | CheckFailed / NoWorkloadObserved | 同上。 |
为什么 BothStandby 要报错
Pilot Light 模式下,正常保护态应满足:
- 当前主集群:业务工作负载有非 0 副本。
- 当前备集群:ResourceSync 恢复资源骨架,工作负载保持
replicas=0。
如果两个集群都变成 Standby,系统无法确认业务是否仍有可服务的副本。继续执行 Failover、Reprotect、手动同步等操作可能扩大影响,因此 operator 会:
- 写入
status.conditions[type=RoleDrift]。 - 设置
status.fsmState=Failed。 - 设置
status.reason=RoleDriftDetected。 - 清空
status.availableOperations,阻断会改变运行期语义的操作。 - 记录 Warning 事件
RoleDriftDetected。
server 不重新计算这个结论,只把 operator 写入的 conditions 和 conditionSummary.roleDrift 回显给前端。
相关代码逻辑
operator 逻辑:
| 代码位置 | 作用 |
|---|---|
internal/controller/disasterinstance/role_drift.go | 采样下游副本、判定 RoleDrift、写 condition。 |
evaluateRoleDrift | 读取 status.primaryCluster/status.secondaryCluster,分别采样两个集群。 |
sampleClusterReplicaRole | 在实例命名空间和标签范围内 list Deployment/StatefulSet,并按 spec.replicas 归类。 |
guardByRoleDrift | 在不可安全解释的漂移时把实例置为 Failed。 |
handleProtected / handleActive | 在稳态下调用 guardByRoleDrift。 |
handleFailed | reason=RoleDriftDetected 时继续复检,真实关系恢复后自动回到 Protected 或 Active。 |
server 回显逻辑:
| 代码位置 | 作用 |
|---|---|
internal/apis/disaster_instance/v1/types.go | 把 status.conditions 转成接口字段 conditions,并提取 conditionSummary.roleDrift。 |
排查步骤
1. 查看实例状态
kubectl -n disaster-system get disasterinstance <instance-name> -o yaml
重点看:
status:
fsmState: Failed
reason: RoleDriftDetected
message: ...
primaryCluster: suse-2
secondaryCluster: local
conditions:
- type: RoleDrift
status: "True"
reason: BothStandby
message: ...
2. 确认期望主备
kubectl -n disaster-system get disasterinstance <instance-name> \
-o jsonpath='{.status.primaryCluster}{"\n"}{.status.secondaryCluster}{"\n"}'
第一行是当前期望主集群,第二行是当前期望备集群。
3. 查看两个下游集群的副本
在期望主集群和期望备集群分别执行:
kubectl --context <cluster-context> -n <protected-namespace> \
get deploy,sts \
-o custom-columns=KIND:.kind,NAME:.metadata.name,REPLICAS:.spec.replicas,READY:.status.readyReplicas
如果实例配置了标签筛选器,还要带上同样的 selector:
kubectl --context <cluster-context> -n <protected-namespace> \
get deploy,sts -l '<label-selector>'
4. 对照报错摘要
示例:
expectedPrimary=suse-2 role=Standby workloads=1 nonZeroWorkloads=0 zeroWorkloads=1 desiredReplicas=0
含义是:
suse-2是管理面期望主集群。- operator 在
suse-2采样到 1 个工作负载。 - 该工作负载期望副本为 0。
suse-2被判断为Standby,不符合“期望主集群应为 Active”的稳态要求。
修复原则
不要直接手改 DisasterInstance.status.primaryCluster/secondaryCluster 或 condition。正确做法是先确认真实业务面,再让下游副本分布与当前期望主备重新一致。
常见处理:
| 场景 | 处理方向 |
|---|---|
BothStandby,期望主集群应继续承载业务 | 在期望主集群恢复 Deployment/StatefulSet 副本到原始值,期望备集群保持 0。 |
RoleReversed,实际业务在期望备集群 | 先确认是否刚经历失败的 Failover/Reprotect,避免流量误切;按运维决策把副本恢复到实例当前期望方向,或执行受控的人工恢复流程。 |
BothActiveObserved | 不会使实例 Failed,但需要确认这是否来自显式跳过源端缩容;如果不是预期双活,应尽快收敛流量和副本。 |
Unknown | 先修复集群连接、权限、命名空间或 selector,避免误判。 |
当真实副本恢复为:
expectedPrimary = Active
expectedSecondary = Standby
operator 会在后续调谐中自动把 RoleDrift=False,并在实例因 RoleDriftDetected 进入 Failed 的情况下自动恢复到稳态:
- 当前主备方向等于基础配置方向时,恢复为
Protected。 - 当前主备方向是 Failover 后方向时,恢复为
Active。
预防建议
- Failover、Reprotect、Undo 执行期间,不要在下游集群手动缩放同一批 Deployment/StatefulSet。
- 如果必须人工缩放,先记录当前实例的
primaryCluster/secondaryCluster,并确保最终只有当前主集群保留非 0 副本。 - 演练使用独立演练命名空间,不要污染生产 standby 命名空间。
- 对生产切换,保留外部流量控制记录,避免“副本已恢复但流量仍指向旧集群”的二次漂移。
- 出现
RoleDriftDetected后先保留事件和 operation 记录,再处理副本,不要清理证据。