集群持续报 EVS 云盘元数据同步失败,排查定位到是废弃 Kafka 残留的僵尸 PV/PVC 指向了已被删除的云盘。就在我以为万事俱备、准备执行清理时,删除前的最后一道终验揪出了一个假阴性------集群里还有一套同名的活 Kafka 集群。本文记录完整排查链路、我自己挖的坑,以及华为云 CCE 存储体系里几个值得记住的冷知识。
01 一条每 5 分钟刷一次的 Warning
那天上午,控制台事件里躺着一堆聚合报错:
text
(combined from similar events): Failed to sync evs metadatas: add evs metadata failed:
failed to send csi request to add metadata to disk a1585c62-xxxx-xxxx-xxxx-xxxx
rpc error: code = Unknown desc = failed to add evs metadata,
evs disk a1585c62-xxxx-xxxx-xxxx-xxxx doesn't exist
拆解这条报错的三个信息点:
- Failed to sync evs metadatas ------ CCE 的云盘 CSI 驱动(everest)在周期性同步 EVS 云盘的元数据
- doesn't exist ------ 华为云 API 明确返回:这块盘不存在了
- combined from similar events ------ K8s 事件聚合机制,说明这不是一次性抖动,它在反复发生
作为单人运维,我的第一反应永远是三连问:影响业务吗?是不是误删?要不要回滚?
先定性:报错来自 controller 侧的后台同步,不触碰任何运行中的负载,当前零业务影响。但它揭示了一个事实------集群里有个存储对象在指向一块已经消失的盘。这事必须查清。
02 定位:谁在引用一块不存在的盘?
一条 jsonpath 把全集群 PV 按底层盘 ID 拉出来对账:
bash
kubectl get pv -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.csi.volumeHandle}{"\t"}{.spec.persistentVolumeReclaimPolicy}{"\t"}{.status.phase}{"\n"}{end}' | grep a1585c62
输出关键信息:
text
pvc-864bf0c0-xxxx a1585c62-xxxx Retain Bound
两个状态值得玩味:
- Retain ------ 删 PVC 不会联动删盘。说明盘是被人单独手动删掉的,不是走 PVC 删除的正常链路
- Bound ------ 对应的 PVC 对象还活着,还绑在这个 PV 上
顺着查 PVC:
bash
kubectl get pvc -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,PV:.spec.volumeName,CREATED:.metadata.creationTimestamp' | grep 864bf0c0
text
middleware-test data-kafka-controller-1 pvc-864bf0c0-xxxx 2026-04-10
是个 Kafka 的数据盘,4 月份创建的。但这里有个矛盾点:正在被 Pod 挂载的 EVS 盘受云厂商保护,删不掉。现在盘没了,反推可知------不可能有任何 Pod 真正挂着它。这大概率是个孤儿 PVC。
03 盘点:不止一块盘
用事件消息把所有同类报错的盘 ID 全量捞出来:
bash
kubectl get events -A -o jsonpath='{range .items[*]}{.message}{"\n"}{end}' | grep -o "evs disk [a-f0-9-]*" | sort | uniq -c
3 块盘全部报 doesn't exist。再一看 StatefulSet:
text
kafka-controller 0/0 139d ← 4月建的旧集群,已缩容到 0,对象没删
kafka-test-controller 1/1 6d19h ← 6天前新上的测试集群
时间线拼图完成:4 月部署的旧 Kafka 集群 → 后来废弃缩容到 0 → 6 天前一波"Kafka 治理"(新集群上线)→ 旧集群 3 块空闲状态的盘被顺手删了 → PV/PVC 对象残留在集群里,每 5 分钟被 csi-evs 摸一次报一次错。
3 块盘正好对上旧集群的 3 个副本,数据盘随集群废弃一同消失。清理方案很快定下来:
bash
kubectl delete sts kafka-controller -n middleware-test
kubectl delete pvc data-kafka-controller-0 data-kafka-controller-1 data-kafka-controller-2 -n middleware-test
kubectl delete pv pvc-c4914973 pvc-864bf0c0 pvc-7c5fdbee
删除前照例三问:误删? ------只删废弃对象,已验证无 Pod 挂载;业务? ------数据已随盘消失,删对象零新增损失;回滚?------无可回滚,值得留的盘早已不存在。
看起来万事俱备。直到最后一道终验。
04 终验:救命的最后一道关
删除 PVC 前,我跑了三类引用核查:运行中的 Pod、工作负载模板、CronJob 模板。其中 Pod 实体扫描是这样:
bash
kubectl get pods -A -o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{range .spec.volumes[*]}{.persistentVolumeClaim.claimName}{","}{end}{"\n"}{end}' | grep data-kafka-controller
输出让我后背一凉:
text
middleware kafka-controller-0 data-kafka-controller-0,,,,,,,
middleware kafka-controller-1 data-kafka-controller-1,,,,,,,
middleware kafka-controller-2 data-kafka-controller-2,,,,,,,
三个 Pod 正在运行,而且挂着 data-kafka-controller-0/1/2!
这里必须先认个错:我第一轮的"无引用"扫描,grep 用的是 PV 的 uid ,而 Pod 输出的是 PVC 名------两者永远匹配不上,输出为空给了我虚假的安全感。假阴性是我自己挖的坑,这次是被"删除前再扫一遍"的流程救的。
冷静下来看 namespace------命中的三行全部在 middleware ,而我要清理的对象全在 middleware-test。
K8s 的一条铁则在这里起了决定性作用:Pod 只能挂载同 namespace 的 PVC。
也就是说,集群里存在两套同名 Kafka 集群 :middleware 里有一套活的,middleware-test 里有一套废弃的------它们用同一个 StatefulSet 名字、同一套 PVC 命名模式(volumeClaimTemplate 生成的 data-<sts名>-<序号>),全靠 namespace 隔离,井水不犯河水。
05 同名集群,两个世界
接下来做边界对账,把两套集群的存储彻底钉死:
bash
# middleware(活集群)的 PVC 绑定的 PV
kubectl get pvc -n middleware -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.volumeName}{"\n"}{end}' | grep data-kafka-controller
# 再查这些 PV 的底层盘 ID
kubectl get pv <PV名> -o custom-columns='NAME:.metadata.name,DISK:.spec.csi.volumeHandle'
text
middleware 活集群: 盘 af26abbb / 40cd901a / 730868a5 (健康,挂载中)
middleware-test 废弃: 盘 a1585c62 / c6f674a6 / ee992f8e (已消失,报错来源)
两组盘 ID 零交集。PV 和云盘是通过 volumeHandle 一对一映射的,PV uid 不相交 = 盘必然不相交------这不是概率推断,是结构性事实。
还有一个漂亮的旁证:middleware 活集群的 kafka-controller-1 Pod 六天前刚重启过,并成功重新挂载了数据盘------挂载中的盘受云厂商保护删不掉,它活得好好的。
边界确认,放行执行。全程命令显式带 -n middleware-test,这个参数就是防同名误伤的最后防线。
06 执行:三步清理与一个 NotFound
bash
kubectl delete sts kafka-controller -n middleware-test # deleted
kubectl delete pvc data-kafka-controller-0 data-kafka-controller-1 data-kafka-controller-2 -n middleware-test # deleted ×3
kubectl delete pv pvc-c4914973 pvc-864bf0c0 pvc-7c5fdbee # NotFound ×3 !
第三步报 NotFound 不是错误------标准 K8s 下 Retain 策略的 PV 在 PVC 删除后应该变成 Released 留下来等手动清理,但 CCE 的 everest 插件在联动删除 PV 对象,我跑到第三步时它们已经没了。
这是华为云 CCE 和原生 K8s 行为的一个真实差异点,记下来,以后清存储别按原生预期走。
执行后验证:生产 3 broker + 1 UI、测试 1 broker + 1 UI,6 个 Pod 的 AGE 一个没变------零重启零触碰;middleware-test 里只剩新集群的 PVC;原来的 doesn't exist 报错归零。
07 一条吓人的警告:finalizers Forbidden
删除完成后,事件里立刻冒出三条新警告:
text
PersistentVolume "pvc-7c5fdbee-xxxx" is invalid: metadata.finalizers: Forbidden:
no new finalizers can be added if the object is being deleted,
found new finalizers []string{"kubernetes.io/pv-protection"}
乍一看吓人,拆解后是删除窗口期的一次正常竞态:
- everest 联动删除 PV,给对象打上"删除中"标记(deletionTimestamp)
- 与此同时 K8s 的 pv-protection 控制器发现"这 PV 刚才还 Bound 着,得加保护锁"
- API server 拒绝:正在删除的对象禁止添加新 finalizer
- 保护锁没加上,PV 干净利落地删除完成
所以这条警告反而是删除成功的旁证。如果 PV 真被保护锁卡住,kubectl get pv 会看到它挂着 Terminating 状态赖着不走,那才是需要处理的情形。
08 怎么判断"报错真的止住了"?
分享一个实战技巧,看 kubectl get events 的 LAST SEEN 列:
bash
kubectl get events -A | grep -E "doesn't exist|finalizers"
- 报错还在发生 → LAST SEEN 永远是
5s、23s、1m这种不断刷新的值 - 报错真停了 → LAST SEEN 定格在某个值不再变化
我删除后 7 分钟查,三条 finalizer 警告全部定格在 7m31s------正是执行删除的那一刻,之后再没发生任何一条。事件对象有约 1 小时的 TTL,过期前 grep 有输出是正常的,别慌,看时间戳有没有在刷新。
09 复盘:这次踩坑换来的东西
知识点清单:
- Pod 只能挂载同 namespace 的 PVC ------ 跨 ns 同名资源必须核对 ns 列,同名≠同一套
- CCE everest 对 Retain PV 会联动删除(而非标准 K8s 的 Released 保留),删 PV 报 NotFound 属预期
- pv-protection finalizer 的竞态警告无害,反而是删除流程正常完成的旁证
- volumeClaimTemplate 的引用不出现在
spec.template.spec.volumes里 ------ 模板层扫描有结构性盲区,引用核查要以运行中 Pod 实体为准 - 判断报错止住没止住,看事件 LAST SEEN 是否持续刷新,不看 grep 有没有输出
- 中间件下线 SOP:删 sts → 删 PVC → 确认云侧盘消失,三连清。僵尸 Bound PVC 不触发业务告警,只刷存储事件------而"没告警的资源"恰恰是最容易被当成垃圾清掉的
流程教训:
- "零引用"这类否定性结论,验证命令的关键词必须和被查对象的命名体系对齐。我拿 PV uid 去匹配 PVC 名,输出为空给了我虚假的安全感
- 删除类操作永远显式带
-n,这是防同名误伤的最后防线 - 终验要设计成"自己跑、眼见为实"------删除面前,口头保证一文不值
写在最后
单人运维没有第二双眼睛,所有结论最终只有自己兜底。这次如果没有删除前的最后一道终验,我可能已经基于一个假阴性结论执行了清理------命令里显式带 namespace 也许能保住活集群,但整个行动就建立在错误的认知上了。
你也有过"输出为空就以为万事大吉"的时刻吗?评论区聊聊。如果觉得有用,点个赞让更多被 K8s 存储问题折磨的同学看到。
本系列记录单人运维的实战日常,下一篇:《镜像堆积把节点磁盘干到 75%,我只删旧版本就释放了 40GB》