一次真实的迁移记录:群晖 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
三个容易踩的点:
- 一定要
sudo -i或sudo bash -c。普通用户跑nohup会在你的家目录写nohup.out,家目录不存在就直接失败退出(而且什么都没拷)。 - 显式写
> restore.log,别依赖默认的nohup.out。 - 日志别写到
/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 就能正常浏览了。
九、踩坑清单
mount失败先看dmesg,mount 自己的报错永远是一句”wrong fs type”,没信息量。- 两个 mirror 报同样的错 = 大概率不是坏盘,是软件层面的判定。
noload是 ext4 的,btrfs 用norecovery。- 网上教程里的
mount ... > /目标,>是笔误。 btrfs restore的-m和-o都要加,前者保权限,后者保断点续跑。-x不要加,群晖 xattr 会刷屏。- 必须
sudo,否则nohup连日志都写不出来,命令直接失败。 - 要
setsid,否则 SSH 一断前功尽弃;用ps确认PPID=1。 - 别用
btrfs check --repair,它是写操作,能把能救的卷改死。 - 别在飞牛界面把群晖的盘”加入存储空间”,那是格式化。
- 群晖盘不要读写挂载,插回群晖会出问题;只读 +
btrfs restore才是无损。 @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)都是写操作。在数据还没拷出来之前,唯一正确的动作是”只读”。