LVM 挂起:大量 Pod 并发创建删除时的磁盘操作瓶颈与解法

在使用 CSI Local Inline Volume 的节点上,当大量 Pod 并发创建和删除时,LVM 命令会出现长时间挂起,整个节点的磁盘操作进入不可用状态。本文分析其根因,并记录通过引入 FIFO 队列限制并发度的解决方案。

问题现象

节点上并发创建删除大量使用 local CSI inline volume 的 Pod 时,LVM 命令挂起,/proc/<pid>/stack 显示进程阻塞在 blkdev_issue_discard

1
2
3
4
5
6
7
8
9
10
# cat /proc/182718/stack
[<0>] submit_bio_wait+0x7f/0xc0
[<0>] blkdev_issue_discard+0x7e/0xd0
[<0>] blk_ioctl_discard+0x110/0x140
[<0>] blkdev_common_ioctl+0x3fc/0x890
[<0>] blkdev_ioctl+0xf6/0x270
[<0>] block_ioctl+0x46/0x50
[<0>] __x64_sys_ioctl+0x91/0xc0
[<0>] do_syscall_64+0x5c/0xc0
[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xae

该状态下,LVM 对整个节点不可用,影响调试、指标导出等所有依赖磁盘操作的功能。


根因分析

单次 Pod 操作的磁盘开销

Local CSI 驱动在处理每个 Pod 的 volume 时,创建和删除流程各需要约 5 次磁盘操作(lvslvcreatemkfswipefslvremove 等),涉及:

  • LVM 元数据查询与修改
  • 文件系统格式化(mkfs
  • 数据擦除(wipefsdiscard

这些操作都需要持有内核块层的锁。

无并发限制的 goroutine 模型

原始驱动框架对每个 CSI RPC 请求直接启动一个 goroutine 执行磁盘操作,没有工作者数量上限。当 100 个 Pod 同时发起请求时:

1
100 个 Pod × 创建阶段 5 次磁盘操作 = 500 次并发磁盘操作

这 500 个操作全部争抢内核块层锁,压力直接传导到 Linux 内核。所有操作在锁上排队等待,时间上集中分布在队列尾部——即大量 Pod 几乎同时完成,而非均匀分散。


解决方案

优化 1:合并重复的磁盘操作

对创建和删除流程做了操作合并:

  • 将多次 lvs 查找合并为一次批量查询
  • 删除流程中移除冗余的清理操作

合并后创建和删除各需约 4 次磁盘操作(减少 1 次)。

优化 2:引入 FIFO 队列 + 限流工作者(核心)

更重要的改变是在驱动层引入了有界 FIFO 队列和固定数量的 worker:

1
2
3
4
5
6
7
8
9
CSI RPC 请求


FIFO 磁盘操作队列(无界入队,有序出队)

├── Worker 1 ──► 磁盘操作
├── Worker 2 ──► 磁盘操作
├── ...
└── Worker N ──► 磁盘操作(N = 10)
  • 所有磁盘操作进入队列排队,而非直接并发执行
  • 固定 N 个 worker(实测 N=10)从队列取任务执行
  • Pod 创建/删除从”并发爆发”变为”均匀流水”

效果对比

测试场景:100 个 Pod 并发创建并删除,使用 local CSI inline volume。

指标 修复前 修复后(workers=10)
Pod Running 时间 17 min 14 min 40s
Pod Terminated 时间 17 min 8 min
完成时间分布 集中在末尾 均匀分散
LVM 可用性 不可用(挂起) 始终可用

修复后:

  • Pod Terminated 时间从 17 分钟缩短到 8 分钟(提速 53%)
  • LVM 在整个过程中保持可用,调试和指标采集不受影响
  • Pod 的运行和终止时间分布从”集中爆发”变为”均匀分散”

关键设计原则

并发限流比减少单次操作更重要。

减少单次操作次数(4 次 vs 5 次)只有边际收益;真正解决问题的是限制同时进入内核块层的操作数量。内核块层的锁争抢是 O(N²) 级别的性能降级——并发越多,每个操作等待时间越长,总时间反而更长。

引入 FIFO 队列将并发模型从”无限并发 + 内核排队”改为”应用层排队 + 有限并发”,把压力从内核转移到用户态,换来了更好的可预测性和更低的总体延迟。