这次排查的结论:冗余配置最完整的地方,实际没有冗余。一次常规存储排查,把一套运行已久的「双盘冗余」打回了原形。
一台 KVM 宿主运行md0 RAID6(12×7T LUN),上层为 bcache writeback + ext4,承载数百台 VM。RAID6 双盘校验通常被视为冗余充分,可容忍两块盘故障。执行cat /sys/block/md0/md/mismatch_cnt后,输出为 12,444,868,280,同时sync_action=idle。
这串数字揭穿了皇帝的新衣:RAID6 双盘冗余并不成立,实际是一套「伪 RAID0」。数据在,parity 不在。
一、新衣是怎么穿上的
md0 创建时使用了 --assume-clean。该参数的作用是创建阵列时向 mdadm 声明「阵列是干净的」,跳过初始同步、直接上线。对 RAID6,初始同步即计算并写入全部条带的 parity;跳过该步骤,parity 从未被计算。
正常创建 RAID6 时,md 在后台扫描全部条带并写入正确 parity。该过程耗时较长,创建者因此用 --assume-clean 跳过。被跳过的这一步是冗余的「出生证明」:没有它,阵列只是把若干盘拼接在一起,元数据层显示 RAID6,数据层没有校验能力。--assume-clean 适用于一次性或测试场景,不适用于生产阵列。
阵列因此呈现两面状态。元数据层:md 认为阵列完整、状态 clean,正常承接读写。数据层:数据盘内容有效,parity 仍是盘上残留数据。两者不一致,且没有对账机制。系统无告警,监控无异常,mdadm --detail 显示 clean。这就是皇帝的新衣:所有状态都显示「衣服存在」。数百台 VM 的多租户随机小写经 bcache writeback 落到 md0,日常读写持续进行,一个 clean 状态的阵列不会引发质疑。
parity 是从同一条带的数据计算出的校验块,数据损坏时可据此恢复;校验块必须由真实数据计算,不能沿用盘上残留字节。更隐蔽的问题是 RMW。RAID6 小随机写执行 read-modify-write:读旧数据、读旧 parity,计算新 parity,再写回。若旧 parity 本身是垃圾,新 parity 只是在垃圾之上再计算一遍,错误被逐次继承,不会自愈。mismatch_cnt 中的 12.4B 扇区即数据与 parity 不一致的累计值;sync_action=idle 表示当前无人修复,也不会自行恢复。冗余只存在于显示层面。
二、再数一遍冗余:不能只数 LUN
逻辑层看,12 个 LUN 像 12 块盘。核对串号后,两两相同:这 12 个 LUN 实际是 6 块物理盘 × 2 LUN(双头/双致动器)。每个 SAS 盘位暴露两个 LUN;双头/双致动器盘在同一盘体内装两组读写头,各自对外暴露一个 LUN,串号共用一套。串号是识别这层关系的关键。从 RAID6 的角度看,成员盘数为 12;从物理角度看,故障域为 6。
可以把 6 块物理盘理解为 6 栋各开两扇门的房子:门有 12 扇,房子只有 6 栋;一栋房子倒塌,两扇门同时消失。一块物理盘损坏即两个 LUN 同时掉线,RAID6 双校验能力一次耗尽。12-LUN RAID6 实际只容忍 1 块物理盘故障。「坏两块盘」的承诺在此配置下不成立:那两块「盘」可能来自同一个物理盘。
叠加 parity 从未计算这一事实:坏一块盘后重建出的 parity 仍是垃圾,「容忍 1 块物理盘」只在纸面上成立。计算冗余应以「物理故障域」为单位,不能只数 LUN 数量;同一物理盘上的两个 LUN 在故障时同时失效。
三、怎么发现它
检测成本很低,三条命令即可:
cat /sys/block/md0/md/mismatch_cnt # 12,444,868,280,非 0 就是有鬼
cat /proc/mdstat
mdadm --detail /dev/md0mismatch_cnt 是成本最低的健康检查:只读一个计数器,不触发修复、不产生额外 IO。md 执行 check 时逐条带比对数据与 parity,不一致的条带计入该计数器,因此非 0 表示数据与 parity 不一致。结合 sync_action=idle,说明不一致尚未被修复。/proc/mdstat 与 mdadm --detail 用于确认阵列状态、同步进度与成员盘情况。健康阵列在完整 check 后该值应回到 0;本例为 12.4B 扇区,且持续 idle。定期检查该文件,优于坏盘后再验证。将三条命令纳入日常巡检的成本接近零:一条命令、一个数字、一次对比;发现非 0 后再决定 check 或 repair,优于坏盘当天才发现。
四、两条路
发现问题后有两种处理方式:原地修复 parity,或推倒重建。
选项一:原地修复 parity。
echo repair > /sys/block/md0/md/sync_action该操作让 md 重新计算 12 盘 RAID6 的 parity,耗时 18-36h。它能修掉当前 mismatch,使 RAID6 的冗余由假变真;不改变两件事:RAID6 小随机写 10-17MB/s 的 RMW 惩罚,以及「6 块物理盘」的拓扑短板。修复后,机器写入能力不变,冗余上限仍为 1 块物理盘。
选项二:重建 RAID10。
mdadm --create /dev/md0 --level=10 --layout=n2 --raid-devices=12 --chunk=512K --bitmap=internal ...12 个 LUN 配成 6 对镜像:无 parity、无 RMW,坏盘重建为纯拷贝。代价是容量从 64T 降至约 38.2 TiB。镜像对必须跨物理盘配对:同一物理盘上的两个 LUN 配成一对,该盘损坏时镜像两个成员同时失效,配对无效。
| 选项 | 动作 | 收益 | 代价 |
|---|---|---|---|
| 原地 repair | echo repair > sync_action | 修掉当前 parity mismatch,恢复 RAID6 名义冗余 | 18-36h;RMW 惩罚与物理拓扑不变 |
| 重建 RAID10 | mdadm --create --level=10 ... | 无 parity、无 RMW,坏盘重建纯拷贝 | 容量 64T → 约 38.2 TiB |
最终选择 RAID10。依据:节点已分配 32.2TB 磁盘,38.2 TiB 容量足够;原 RAID6 小随机写仅 10-17MB/s,RAID10 镜像写无 RMW 惩罚,更适合多租户小写负载。数百台 VM 的随机小写在 RAID6 上每次都要承担 RMW 开销,在 RAID10 上为镜像双写,写路径更短。容量从 64T 降至约 38.2 TiB,仍可容纳已分配的 32.2TB,并留有周转空间。以容量换取更短的写路径,在该负载下成立。创建时镜像对跨物理盘配对,不使用 --assume-clean。
初始同步阶段:md 默认限速 200MB/s,初始 ETA 55h。提高限速上限:
echo 800000 > /proc/sys/dev/raid/speed_limit_max调整后同步速度 543MB/s,ETA 约 21h。同步慢不一定来自磁盘,可能来自默认限速。此次未使用 --assume-clean,等待同步完成;这是唯一应当放慢的环节。
五、结语
皇帝的新衣最可怕之处不在衣服不存在,而在所有人都称赞它。这台机器同样如此:状态 clean、面板无告警、数百台 VM 正常运行,冗余始终不成立。坏盘时重建出的 parity 仍是垃圾。mismatch_cnt 这类计数器持续记录不一致,等待被查看。存储系统里最贵的不是盘,也不是性能,而是「看起来没问题」。每个 clean 状态都应经得起一次检查;每次抄近路省下的时间,最终会以更长的恢复时间偿还。
复盘要点
--assume-clean只配用在一次性/测试场景,生产阵列必须跑完初始同步mismatch_cnt是最便宜的健康检查,定期看一眼- 冗余要用「物理故障域」来算,不能只数 LUN