CSI Inline Volume 孤儿问题:Kubelet 重启后 Volume 无法清理的根因与修复

当 Pod 正在 Terminating 期间 kubelet 发生重启,CSI Inline Volume(ephemeral volume)可能进入”孤儿”状态——kubelet 重启后既不卸载该 volume,也不清理挂载点,导致底层 LVM 资源泄漏。本文分析其根因,并给出修复方案。

问题现象

Pod 处于 Terminating 状态时,节点上的 kubelet 重启。重启后日志中出现如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
kubelet: I reconciler.go:388] "Could not construct volume information, cleaning up mounts"
podName=1517b38e-fa84-4138-b6c0-06663741e385
volumeSpecName="data"
err="failed to GetVolumeName from volumePlugin for volumeSpec \"data\"
err=kubernetes.io/csi: plugin.GetVolumeName failed to extract volume source
from spec: unexpected api.CSIVolumeSource found in volume.Spec"

kubelet: E operation_generator.go:952] UnmountVolume.MarkVolumeMountAsUncertain failed
for volume "" (UniqueName: "data")
pod "1517b38e-fa84-4138-b6c0-06663741e385"
Error: UnmountVolume.TearDown failed: rpc error: code = Aborted
desc = NodeUnpublish operation for volume csi-c6b0a910... still ongoing

最终结果:Pod 被从 API Server 删除(SyncLoop DELETE),但 volume 的 LVM 逻辑卷仍然存在于节点上,成为孤儿资源。


根因分析

1. reconstructVolume 走错了插件查找路径

Kubelet 重启后,会通过 reconstructVolume 从磁盘上残留的挂载信息重建 volume 状态。核心流程如下:

1
2
3
4
5
6
7
kubelet restart
→ reconstructVolume
→ FindAttachablePluginByName ← 问题在这里
→ FindDeviceMountablePluginByName
→ GetUniqueVolumeNameFromSpec ← 只处理 CSIPersistentVolumeSource
→ getPVSourceFromSpec
→ ERROR: "unexpected api.CSIVolumeSource found in volume.Spec"

CSI Inline Volume(api.CSIVolumeSource)与 CSI PV(api.CSIPersistentVolumeSource)是两种不同的结构体。getPVSourceFromSpec 只处理后者,遇到前者时直接报错。

2. 应走 GetUniqueVolumeNameFromSpecWithPod

对于 ephemeral volume,唯一名称需要结合 Pod UID 生成,应调用 GetUniqueVolumeNameFromSpecWithPod,而非 GetUniqueVolumeNameFromSpec。判断走哪条路径的逻辑是:

1
2
3
4
5
if attachablePlugin != nil || deviceMountablePlugin != nil {
uniqueVolumeName, err = util.GetUniqueVolumeNameFromSpec(plugin, volumeSpec)
} else {
uniqueVolumeName = util.GetUniqueVolumeNameFromSpecWithPod(volume.podName, plugin, volumeSpec)
}

CSI Inline Volume 不应有 attachablePlugindeviceMountablePlugin,但代码中使用了 FindAttachablePluginByName(按名字查找),而非 FindDeviceMountablePluginBySpec(按 spec 查找)。这导致本应进入 else 分支的 inline volume 错误地进入了 if 分支。

3. reconstructVolume 失败后的后果

1
2
3
4
5
6
7
8
9
10
reconstructedVolume, err := rc.reconstructVolume(volume)
if err != nil {
if volumeInDSW {
// 某个 Pod 还需要这个 volume,跳过清理
continue
}
// 没有 Pod 需要它,尝试清理挂载点
rc.cleanupMounts(volume)
continue
}

由于 Pod 正在 Terminating,Pod 没有被加入 Desired State of World(DSW),所以 volumeInDSW 为 false。kubelet 调用 cleanupMountsUnmountVolume,但此时容器还没有完全退出,底层 CSI 驱动返回 Aborted(NodeUnpublish 操作仍在进行中)。

这是唯一一次清理机会,失败后 kubelet 不再重试,volume 就此成为孤儿。

触发条件

三个条件同时满足才会触发:

  1. Pod 正处于 Terminating 状态
  2. Kubelet 在此期间重启或宕机
  3. reconstructVolume 失败,且 Pod 未被加入 DSW

CSI 驱动侧的报错

在 kubelet 尝试清理的同时,CSI 驱动(本地 LVM 驱动)也尝试执行 NodeUnpublishVolume,但因 LV 仍被使用而失败:

1
2
3
4
5
6
CSI local driver: NodeUnpublishVolume req=volume_id:"csi-17ef0134..." target_path:"...mount"
CSI local driver: Removing volume with id=vg10000_csi-17ef0134...
mke2fs: /dev/vg10000/vg10000_csi-17ef0134... is apparently in use by the system
wipefs: error: probing initialization failed: Device or resource busy
lvremove: Logical volume vg10000/vg10000_csi-17ef0134... contains a filesystem in use.
NodeUnpublishVolume failed: Failed to lvremove lv: filesystem in use

LV 因容器仍在使用文件系统而无法被删除,NodeUnpublish 以 Internal 错误返回。


修复方案

上游修复

该 bug 已在 Kubernetes 1.25 修复:kubernetes/kubernetes#108997

修复核心:将 reconstructVolume 中对 CSI Inline Volume 的插件查找改为使用 FindDeviceMountablePluginBySpec,使其正确跳过 attachablePlugindeviceMountablePlugin 的查找,进而走 GetUniqueVolumeNameFromSpecWithPod 路径,成功重建 volume 状态并正常触发卸载流程。

低版本临时处理

在 bug 修复版本未上线之前,需要手动清理孤儿 LV。步骤:

1
2
3
4
5
6
7
8
9
# 1. 确认孤儿 LV(无对应挂载点的 LV)
lvs vg10000

# 2. 检查 LV 是否仍被挂载
lsblk /dev/vg10000/<lv-name>

# 3. 强制卸载后删除
umount -f /dev/vg10000/<lv-name>
lvremove -f vg10000/<lv-name>

总结

环节 问题 影响
插件查找 FindAttachablePluginByName 误匹配 inline volume reconstructVolume 报错
路径判断 inline volume 错进 GetUniqueVolumeNameFromSpec volume 无法加入 ASW
清理时机 容器未退出时 cleanupMounts 必然失败 volume 成为孤儿

根本原因是 CSI Inline Volume 在 kubelet 重建路径上的判断逻辑与 CSI PV 混用,导致重启窗口期内的 volume 无法被正确重建和清理。上游在 1.25 通过区分插件查找方式解决了这一问题。