Skip to content

[Enhancement]: LogSet Repair 强制重建 PVC 导致 WAL 丢失 — 增加安全检查与保护机制 #590

Description

@xzxiong

关联 Issue

背景

2026-05-17 IDC 环境多节点同时重启导致 LogSet 3 副本全部短暂不可用(~1 分钟)。matrixone-operator 重启后观察到 majority failure,等待 storeFailureTimeout(10m)后触发 Repair 逻辑。但由于节点已恢复而 Pod 未能重新 schedule,operator 最终执行了 PVC 强制重建,导致 WAL 全部丢失。后续 DN 从 S3 恢复时部分 block 元数据缺失,CN 触发 panic 形成 crash loop。

当前 Operator 处理逻辑分析

Repair 路径 (pkg/controllers/logset/controller.go:217)

func (r *WithResources) Repair(ctx *recon.Context[*v1alpha1.LogSet]) error {
    minorityLimit := (*ctx.Obj.Spec.InitialConfig.LogShardReplicas) / 2
    // 安全阀 1: majority 故障时等待人工介入
    if len(ctx.Obj.Status.FailedStores) > minorityLimit {
        ctx.Log.Info("majority failure might happen, wait for human intervention")
        return nil
    }
    // 安全阀 2: 已 failover 数量达到 minority 上限
    if len(r.sts.Spec.ReserveOrdinals) >= minorityLimit {
        ctx.Log.Info("failover limit has reached, only minority failover can be safely automated")
        return nil
    }
    // 执行 failover: 通过 ReserveOrdinals 跳过故障 ordinal → kruise sts 创建新 Pod + 新 PVC
    candidate := toRepair[0]
    ordinal, _ := util.PodOrdinal(candidate.PodName)
    r.sts.Spec.ReserveOrdinals = util.Upsert(r.sts.Spec.ReserveOrdinals, ordinal)
    ctx.Update(r.sts)
}

PVC 生命周期

// pkg/controllers/logset/sts.go:142
func syncStatefulSetSpec(ls *v1alpha1.LogSet, sts *kruisev1.StatefulSet) {
    case v1alpha1.PVCRetentionPolicyDelete:
        sts.Spec.PersistentVolumeClaimRetentionPolicy = &kruisev1.StatefulSetPersistentVolumeClaimRetentionPolicy{
            WhenDeleted: kruisev1.DeletePersistentVolumeClaimRetentionPolicyType,
            WhenScaled:  kruisev1.DeletePersistentVolumeClaimRetentionPolicyType,  // scale-in 即删 PVC
        }
}

问题链条

  1. 多节点同时重启 → 3/3 log pod 不可用 → len(FailedStores)=3 > minorityLimit(1) → 进入 "wait for human intervention"
  2. 节点 ~1min 后恢复 Ready,但 Pod 未能自动恢复(或 operator 自身重启导致状态不准确)
  3. Operator 再次 reconcile 时状态更新,FailedStores 降为可处理范围后逐个 failover
  4. ReserveOrdinals 机制跳过故障 ordinal → kruise 创建新 ordinal Pod → PVCRetentionPolicy=Delete + WhenScaled=Delete → 旧 PVC 被删除
  5. 新 PVC 为空盘 → WAL 丢失 → 后续 DN/CN 从 S3 恢复不完整

S3 Bucket Reclaim Job(关联问题)

当 s3Reclaim feature gate 启用且 s3RetentionPolicy=Delete 时,集群删除还会触发一个 aws-cli Job 清理 S3 数据(pkg/controllers/bucketclaim/controller.go)。在 PVC 重建场景下,如果集群被判定不可恢复而整体重建,S3 数据可能也被一并清理,彻底断绝恢复可能。

问题总结

问题 根因 影响
节点短暂中断后 PVC 被删除 Repair 逻辑未验证旧 PVC/PV 是否仍可用 WAL 丢失,元数据不一致
majority 故障无超时升级 "wait for human intervention" 是无限等待,无 alert/event 通知 人工可能不知道需要介入
storeFailureTimeout 默认 10min 太短 节点重启场景下 Pod 恢复可能需要更长时间(拉镜像、PVC attach) 过早触发 failover
failover 后 PVC 被立即删除 WhenScaled: Delete 配合 ReserveOrdinals 跳过 ordinal 等价于 scale-in 无法回退

优化建议

P0: Repair 前增加 PVC/Node 可用性检查

func (r *WithResources) Repair(ctx *recon.Context[*v1alpha1.LogSet]) error {
    // ... existing checks ...
    candidate := toRepair[0]
    pod := getPod(candidate.PodName)
    
    // NEW: 检查 Pod 所在 Node 是否已恢复 Ready
    if node := getNode(pod.Spec.NodeName); node != nil && isNodeReady(node) {
        ctx.Log.Info("node recovered but pod still not ready, extending wait", 
            "pod", candidate.PodName, "node", pod.Spec.NodeName)
        return recon.ErrReSync("wait for pod recovery on recovered node", reSyncAfter)
    }
    
    // NEW: 检查旧 PVC 是否仍然 Bound 且可用
    pvc := getPVC(candidate.PodName)
    if pvc != nil && pvc.Status.Phase == corev1.ClaimBound {
        ctx.Log.Info("PVC still bound, pod may recover without failover",
            "pvc", pvc.Name, "pod", candidate.PodName)
        // 可以尝试删除 Pod 触发 reschedule 而非 failover
    }
    // ... existing failover logic ...
}

P1: Failover 时 PVC 保护策略

引入 failoverPVCRetentionPolicy 字段或将 failover 场景下的 PVC 从 Delete 改为 Retain:

// 方案 A: failover 前将旧 PVC annotation 保护
func retainPVCBeforeFailover(ctx, pvcName string) error {
    pvc := getPVC(pvcName)
    if pvc != nil {
        pvc.Annotations["matrixorigin.io/failover-retain"] = "true"
    }
}

// 方案 B: LogSet spec 新增字段
type LogSetSpec struct {
    // FailoverPVCRetentionPolicy controls PVC behavior during failover
    // Default: Retain (safer, requires manual cleanup)
    FailoverPVCRetentionPolicy *PVCRetentionPolicy `json:"failoverPVCRetentionPolicy,omitempty"`
}

P2: Majority 故障通知机制

func (r *WithResources) Repair(ctx *recon.Context[*v1alpha1.LogSet]) error {
    if len(ctx.Obj.Status.FailedStores) > minorityLimit {
        // NEW: emit event + condition 供告警系统感知
        ctx.Event(ctx.Obj, "Warning", "MajorityFailure", 
            fmt.Sprintf("%d/%d stores failed, manual intervention required", 
                len(ctx.Obj.Status.FailedStores), ctx.Obj.Spec.Replicas))
        ctx.Obj.Status.SetCondition(metav1.Condition{
            Type:    "MajorityFailure",
            Status:  metav1.ConditionTrue,
            Reason:  "QuorumLost",
            Message: "Majority of log stores failed, waiting for human intervention",
        })
        return nil
    }
}

P3: 增加 storeFailureTimeout 推荐值与 Backoff

  • 对于节点级故障(多 Pod 同时 down),timeout 应至少 20-30 分钟
  • 考虑引入 exponential backoff:首次 failover 后延长后续 failover 的等待时间
  • 在 webhook defaulter 中基于 replicas 数量推算推荐值

P4: S3 Bucket Reclaim 安全性

  • 当检测到 PVC 重建导致 WAL 丢失时,S3 数据应绝对保护不被清理
  • 建议在 BucketClaim 上增加 protectedUntil annotation,在 PVC 重建后设置一个保护期(如 7 天),期间不允许 S3 清理 Job 执行

复现条件

  • LogSet replicas: 3, logShardReplicas: 3
  • storeFailureTimeout: 10m0s
  • pvcRetentionPolicy: Delete
  • storageClass: directpv-ssd(或其他 local-volume)
  • 多节点同时短暂不可用(断电重启、网络分区后恢复)

期望行为

  1. 节点恢复后,operator 应优先等待 Pod 自恢复而非立即 failover
  2. 即使 failover,旧 PVC 应保留而非立即删除(至少在确认新 Pod 数据完整性后再清理)
  3. Majority failure 时应发出 K8s Event / Condition 供外部告警感知
  4. S3 数据在 PVC 重建场景下应有保护期

相关代码

文件 关键逻辑
pkg/controllers/logset/controller.go:217 Repair — failover 入口
pkg/controllers/logset/controller.go:108 Observe — StoresFailedFor 触发 Repair
pkg/controllers/logset/sts.go:142 syncStatefulSetSpec — PVCRetentionPolicy 配置
pkg/controllers/common/failover.go:35 CollectStoreStatus — 计算 AvailableStores/FailedStores
api/core/v1alpha1/logset_types.go:57 StoreFailureTimeout 定义与默认值
api/core/v1alpha1/logset_helpers.go:34 setDefaultRetentionPolicy — 默认 Delete
pkg/controllers/bucketclaim/controller.go:64 BucketClaim Finalize — S3 清理 Job
pkg/webhook/logset_webhook.go:182 S3 immutable 校验

环境信息

  • Operator 版本: submodule at commit (third/matrixone-operator)
  • 事故集群: IDC mo-benchmark-moi (idc-unit)
  • MatrixOne 版本: v3.0.11-6a0394c4f-2026-05-11

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions