Skip to content

CNClaim Finalize 卡死 + Scale-in 选中迁移中 claim (dev freetier-01, 日志确认) #591

Description

@xzxiong

关联

现象

2026-05-31 在 new-dev freetier-01 观察到 CNClaim 错乱:

1. CNClaim 绑定到已不存在的 Pod

CNClaim: ws-bf2d347f-dedicated-dedicated-mxxhz
  spec.podName: cn-zkskr     ← Pod 已不存在
  status.phase: Bound        ← 未变为 Lost

2. Pod label 与 CNClaim spec 不一致(双重引用)

Pod cn-n6zb9:
  label claimed-by = ws-bf2d347f-dedicated-dedicated-mxxhz   ← Pod label
  MIGRAT = y                                                  ← migration-requested

CNClaim sys-sys-hpjqv:
  spec.podName = cn-n6zb9    ← spec 引用同一 Pod
  status.phase = Bound

日志确认的时间线

从 DP Loki 日志(unit-agent + matrixone-operator)完整还原:

时间 (UTC) 组件 事件
05-30 11:20:18 operator start bind cn claimCN bind success 绑到 cn-rvghl
05-30 11:23:50 unit-agent consolidator 触发迁移,migrate CN pod=cn-rvghl claim=mxxhz
05-30 11:23:50 operator migrate claimed pod source=cn-rvghl target=cn-n6zb9
05-30 11:24:06 operator start draining source pod pod=cn-rvghl
05-30 11:24:06~25:xx operator 迁移进行中(source draining)
05-30 11:26:04 operator sort claims to scale-in — CNClaimSet replicas 减少,选中 mxxhz 删除
05-30 11:26:04 operator finalize CNClaim name=mxxhz
05-30 11:26:04~27:08 operator Finalize 循环重试 60+ 次(每秒 1-2 次)无法完成
05-30 11:30:05~11:46+ unit-agent 仍在重试 migrate CN pod=cn-n6zb9 claim=mxxhz(每 5s)
05-31 14:xx unit-agent scaling-collector 持续报 Failed to get Pod cn-zkskr not found

根因

核心问题:CNClaimSet scale-in 与 CNClaim migration 竞态

Bug 1 (主因): Finalize() 在迁移中途被调用后卡死

Finalize() 逻辑 (controller.go:340-371):

func (r *Actor) Finalize(ctx) (bool, error) {
    ownedCNs = ListPods(phase=Bound, claimed-by=self)
    if len(ownedCNs) == 0 {
        return true, nil  // finalize 完成
    }
    claimIndex = buildPodClaimIndex(...)  // #587 fix
    for each owned pod:
        if holder, ok := claimIndex[pod.Name]; ok {
            skip  // #587: 其他 claim spec 引用该 Pod,不回收
        } else {
            reclaimCN(pod)
        }
    return false, nil  // ← 还有 owned CN 未处理
}

场景:

  1. cn-n6zb9ensureOwnership() 打上 claimed-by=mxxhz + phase=Bound
  2. buildPodClaimIndex 发现 sys-sys-hpjqv.spec.podName=cn-n6zb9正确 skip reclaim
  3. ownedCNs 非空 → Finalize() 返回 (false, nil)永远无法完成

#587 fix 的漏洞:正确防止了误 reclaim,但 Finalize 无法判定 "这个 Pod 不归我管了" 并完成 finalizer 移除。

Bug 2: CNClaimSet scale-in 不排除正在迁移的 claim

cnclaimset/controller.go sort claims to scale-in 逻辑没有排除 spec.sourcePod != nil 的 claim。结果在迁移进行到一半时就删除 claim,产生:

  • Finalize 与 migration 的冲突
  • Pod label 残留
  • unit-agent migrator 无限重试

Bug 3: Sync() Pod NotFound 时不清理 spec.podName (controller.go:264-269)

if apierrors.IsNotFound(err) {
    c.Status.Phase = v1alpha1.CNClaimPhaseLost
    return nil  // spec.podName 未清空,claim 永远卡在 Lost
}

Bug 4: watchPodChange Pod 删除事件关联不完整

Pod 删除时如果 claimed-by label 已被修改/删除(如迁移中 reclaimCN() 调用),则不会触发关联 claim 的 reconcile。

修复建议

Fix 1 (优先): Finalize() skip 后清理 Pod label 并正确完成

for i := range ownedCNs {
    cn := ownedCNs[i]
    if holder, ok := claimIndex[cn.Name]; ok {
        ctx.Log.Info("skip reclaim, pod claimed by other", "pod", cn.Name, "holder", holder)
        // FIX: 清理自己的 claimed-by label(claim 正在删除)
        ctx.Patch(&cn, func() error {
            delete(cn.Labels, v1alpha1.PodClaimedByLabel)
            return nil
        })
        continue  // 视为已处理
    }
    // ... reclaimCN
}
// FIX: 如果所有 ownedCNs 都被 skip/处理完,应返回 (true, nil) 允许 finalizer 移除
return true, nil

Fix 2: CNClaimSet scale-in 排除迁移中 claim

func (r *Actor) scaleIn(ctx, oc, count) error {
    // 过滤掉正在迁移的 claim
    candidates := filterNonMigrating(oc.claims)
    sort(candidates)
    toDelete := candidates[:count]
    ...
}

func filterNonMigrating(claims []CNClaim) []CNClaim {
    var result []CNClaim
    for _, c := range claims {
        if c.Spec.SourcePod == nil {
            result = append(result, c)
        }
    }
    return result
}

Fix 3: Sync() Pod NotFound 清理 spec.podName

if apierrors.IsNotFound(err) {
    if c.Status.BoundTime != nil && time.Since(c.Status.BoundTime.Time) < waitCacheTimeout {
        return recon.ErrReSync("wait cache", waitCacheTimeout)
    }
    c.Status.Phase = v1alpha1.CNClaimPhaseLost
    ctx.Patch(c, func() error {
        c.Spec.PodName = ""
        c.Spec.NodeName = ""
        return nil
    })
    return nil
}

Fix 4: watchPodChange 补充 spec.podName 关联

Pod 删除事件应额外遍历 CNClaim 列表检查 spec.podName 匹配,不仅依赖 Pod label。

复现方式

可构造 UT:

  1. 创建 CNClaimSet replicas=2
  2. 创建 2 个 claim (A, B),A 绑 pod-1,B 绑 pod-2
  3. 触发 A 迁移(设 sourcePod=pod-1, podName=pod-2)— 此时 pod-2 被 A 和 B 双重引用
  4. CNClaimSet replicas=1,触发 scale-in 选中 A
  5. 验证 Finalize 是否卡死
  6. 验证 pod-2 的 claimed-by label 是否残留

环境信息

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