从群晖到飞牛:921G Btrfs 数据卷的「无损」迁移实录

一次真实的迁移记录:群晖 DSM 7.2 的 volume_1(921G Btrfs)整盘插到飞牛 fnOS 上,全程只读、不写源盘一个字节,400GB 数据 7 小时拷完。

中间踩过的坑、试错的死路、以及最终为什么只有 btrfs restore 能走通,都写在下面。


一、背景

用了几年黑群晖,决定换飞牛 fnOS。数据不想走”外接硬盘中转”那一套(要来回倒两轮),也不想在中间再搭一台机器。

最理想的做法是:把群晖的数据盘直接插到飞牛机器里,在飞牛里把数据读出来、拷到飞牛自己的存储空间,然后群晖这组盘原样收起来——将来想回去还能插回去接着用。

听起来很简单,实际上卡了整整一晚上。

二、环境

项目 内容
源 群晖 DSM 7.2,单盘 931.5G,volume_1 = 921G Btrfs
目标 飞牛 fnOS,系统盘 15G + 数据盘 3.6T
源卷元数据 label 2024.05.11-14:52:46 v69057,transid 4516191
源卷实际用量 bytes_used = 399,898,472,448(约 372.5 GiB)
工具 btrfs-progs v6.2,内核 6.18.18.c1126-trim

三、第一步:先认清盘的结构

群晖的存储是三层套娃,必须一层层剥,跳步就会得到”wrong fs type”这种毫无信息量的报错:

物理盘  sdb3
  └─ mdadm RAID1  md2           ← 群晖的软 RAID(SHR / Basic 也是 md)
      └─ LVM PV   vg1
          └─ LV   volume_1      ← 921G
              └─ Btrfs          ← 真正的文件系统在这里

看一眼现状:

cat /proc/mdstat        # md2 是否 active、是否 degraded
pvs; vgs; lvs -a -o +segtype,devices
lsblk -o NAME,SIZE,TYPE,FSTYPE,LABEL,MOUNTPOINT

我的实际输出(简化):

sdb                              931.5G disk
├─sdb1                             8G part  linux_raid_member <host>:trim
├─sdb2                             2G part  linux_raid_member DiskStation:1
└─sdb3                         921.3G part  linux_raid_member <host>:2
  └─md2                        921.3G raid1 LVM2_member
    ├─vg1-syno_vg_reserved_area    12M lvm
    └─vg1-volume_1                921G lvm   btrfs  2024.05.11-14:52:46 v69057

小提示:vg1-volume_1 显示出来了不等于能用。LVM 的 Attr 里第 5 位是 a 才是 active;-wi-a----- 表示已激活但没被打开,-wi-a----- 和 -wi-ao---- 只差”有没有挂载中”。

激活:

vgchange -ay vg1
ls -l /dev/mapper/

四、第二步:为什么 mount 一定失败

这时候所有人(包括我)的第一反应都是:

mount -o ro /dev/mapper/vg1-volume_1 /mnt/syno1

结果:

mount: /mnt/syno1: wrong fs type, bad option, bad superblock on /dev/mapper/vg1-volume_1,
       missing codepage or helper program, or other error.

dmesg 里才是真相:

BTRFS: device label 2024.05.11-14:52:46 v69057 devid 1 transid 4516191
       /dev/mapper/vg1-volume_1 (253:2) scanned by mount (4100)
BTRFS critical (device dm-2 state ES): corrupt leaf: root=1 block=487157137408 slot=0,
       invalid root flags, have 0x40000000 expect mask 0x1000000000000001
BTRFS error (device dm-2 state ES): read time tree block corruption detected on logical 487157137408 mirror 1
BTRFS error (device dm-2 state ES): read time tree block corruption detected on logical 487157137408 mirror 2
BTRFS error (device dm-2 state ES): open_ctree failed: -5

翻译一下:

  • 不是坏盘,也不是文件系统坏了。 两个 mirror 报一模一样的错,说明两边数据完全一致——真损坏不会这么整齐。
  • 群晖在自己的 Btrfs 里给 root item 加了一个私有标志位 0x40000000(用来标记共享文件夹/快照之类的状态)。
  • 主线的 btrfs tree-checker 只认 BTRFS_ROOT_SUBVOL_RDONLY / BTRFS_ROOT_SUBVOL_DEAD 两个合法位,看到 0x40000000 直接判 EUCLEAN,中止挂载。
  • 这个校验大约在内核 5.4 之后变严格,所以老内核(如 Ubuntu 18.04 的 4.15)能挂,新内核不能。

于是所有”常规补救”都注定无效:

mount -o ro ...                      # ❌
mount -o ro,rescue=all ...           # ❌
mount -o ro,usebackuproot ...        # ❌
mount -t btrfs -o sb=1 ...           # ❌

它们都绕不过 tree-checker。

顺带记一个坑:noload 是 ext4 的选项,btrfs 对应的叫 norecovery / nologreplay。我看网上教程写了 -o ro,noload,内核直接回 btrfs: Unknown parameter 'noload'。

五、四条死路(别试,会掉数据)

做法 后果
btrfs check --repair 真改元数据。论坛上有人跑完,卷彻底报废
btrfs rescue zero-log / super-recover 都会写盘;你的问题不在日志/超级块,跑了只会更糟
在飞牛界面点”创建存储空间”指向这块盘 等于格式化,921G 当场归零
去掉 -o ro 读写挂载 群晖的 Btrfs 被其他内核写过之后,插回群晖大概率报”存储空间已损毁”

另外一个隐藏坑:网上很多教程的这行命令是笔误——

mount /dev/mapper/vg1-volume_1 > /mnt/syno1

> 是 shell 重定向,不是参数。你会得到 bash: /mnt/syno1: Is a directory,而且 mount 根本没执行。

六、第三步:正解是 btrfs restore

核心认知的转变:

挂载是内核在做(要过 tree-checker),btrfs restore 是 btrfs-progs 用户态在做。
后者不做那么严的 flags 校验,而且只读——它不会往源盘写一个字节。

它本来是给”文件系统损坏时抢救数据”用的工具,正好解决这种”内核不认但数据完好”的场景。

关键参数

btrfs restore -i -m -S -o /dev/mapper/vg1-volume_1 /vol1/1000/<user>/syno_restore
参数 作用 说明
-i 忽略读错误,继续拷 遇到坏块不中断,能救多少救多少
-m 恢复属主/权限/时间 不加的话拷出来全是 root、权限错乱
-S 恢复子卷 群晖的共享文件夹都是子卷(@syno 等)
-o 目标已存在则覆盖 断点续跑必须加,重跑不会重复占空间
-x 恢复 xattr 不要加! 群晖的 xattr 目标端存不了,会刷屏拖慢
-d 干跑,只打印不写 先验证路径正则很有用

后台跑,别让 SSH 断了白干

400G 要跑几个小时,必须脱离终端:

cd /vol1/1000/<user>
setsid nohup btrfs restore -i -m -S -o \
  /dev/mapper/vg1-volume_1 \
  /vol1/1000/<user>/syno_restore \
  > /vol1/1000/<user>/restore.log 2>&1 &

# 验证真的脱离了终端:PPID 应该是 1,TTY 是 ?
ps -o pid,ppid,tty,etime,cmd -C btrfs

三个容易踩的点:

  1. 一定要 sudo -i 或 sudo bash -c。普通用户跑 nohup 会在你的家目录写 nohup.out,家目录不存在就直接失败退出(而且什么都没拷)。
  2. 显式写 > restore.log,别依赖默认的 nohup.out。
  3. 日志别写到 /var/log,会撑爆根分区。目标盘上有空间,就写目标盘。

只想拷某几个目录

先干跑确认匹配,再去掉 -d:

btrfs restore -d -i -m -S -o --path-regex '^/(|@syno(/.*)?)$' \
  /dev/mapper/vg1-volume_1 /目标目录

七、实战时间线

从 2026-10-29 22:12 开始,到 10-30 05:06 结束,约 6 小时 54 分:

时间 已写 速度 备注
22:12 0 — 启动
22:39 52G ~26MB/s 初期,小文件多
23:14 76G ~3MB/s 大量小文件(照片/缩略图),最慢
00:38 129G ~20MB/s 起伏
01:46 175G ~5MB/s 又一段小文件
02:59 186G ~1MB/s 几乎停滞(怀疑卡在 @eaDir 缩略图)
03:42 237G ~35MB/s 开始出现大文件
03:59 321G ~84MB/s 大文件段,速度拉满
05:06 388G — 完成

结论:btrfs restore 的速度完全取决于文件大小分布,不是盘慢。小文件多的时候 1~5MB/s,全是大文件能跑到 80~100MB/s。所以预估时间时要看你的数据构成——全是照片和文档的话,做好跑一晚上的心理准备。

日志里还会大量出现:

offset is 4096
offset is 8192
offset is 65536
Skipping snapshot 65e18cc66456f60b2e20137bfbe740371fc629bfff6d89aeafdd3ecb81de1649

这两个都不是错误:

  • offset is N 是它处理每个 extent 的流水信息;
  • Skipping snapshot ... 是 -S 模式下跳过群晖的快照(快照是旧数据的副本,你只要当前数据)。

真正要担心的是 ERROR / Couldn't read tree root / can't read superblock。

八、第四步:核对与权限

1. 对账

# 源端真实占用量
btrfs inspect-internal dump-super -f /dev/mapper/vg1-volume_1 \
  | awk '/^bytes_used/{print $2; exit}'
# 372.5 GiB 级别 → 399898472448

# 目标端
du -sb /vol1/1000/<user>/syno_restore
df -h /vol1

我这里:du ≈ 377G,源端 bytes_used ≈ 372.5 GiB,基本一致;df 显示 389G,多出来的是 Btrfs 自身的元数据开销,属于正常。

2. 目录结构

拷完的目录长这样:

syno_restore/
└── @syno/                 ← 群晖的"卷根",共享文件夹都在这里面
    ├── photo/  music/  video/  docker/  homes/  chat/ ...
    ├── @docker/ @database/ @appconf/ @appdata/ ...   ← DSM 内部数据
    └── @eaDir/            ← 缩略图缓存,可以删

@syno 是群晖的隐藏卷根,@ 开头的是 DSM 自己的;真正的用户数据在 @syno/photo、@syno/music 这些目录里。

3. 权限(这一步不做,飞牛界面里进不去)

btrfs restore -m 会原样保留群晖的权限,而群晖给 @syno 挂的是 0000:

d--------- 1 root root  @syno

飞牛的用户是 uid=1000,所以必须改:

chown -R 1000:1000 /vol1/1000/<user>/syno_restore
chmod -R u+rwX   /vol1/1000/<user>/syno_restore

-R 在几十万文件上会跑十几到几十分钟,属正常。改完飞牛的文件管理、SMB 就能正常浏览了。

九、踩坑清单

  1. mount 失败先看 dmesg,mount 自己的报错永远是一句”wrong fs type”,没信息量。
  2. 两个 mirror 报同样的错 = 大概率不是坏盘,是软件层面的判定。
  3. noload 是 ext4 的,btrfs 用 norecovery。
  4. 网上教程里的 mount ... > /目标,> 是笔误。
  5. btrfs restore 的 -m 和 -o 都要加,前者保权限,后者保断点续跑。
  6. -x 不要加,群晖 xattr 会刷屏。
  7. 必须 sudo,否则 nohup 连日志都写不出来,命令直接失败。
  8. 要 setsid,否则 SSH 一断前功尽弃;用 ps 确认 PPID=1。
  9. 别用 btrfs check --repair,它是写操作,能把能救的卷改死。
  10. 别在飞牛界面把群晖的盘”加入存储空间”,那是格式化。
  11. 群晖盘不要读写挂载,插回群晖会出问题;只读 + btrfs restore 才是无损。
  12. @eaDir 是缩略图,占空间又不重要,迁移完可以删。

十、迁移之后:要不要把群晖盘彻底转成飞牛存储空间

两条路,看你急不急:

A. 保守路线(我选的)

  • 只读读出数据 → 拷到飞牛自己建的存储空间(另一块盘)
  • 群晖那组盘原样收起来,将来想回群晖,插回去就能开机
  • 代价:多占一块盘的容量

B. 彻底转换

  • 先按 A 把数据拷出来并校验
  • 确认无误后,在飞牛里把这块盘格成自己的存储空间
  • 但没有第二块盘的话,中间没有任何冗余,一旦出错就是双份丢失

另外要明白一点:飞牛的”存储空间”是它自己的 Btrfs + mdadm 布局,没法直接”导入”群晖的 LVM/Btrfs 卷。所谓”挂载群晖盘到飞牛存储空间”,实际只能是”挂载成普通目录”或”拷进飞牛的空间”,没有第三种。

十一、命令速查

# ---- 1. 认清结构 ----
cat /proc/mdstat
mdadm --assemble --scan
pvs; vgs; lvs -a -o +segtype,devices
vgchange -ay vg1
ls -l /dev/mapper/

# ---- 2. 确认失败原因(只读)----
mount -o ro /dev/mapper/vg1-volume_1 /mnt/x
dmesg | tail -30
blkid /dev/mapper/vg1-volume_1
btrfs inspect-internal dump-super -f /dev/mapper/vg1-volume_1 | head -30

# ---- 3. 只读抢救 ----
setsid nohup btrfs restore -i -m -S -o \
  /dev/mapper/vg1-volume_1 /目标目录/syno_restore \
  > /目标目录/restore.log 2>&1 &
ps -o pid,ppid,tty,etime,cmd -C btrfs

# ---- 4. 对账 ----
btrfs inspect-internal dump-super -f /dev/mapper/vg1-volume_1 | awk '/^bytes_used/{print $2; exit}'
du -sb /目标目录/syno_restore
df -h

# ---- 5. 修权限 ----
chown -R 1000:1000 /目标目录/syno_restore
chmod -R u+rwX    /目标目录/syno_restore

# ---- 6. 收尾,准备把盘还给群晖 ----
umount /mnt/x 2>/dev/null
vgchange -an vg1
mdadm --stop /dev/md2
poweroff   # 再拔盘

写在最后

整个过程最大的教训不是”命令怎么敲”,而是先搞清楚失败发生在哪一层:

  • mount 报错 → 看 dmesg;
  • dmesg 报 tree block corruption → 看是”真损坏”还是”校验不认”;
  • 两个 mirror 完全一致 + 错误信息里出现了具体的 flag 值 → 基本可以判定是软件判定问题,不是数据问题。

一旦确认数据是好的、只是内核不认,思路就从”修文件系统”转成”绕开内核把它读出来”——btrfs restore 就是干这个的。

最后提醒一句:任何”修复”类命令(--repair、zero-log、super-recover)都是写操作。在数据还没拷出来之前,唯一正确的动作是”只读”。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注