是什么:一台 KVM 宿主的 bcache 脏数据最高冲到 540G+,长时间排不完,宿主 IO 持续劣化。
为什么:多租户随机小写叠加 RAID6 的 RMW 写惩罚,持续入写(约 18MB/s)跑赢了底层 md0 的随机小写能力(约 10-17MB/s)。
怎么做:找到写大户并热迁移到 CEPH(iSCSI SSD),锁定 writeback_percent=1,注释掉 cron 里改写水位的定时任务,保护性限速,让 dirty 从 540G 一路降到 0。
存储运维常有一句话:缓存能削峰,不能填海。本次事故把这句话变成了 540G 的账单。
一、事故现场:池子里的水排不干了
宿主 IO 出现持续劣化,虚机侧卡顿明显。检查 bcache 状态,dirty_data 最高升至 540G+,且长期无法排空:回写速率上不去,脏数据如池中之水,进水持续大于出水。
该宿主的存储栈如下:
qcow2 → /hdd(ext4) → bcache0(writeback) → md0 RAID6(12×7T LUN=6块双磁头盘) → HDD12 个 7T LUN 对应 6 块双磁头盘,每块物理盘拆成 2 个 LUN。bcache 处于 writeback 模式,写请求先落缓存,再由 bcache 按水位与速率回写到 md0。该回写路径在常规负载下正常,本次事故中变为「排不动」。缓存写穿、宿主卡顿、虚机集体变慢是运维常见现象;本次异常在于回写持续运行,dirty 仍不下降,涨速超过排速。
二、排查第一层:不是 bcache 偷懒,是 md0 排不动
查看 bcache 状态与回写速率调试信息:
cat /sys/block/bcache0/bcache/dirty_data
cat /sys/fs/bcache/*/bdev0/writeback_rate_debugdebug 字段为 rate/dirty/target/proportional/integral/change/next io,显示 bcache 回写速率由一套 PID 类控制器调节。现场实测结论更直接:md0 的小随机写能力约 10-17MB/s,业务入写基线约 18MB/s。 入写持续大于落盘,净排速经常仅 0-3MB/s,脏数据无法排空。控制器参数只能微调斜率,不能改变物理极限:入写大于落盘时,脏数据曲线只会向上。
md0 随机小写弱的原因在 RAID6 的 RMW(read-modify-write)写惩罚。RAID6 处理一次小随机写,需读旧数据、读旧校验、计算新校验、再写数据与双校验,一次业务写被放大为多次盘面操作。12 LUN 背后是 6 块双磁头盘,寻道抖动加大后,随机小写降至 10-17MB/s。
贯穿全程的一个认知是:md0 的顺序读(几百 MB/s)与随机小写(10-17MB/s)是两个世界。 热迁移期间 VM 仍在写源盘,源盘的随机小写使磁头来回抖动,会把数百 MB/s 的顺序读一并拖垮。
三、排查第二层:crontab 里的一行「背刺」
若仅是入写大于落盘,调低水位、迁移写大户仍有回旋空间。现场在 root crontab 中发现一条每天 06:00 执行的「背刺」:
0 6 * * * echo 35 > /sys/block/bcache0/bcache/writeback_percent该任务每天 06:00 将 writeback_percent 从 1% 改回 35%,把 dirty 目标水位重新拉高到约 530G。前一天排到接近 15G 的进度次日即被抵消,dirty 长期无法排空。定位到该行配置后将其注释,writeback_percent 才恢复为可控状态。
四、取证:谁在写?
治本需要先定位写来源,并精确到进程。top 与 iostat 只能给出聚合总量,bcache 之后的写放大最终来自各 qemu 进程,谁在写、写多少需要点名到户。现场直接按 qemu 进程采样 /proc/$pid/io 的 write_bytes 差值,找出实时写大户:
declare -A w1
for pid in $(pgrep -f qemu-system-x86_64.real); do
w1[$pid]=$(awk '/^write_bytes/{print $2}' /proc/$pid/io)
done
sleep 12
for pid in $(pgrep -f qemu-system-x86_64.real); do
n=$(tr '\0' ' ' </proc/$pid/cmdline | grep -oE 'guest=[^ ,]+' | cut -d= -f2)
w2=$(awk '/^write_bytes/{print $2}' /proc/$pid/io)
d=$((w2-w1[$pid]))
[ $d -gt 1048576 ] && echo "$((d/12/1048576))MB/s $n"
done | sort -rn | head锁定写大户后,缓解手段是将这些 VM 的磁盘热迁移到 CEPH(iSCSI SSD)存储。迁移对源机主要是「读 MD0」;热迁移期间 VM 仍在写源盘,源盘上同时存在顺序读与随机小写。二者分属两个世界,磁头抖动会连顺序读一起拖垮,因此迁移逐台进行,紧盯 dirty 与回写速率,不做盲目并行。
五、调参:把池子调回能排干的状态
最终落地的调优参数:
# 后三项路径按实际 cache set UUID 调整
echo 1 > /sys/block/bcache0/bcache/writeback_percent # 目标约 15G
echo 0 > /sys/fs/bcache/.../writeback_consider_fragment
echo 40000 > /sys/fs/bcache/.../p_term_inverse
echo 100000000 > /sys/fs/bcache/.../i_term_inversewriteback_percent=1 将目标水位压至约 15G;writeback_consider_fragment=0 与 PI 控制器的 p_term_inverse、i_term_inverse 按现场情况调优,配合 debug 输出观察回写速率收敛行为。迁移期间另施加保护性限速:
echo 20480 > /sys/block/bcache0/bcache/writeback_rate # 扇区/s = 10MiB/s效果直接:dirty 从 540G → 26.8G → 0,回写速率达到 42MB/s 且持续下降。 水位锁死后,排水能力首次大于入写。水开始退了。
六、两条绝不能碰的红线
复盘时强调两条红线,均经现场反复确认:
第一,绝不能切 cache_mode=writethrough。 入写基线 18MB/s,大于 md0 的小随机写能力(10-17MB/s)。writethrough 等效于拆掉缓存缓冲,让所有写直通 RAID6,结果不是脏数据排不完,而是整个宿主的写入直接被打爆,全宿主一起受影响。
第二,绝不能 echo 0 > /sys/block/bcache0/bcache/running。 该操作会停掉整台 bcache,/hdd 直接蒸发,属一键事故级操作。
七、回到标题:水为什么排不干?
水排不干的原因不是排水管堵塞,而是进水口长期大于排水口。bcache 的 dirty 是缓冲池,容量再大也只负责削峰:只要「持续入写 > 持续落盘」,缓冲早晚打满。这次事故是三重叠加:多租户随机小写是进水,RAID6 RMW 写惩罚是排水管太细,cron 定时改写反复拉高目标水位,面板重装/迁移突发进一步加剧。
最后记住一句话:缓存负责削峰,平均值必须小于落盘能力。 排不干的水不靠加缓存解决,而靠关小进水口、修好排水管。之后每次查看 bcache 的 dirty_data,都会想起那条每天 06:00 改写水位的 cron。事故的根源往往不是单点,而是一串「看起来都合理」的配置叠加。
事故复盘要点
- 存储栈:
qcow2 → /hdd(ext4) → bcache0(writeback) → md0 RAID6(12×7T LUN=6块双磁头盘) → HDD;12 LUN 背后是 6 块双磁头盘。 - 根因链:多租户随机小写 + RAID6 RMW 写惩罚 + bcache dirty 排不完 + 面板重装/迁移突发。
- 关键数字:md0 小随机写约 10-17MB/s,入写基线约 18MB/s,净排速经常 0-3MB/s;dirty 最高 540G+。
- cron 背刺:每天 06:00 把
writeback_percent从 1% 改回 35%(约 530G 目标),导致 dirty 长期无法排空,已注释。 - 最终调优:
writeback_percent=1(目标约 15G)、writeback_consider_fragment=0、p_term_inverse=40000、i_term_inverse=100000000;迁移期间手动writeback_rate=20480(10MiB/s)保护性限速。 - 监控手段:
dirty_data与writeback_rate_debug(rate/dirty/target/proportional/integral/change/next io);用declare -A数组采样 qemu 进程/proc/$pid/io的write_bytes差值找实时写大户。 - 缓解手段:写大户磁盘热迁移到 CEPH(iSCSI SSD),保持
percent=1;结果 dirty 从 540G → 26.8G → 0,回写速率 42MB/s 且持续下降。 - 红线 1:绝不切
cache_mode=writethrough——入写 18MB/s > md0 随机写能力,直通 = 全宿主炸。 - 红线 2:绝不
echo 0 > /sys/block/bcache0/bcache/running——停整台 bcache = /hdd 蒸发。 - 核心认知:缓冲(缓存/journal)不是解药;持续入写 > 持续落盘时缓冲早晚打满,缓存负责削峰,平均值必须小于落盘能力。
- 顺序读与随机小写是两个世界:md0 顺序读几百 MB/s、随机小写 10-17MB/s;热迁移期间 VM 仍写源盘,磁头抖动会连顺序读一起拖垮,迁移要一台一台盯。
- 定时任务也会背刺:任何写 sysfs 的 cron 都要留痕、审计;「看起来无害」的一行 echo,可能就是水位失控的源头。