Kubernetes Local PVC 丢失时的 Fake Device 解决方案

节点重启或 OS patching 时,本地磁盘可能短暂丢失,导致依赖 Local PV 的 Pod 启动失败——kubelet 找不到设备路径,mount 操作报错。本文记录一种通过 loop device 创建”假设备”(fake device)来解除 Pod 启动阻塞的工程方案,以及各类节点修复场景下的处理策略。

问题背景

节点重启或 OS patching 后,/dev/sdc 及对应的 /dev/disk/by-id/wwn-xxxxxx 路径可能不再存在。但 Kubernetes 中对应的 PV 仍处于 Bound 状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spec:
localVolume:
path: /dev/disk/by-id/wwn-xxxxxx
volumesize: "0"
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- tess-node-g568m-tess12.stratus.lvs.ebay.com
storageClassName: local-ssd
volumeMode: Block
status:
phase: Bound

此时 kubelet 尝试挂载时,因找不到源设备而失败,Pod 无法启动。

方案思路

Local Volume Provisioner(下称 “provisioner”)在检测到 PV 路径不存在时,主动创建一个 loop device 作为假设备,让 kubelet 能够完成 mount 操作,使 Pod 正常启动。Pod 内的应用程序随后通过设备大小识别假设备并将其移除,再进入正常业务流程。


为什么不能用普通文件替代设备

直觉上,用 touch + bind mount 创建一个假文件映射到设备路径似乎可行:

1
2
3
touch test_fake_file
mount -o bind,ro test_fake_file /dev/sdc
ln -s /dev/sdc /dev/disk/by-id/wwn-xxxxxx

但 kubelet 会拒绝挂载非设备节点的路径:

1
2
Warning  Failed  kubelet  Error: failed to generate container spec:
failed to generate spec: not a device node

因此必须使用真正的块设备——通过 loop device 来实现。这要求 provisioner Pod 挂载宿主机的 /dev/ 路径(需要相应的安全策略支持)。


实现步骤

1. Provisioner 检测设备丢失

Provisioner 以 10 秒为周期轮询 PV 路径(wwn-xxx)是否存在。若不存在,读取节点 label 判断是否需要创建假设备:

1
shouldCreateFakeDisk := ShouldCreateFakeDisk(config.Node.Labels[DedicatedLabelKey])

2. 创建 loop device

shouldCreateFakeDisk 为 true,provisioner 执行以下操作:

  1. 在本地临时目录创建一个 512 字节的镜像文件
  2. loop101 开始设置 loop device
  3. 创建软链接:/dev/loopX/dev/disk/by-id/wwn-xxxxxx

创建后,kubelet 在下一次同步时即可成功完成 mount:

1
2
3
4
5
6
7
Normal  SuccessfulMountVolume  kubelet
MapVolume.MapPodDevice succeeded for volume "local-pv-44903f99"
globalMapPath "/var/mnt/kubelet/plugins/kubernetes.io~local-volume/volumeDevices/local-pv-44903f99"

Normal SuccessfulMountVolume kubelet
MapVolume.MapPodDevice succeeded for volume "local-pv-44903f99"
volumeMapPath "/var/mnt/kubelet/pods/7e6d87b7.../volumeDevices/kubernetes.io~local-volume"

3. Pod 内识别并移除假设备

假设备大小仅为 512 字节,写入超过 512B 会报错:

1
2
3
4
root@pod:/# dd if=/dev/zero of=/dev/vda bs=1 count=1024
dd: error writing '/dev/vda': No space left on device
512+0 records out
512 bytes copied

应用启动脚本通过读取设备大小识别假设备,读出 512B 的设备即为假设备,将其删除后再启动服务:

1
2
3
4
5
6
for device in `ls /dev/data*`; do
dd if=$device of=/tmp/test_image bs=1024 count=1 oflag=direct
# 若只读出 512 bytes,判定为假设备
done
# 删除假设备
rm /dev/data3

移除假设备后,服务正常启动。


完整流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
节点重启


Provisioner 重启
│ 10s 轮询

检测 PV 路径不存在?
│ YES

创建 512B 镜像文件


losetup 设置 loop device(从 loop101 起)


创建软链接 /dev/loopX → /dev/disk/by-id/wwn-xxxxxx


Kubelet 下次 sync 成功 mount,Pod 启动


Pod 内应用检测假设备(设备大小 == 512B)


删除假设备,服务正常启动

注意事项

假设备不会被 provisioner 重新分配

假设备通过 loop device 创建,软链接指向 /dev/loopX 而非 /dev/sdc,不在 provisioner 的 volume 扫描列表中,因此不会被重新注册为可用 PV。

需要 /dev/ hostPath 权限

创建 loop device 需要 provisioner Pod 能访问宿主机 /dev/ 路径,须通过安全策略(如 PSP / SecurityContext)明确授权。/dev/disk/by-id/ 是已有的 hostPath,但 /dev/ 本身默认不挂载,需要额外的 PR 支持。

PVC/PV 生命周期

假设备期间 PVC 和 PV 不会被删除。若用户手动删除 PVC,PV 会被重新 provision 但大小变为 512B,新 PVC 无法绑定到该 PV。若需要在保留 PV 的前提下重建 PVC,可设置注解:

1
storage.tess.io/reclaimpolicy.override = Recycle

等待 PV 同步后再删除 PVC。


节点修复场景分析

场景 处理方式
节点重启 / OS patching Provisioner 重启后自动走检测流程,假设备重建
节点重新 provision(新节点) 全新节点,无历史状态,无需处理
替换磁盘(新 sdc + 新 wwn) 新 wwn 不匹配旧 PV,用户需删除旧 PVC 并重建
磁盘恢复(原 sdc + 原 wwn) Linux 自动重建软链接,kubelet 恢复正常挂载
NPD 检测到 sdc 丢失 在 computenode 标记 sdc 为 lost,触发告警
节点 decommission Pod 和 PVC 需要手动删除

用户侧影响场景

以下情况会触发本方案的运作:

  1. 节点重启:OS patching 等运维操作导致节点重启
  2. Pod 删除重建:用户主动删除并重建 Pod
  3. Provisioner Pod 重建:provisioner 自身重启
  4. 节点修复(单盘替换):仅更换一块磁盘
  5. 新节点 provision:节点首次加入集群
  6. 故障磁盘自动归还:之前被判定故障的磁盘恢复后自动接回