Files
hoelee-blog/src/content/posts/zh/when-smart-says-healthy-but-your-raid-is-corrupting-data.md
T
hoelee 327d516ed9
Deploy / build (push) Successful in 17s
feat(art): replace all 72 posts' artwork with the v2 pipeline
Every post now carries one unique local-ComfyUI/Ideogram 4 base frame, composited
into EN + ZH share cards, a banner and list thumbs.

- the shipped prompts_catalog held only 21 distinct metaphors for 72 posts (16
  shared one visual), so all 72 were rewritten as unique object+state metaphors
  and 10 frames re-rolled after OCR found pseudo-text rendered at legible size
- render_from_art.mjs emits JPEG by default (558 KB PNG -> 84 KB mozjpeg q85);
  the ZH 1200x630 card did not exist before this batch
- 288 frontmatter fields migrated; the 144 v1 PNGs are gone
- PostList's thumb helper keys off the base slug (a ZH card is zh-<slug>.jpg while
  the thumb is <slug>-800.webp, which 404'd the whole ZH listing)
- [...slug].astro's <picture> regex accepts .jpg
- the repo now carries the art inputs (metaphors.json, captions/<slug>.json,
  refreshed prompts_catalog.json) so a frame is reproducible from git alone
2026-10-19 04:18:59 +08:00

12 KiB
Raw Blame History

title, description, pubDate, category, tags, ogImage, banner
title description pubDate category tags ogImage banner
SMART 显示健康,RAID 却在悄悄损坏数据 两块看起来完全健康的 NVMe 组成的 btrfs RAID1 阵列,静默地把 Windows 虚拟机喂成了全零字节。我靠校验和取证抓住它——但根因(M.2 转接卡链路故障 vs 批次缺陷)至今仍未定论,现在两块盘测试都干净。 2026-09-16 devops
unraid
btrfs
nvme
raid
storage
data-loss
troubleshooting
/og/zh-when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg /banners/when-smart-says-healthy-but-your-raid-is-corrupting-data.jpg

所有存储指南都告诉你同一件事:相信 SMART、检查事件日志、相信你的文件系统。我的硬件不这么想。2026 年 9 月,一个 btrfs RAID1 阵列——两块企业级 NVMe 盘镜像,我用每个能想到的工具查它都说**「健康」**——却在悄悄地把我的 Windows 11 虚拟机喂成全零字节。不是看得见的故障,是静默损坏。

下面是完整故事:损坏是怎么浮现的、哪个校验和出卖了它——以及为什么最终定论如今变得不确定。

更新(2026-09-16):根因至今未定论。 下面的「两块盘都换」是我当时的判断,但后来一次不相关的故障让我不得不重新审视。现在两块 NVMe 测试都干净,而最强的头号嫌疑是两块盘共用的那块 M.2 转接卡——转接卡上的链路层故障,恰好会复现我当初归咎于硬盘的那种「两块盘关联地吐出全零」的损坏。SMART 对链路故障完全无感,所以无法排除它。我在一篇配套文章里写了那次警醒(一颗「死掉的」SATA SSD 其实只是坏线)。先读下面的原始分析,再看更正——然后还有续集:两周后同一个全零指纹出现在完全不同的硬盘堆栈上,这次终于挖出了根因。见《换了不同的硬盘,同样的损坏又来了——真凶是 TRIM,不是 SSD》。

更新:为什么我不再怪两块盘

这篇文章发出后,有两件事改变了我的想法。

1. 现在两块盘测试都是干净的。 在当前条件下,两块 PM9A3 读写都正常。如果它们带着 9 月还在活跃损坏数据的批次级缺陷,应该会挂掉一轮持续的写入+校验(verify=crc32c)泡测——但它们没有。

2. 链路故障才是真正的教训。 就在这次事故之后,我在另一颗盘上撞见了完全相同的故障签名(一颗 Seagate IronWolf 110 狂报 WRITE FPDMA QUEUED 超时、mkfs 失败、然后静默——教科书般的「死盘」)。结果是坏在 SATA 数据线上。一条不稳定的链路会复现下面我归咎于硬件的每一个症状:在算出 CRC 之前就已丢掉的超时 I/O、真的丢掉的页面,以及——如果两个 RAID1 成员共用同一块有问题的 M.2 转接卡或插槽——两块盘关联地损坏。这是唯一一个不需要任何硬盘缺陷、却能解释「两个成员用同一种方式撒谎」的答案。

所以我没法再诚实地断言「盘有问题」。它们可能一直好好的,损坏可能来自它们共同坐着的 M.2 转接卡。注意:这并不代表这次事故是虚惊一场——真实数据确实读不回来过,无论根因是什么,我都把它移走、迁移数据、并逐字节验证过。但正确的教训是「先排除链路,再给盘判死刑」,而不是「同批次盘不可信」。

完整的链路对盘排查,请看 "我差点把这颗 SSD 送修——真正坏的是 SATA 数据线"。

原始分析(保留供透明考量)

下面是我当时得出的结论。这是当时证据下最站得住的解读,而且可能仍然是对的——只是我无法排除别的可能——但请把它当作事故报告来读,而不是最终定论。

为什么这件事重要

损坏状态的磁盘是最糟的存储故障,因为它最容易漏掉:

  • SMART 看起来完美。 零媒体错误、几乎满余量,而且是能扛企业级写入负载的盘。
  • 阵列有冗余。 btrfs RAID1 把每个块镜像到两块盘上。当两个成员用同一种方式撒谎时,冗余毫无意义。
  • 故障是静默的。 数组状态没有红色警告、没有硬盘报错、没有硬件事件——只有虚拟机读回垃圾,直到备份恢复成了唯一的选择。

如果你的数据在一切看起来都正常的时候悄悄坏掉,「备份」就不是偏执——它是全部。这次事故让我明白:一条可测试、可验证的恢复胜过任何阵列状态屏幕。

环境

  • unRaid 7.x,Intel i9-13900K,Windows 11 虚拟机是主力工作站(GPU 直通)。
  • 缓存池: 两块 Samsung PM9A3 3.84TB NVMe 组 btrfs RAID1。而且两块是同一生产批次(同一个控制器/固件版本 GDC7702Q)。
  • Windows 虚拟机的虚拟磁盘(vdisk1.img、vdisk2.img)就在这个池的 domains/ 共享里,另外还有 Docker 数据根目录。
  • 虚拟磁盘没有设 NOCOW——所以每次读取都会被 btrfs 校验和验证。也正因为这个,我最后才抓得住它。

症状

我开始发现缓存读取慢得离谱,主机命令也卡住:cat、读 /proc/loadavg、甚至 docker image ls 都在超时。那块坏 NVMe 正以 45–75 MB/s 被狂读,每次读都塞满整台主机的 I/O。本该几秒跑完的 docker system prune 直接挂死。

然后真正的麻烦来了:Windows 虚拟机开始狂报读错误,回来的是错的数据——满文件的全零字节、guest 里的校验和失败,最后主力 VM 直接引导不了。

取证:csum 0x8941f998

btrfs 在读校验失败时会往内核日志记一条校验和不匹配。同一个值反复出现:

btrfs: checksum verify failed on <device> wanted 0x8941f998 found <other>

这个特定值是最重要的线索。0x8941f998 就是一整块零字节的 CRC32C。 它每次都出现、跨越互不相关的文件和 inode,绝不是巧合。正在发生的不是随机比特腐烂,而是盘稳定地返回全零块,而这些零填充的读取正是 btrfs 校验失败的对象:

  • 验证一下:零填充块的 crc32c 就等于 0x8941f998。
  • 同一个值反复出现 → 读取是确定性地吐出空数据,不是偶尔翻几个比特。

两块虚拟磁盘都中招(vdisk1.img inode 2358574、vdisk2.img inode 2362817),正好对上「存储层在把零当成被请求的数据返回」的图景。

破绽:永远不生效的「自愈」

btrfs RAID1 本应自愈:一个成员坏块、从镜像重建、写回好数据。但我一遍遍 scrub、重读同一块(仍然全零的)数据后发现,重写根本不落地。每次重建「成功」后,立刻重读同一 extent 又吐出零。

这是关键区别:

  • 一块只读不出来的盘是读问题——自愈应该能从镜像救回来。
  • 一块连回调也无脑吞垃圾存进去的盘是写或控制器问题——镜像救不了你,因为两条路径都对同一个错误答案达成一致。

当自愈重写一直不生效,你就不在「坏一个扇区」的范畴了,而是「这块盘在写入上撒谎」——RAID 冗余救不了。

为什么两块盘都得换(我当时的推理)

这是当初「都换掉」决定的依据。动手前先读上面更新里的更正——一块共用的 M.2 转接卡链路故障,在无需任何硬盘缺陷的情况下也能对上这份证据。

数据指向的是同批次盘的硬件缺陷,而不是磨损或一次性的偶发事件:

  • SMART 干净(0 媒体错误、几乎满余量,nvme0 大约写了 586 TiB——对能扛企业级负载的盘来说算重负载但仍正常)。
  • 两块盘同批次、同控制器/固件(GDC7702Q)。
  • 损坏以相关的方式同时命中两个 RAID1 成员——镜像「死一块也能幸存」的整个意义,因为两块盘正以同一种方式一起死掉而完全失效。

面对批次级的关联缺陷,留下那块「看起来健康」的盘当幸存成员是假省心。我从备份恢复了 Windows 虚拟机,然后把两块盘都换掉。

我同时做的事:把数据迁出那个池

在换盘之前,任何还值得读的数据都得搬出这个坏池——而且要验证,不能想当然:

  • Docker 数据根目录迁出缓存:docker 挪到 /mnt/user/docker(在 disk1 上,overlay2)。整棵树通过路径、大小、mtime 逐文件比对、并与字节对比交叉验证,共 3,997,569 个文件。
  • 缓存的 isos/、system/、swapfile 搬到 disk1;isos/(约 13 GB)用 diff -rq 检查 → EXIT=0,无差异。
  • 所有搬走的文件都在删除缓存副本之前先跟源比对过——没验证过的副本绝不删。

因为存在读侧损坏(全零块)和写侧损坏(自愈不落地),那个池里的任何东西,没有镜像、没有校验和、没有新鲜备份可对照,我就一律不信任。

如果重来我会怎么做

  1. 在需要它之前就先测试恢复虚拟机备份。 我当时确认了备份存在,但损坏事故不是该第一次学恢复流程的时候。定时做的测试恢复既能发现备份问题,也能发现静默的存储损坏。
  2. 把同批次的两块盘当成一块盘。 两块 PM9A3 同批次同固件。对需要长期搭档的缓存池(任何 RAID1)这是关联失效风险——优先选不同批次的盘,或者至少在信任镜像前把这对盘狠狠测一遍。
  3. 主动检测,而不是被动祈祷。 正是 btrfs 校验和让它在噪声里浮出来。不开 NOCOW 意味着文件系统在验证每一次读取;一个带校验的数据池,就是「当场抓住并恢复」和「把损坏文件带出去几个月」之间的分水岭。
  4. 别用 SMART 判断闪存。 一块看起来健康却照样损坏的 NVMe,事件计数什么也说明不了——固件级的缺陷可以躲过所有 SMART 属性。

结果

一台 Windows 虚拟机从备份恢复、数据安全迁出那个坏池、Docker 数据根目录也搬了家——外加一条事后再推翻的教训。 我当时换掉了两块盘,但现在我没法确定它们就是元凶:证据同样支持一块共用的 M.2 转接卡链路故障,而且现在两块盘测试都干净。(完整排查见 "我差点把这颗 SSD 送修——真正坏的是 SATA 数据线"。)

事故被控制住,是因为损坏被校验和抓住了、而不是在里面泡了几个星期——这一点无论根因是什么都成立。但更诚实的教训,比「把盘换掉」这句要更广、也更不戏剧化:

  1. 一块「正在损坏」的盘,并不等于盘本身坏了。 在给硬件判死刑前,先排除链路:重新插拔/更换线材、换到别的口/转接卡/插槽,重跑相同的失败操作。如果故障跟着口/转接卡走,那就是连接的问题。
  2. SMART 看不见链路故障。 零媒体错误和几乎满余量,对一条不稳定的线材或转接卡毫无意义——超时在算出 CRC 之前就把 I/O 丢了。
  3. 同批次盘共用一条通道,无论谁坏都是共享失效风险。 不管损坏来自盘还是转接卡,一对盘坐同一条故障路径上,就是穿着两个序列号的单点故障。
  4. 给你的数据加校验、并提前测试恢复备份。 把数星期静默损坏变成一次被控制住的事故的,是这一点——不是我那部分诊断。