最近我把服务器上的 /data,从单 12TB 盘 XFS + LVM,迁移成了三块 12TB 硬盘组成的 bcachefs RAID5。主要流程是先双盘组 RAID1,再将数据从旧单盘上拷贝过去,最后把旧盘加入 bcachefs 阵列并实现 RAID5。

听起来只是一次普通的文件系统迁移,但实际操作过程中,遇到了内核不支持、工具链版本冲突、多设备挂载、systemd 依赖、SAS 链路异常、服务误写根分区、SSH 中断、残留 XFS 超级块和旧版 libblkid 拒绝操作等一系列问题。

一、原始环境

服务器系统是 Ubuntu 24.04,后来使用的内核版本为:

7.0.0-28-generic

原来的 /data 是一块 12TB 机械硬盘:

/dev/mapper/vg--data-lv--data

存储结构为:

物理盘
  └── LVM PV
       └── vg-data
            └── lv-data
                 └── XFS
                      └── /data

实际已经使用大约 2.6TB。

服务器上还有 Docker 和 Incus。很多容器、模型、代码仓库和服务数据都在 /data 下,因此这个迁移不能简单停机格式化,必须先建立新文件系统、同步数据、切换挂载,最后才能处理旧盘。

我的目标配置是三块 12TB 硬盘:

data_replicas=2
metadata_replicas=2

也就是数据和元数据都保存两份。

三块盘原始总容量大约是:

3 × 10.8 TiB ≈ 32.4 TiB

因为数据保存两份,理论有效数据容量大约是:

32.4 TiB ÷ 2 ≈ 16.2 TiB

df 会显示文件系统总原始容量接近 30T,并不会直接除以副本数。实际能安全存放多少用户数据,还是需要结合副本策略计算。

二、第一个问题:Ubuntu 的内核没有 bcachefs

一开始我以为安装一个 bcachefs-tools 就可以直接使用,实际上 Ubuntu 当前使用的内核并没有包含可用的 bcachefs 模块,因此即使用户态工具安装成功,内核仍然无法挂载 bcachefs。

最后采用的方案是从官方源码编译:

  • bcachefs-tools v1.38.8
  • 对应的 bcachefs DKMS 模块
  • 将模块安装到当前 7.0.0-28-generic 内核
  • 更新 initramfs

安装完成后确认:

bcachefs/v1.38.8, 7.0.0-28-generic, x86_64: installed

并且模块已经包含在:

/boot/initrd.img-7.0.0-28-generic

这里还有一个容易误判的问题。

Ubuntu 自带的 stat 不认识 bcachefs 的文件系统 magic,会显示:TYPE=UNKNOWN (0xca451a4e)

这不代表文件系统有问题。正确的验证方法是:

findmnt -o FSTYPE

只要显示 bcachefs

就说明内核已经正常识别。


三、先做 loop 测试,再碰真实硬盘

在真实硬盘上操作前,我先使用 loop 设备完整测试了一遍。

测试内容包括:

  • 双设备格式化;
  • data_replicas=2
  • 写入文件并校验哈希;
  • 模拟移除一个成员;
  • 降级只读挂载;
  • 恢复第二成员;
  • 再次完整挂载;
  • 离线 fsck。

这一步很重要。

存储迁移不是普通的软件部署。一旦命令写错,后果是直接丢数据。loop 测试至少可以先验证:

  • 当前内核模块是否可用;
  • bcachefs-tools 与内核是否匹配;
  • 多设备挂载语法是否正确;
  • 副本策略是否生效;
  • 降级挂载是否符合预期。

测试完成后,才在两块空白硬盘上创建真实 bcachefs。


四、先使用两块新盘建立 bcachefs

最开始参与 bcachefs 的两块盘是:

/dev/disk/by-id/wwn-0x5000c500a6a356df-part1
/dev/disk/by-id/wwn-0x5000c500a6ca5b97-part1

这里我从一开始就坚持使用 /dev/disk/by-id,没有在任何破坏性操作中使用 /dev/sda/dev/sdb。这是整个过程中最重要的原则之一。

Linux 下的 /dev/sdX 并不稳定。重启、HBA 初始化顺序变化或者设备重新枚举后,原来的 /dev/sdb 完全可能变成 /dev/sdc。这次迁移过程中也确实发生过盘符交换。如果当时脚本里写死 /dev/sdc,后果可能就是直接清错盘。

新文件系统最终 UUID 为:

f463f76b-cc52-4504-8233-6f0464922edf

两个成员标签分别为:

hdd-a
hdd-b

五、第一次数据同步

新 bcachefs 建好后,先挂载到临时目录,然后使用 rsync 迁移数据。

第一次同步时,原 /data 仍然在线,服务也还在运行。因此这次同步只负责搬迁绝大部分静态数据,不负责最终一致性。

第一次同步结果大致为:

2.63T
1,182,799 files
RSYNC_RC=0

之后停止 Docker、Incus 和其他写入服务,再进行第二次增量同步。

最终增量大约只有:

8.61G

最后使用 dry-run 再检查一次,确保没有剩余差异。

这种迁移方式的核心是:

  1. 在线完成大部分数据复制;
  2. 最后只需要较短停机时间;
  3. 停止写入后执行最终同步;
  4. 验证无差异;
  5. 再切换挂载。

六、第一次切换成功,但重启后 /data 没有挂载

手动切换时一切正常。

当时的挂载配置类似:

/dev/disk/by-id/wwn-...-part1:/dev/disk/by-id/wwn-...-part1 /data bcachefs defaults 0 0

手动执行:

mount /data

可以成功。

Docker 和 Incus 也都能正常启动。

于是我重启了服务器。

结果重启后发现:

findmnt /data

没有任何输出。

/data 实际上只是根分区上的一个空目录。

更危险的是,部分服务已经开始向这个空目录写入数据。也就是说,本来应该写入机械盘的数据,被写到了系统盘的根文件系统中。

最后在根分区的 /data 下发现了大约 44KB 的新文件和目录,随后将这些内容单独保存到了:

/root/data-rootfs-after-failed-boot-20260802-125301

这次没有造成严重后果,但说明仅仅设置:

nofail

或者依赖普通的 fstab 挂载,对于这种生产数据目录并不安全。

如果 /data 没有挂载,Docker 和 Incus就不应该启动。


七、真正的问题之一:systemd 无法正确处理这个多设备源

bcachefs 的多设备挂载语法是:

device1:device2

但是直接写进 fstab 后,systemd 会尝试把整个字符串推导成一个设备单元。

日志中出现了类似:

Timed out waiting for device
dev-disk-by\x2did-...part1:-dev-disk-by\x2did-...part1.device

也就是说,systemd 把:

设备 A:设备 B

当成了一个完整设备名。

手动 mount /data 可以成功,但开机自动挂载会失败。

最后我没有继续和 fstab 较劲,而是创建了一个专用的 oneshot systemd 服务:

bcachefs-data-mount.service

这个服务的逻辑是:

  1. 等待所有成员的 WWN 路径出现;
  2. 使用 udevadm settle 等待设备初始化完成;
  3. 检查每个成员的 bcachefs superblock;
  4. 使用三个稳定 WWN 路径挂载;
  5. 检查挂载后的 UUID;
  6. 检查文件系统是否为 undegraded
  7. 检查关键目录是否存在;
  8. 验证完成后才退出成功。

挂载命令实际使用的是:

SOURCE="${PART_A}:${PART_B}:${PART_C}"
bcachefs mount "$SOURCE" /data

同时给 Docker 和 Incus 添加依赖:

[Unit]
Requires=bcachefs-data-mount.service
After=bcachefs-data-mount.service

这样,如果 /data 挂载失败:

  • Docker 不会启动;
  • Incus 不会启动;
  • 服务不会向根分区的空 /data 写入数据。

这比简单依赖 fstab 安全得多。


八、一度怀疑硬盘,但实际更像 SAS 链路异常

第一次失败启动后,其中一块盘出现了一系列异常。

表现包括:

Nak received
Ack/nak timeout
DID_NO_CONNECT
I/O error
Synchronize Cache failed

设备还多次被 HBA 移除,又重新检测到。

SMART 结果本身却是正常的:

SMART Health Status: OK
Elements in grown defect list: 0
uncorrected errors: 0
Non-medium error count: 0

真正异常的是 SAS PHY 统计:

negotiated logical link rate: 1.5 Gbps
Loss of DWORD synchronization count: 299
Phy reset problem count: 14744

另一块正常盘则是:

6 Gbps

因此,只能怀疑 SAS 传输链路发生过严重异常,但没有证据证明磁盘介质损坏。

可能原因包括:

  • 盘的 SAS 接口;
  • 托架接触;
  • 背板槽位;
  • SAS 线缆;
  • HBA PHY;
  • 电源或初始化时序。

后来再次重启后,三块盘都能正常出现,错误没有继续复现,最终多次重启验证也正常。


九、验证只读挂载

排查过程中,我曾尝试降级只读挂载:

bcachefs mount -o degraded=yes,read_only ...

但实际 findmnt 显示的是:

rw,relatime,degraded=yes

也就是说,参数没有按预期产生只读挂载。

之后改成:

bcachefs mount -o ro ...

再通过:

findmnt -n -o OPTIONS

确认输出:

ro,relatime

十、加入第三块盘时,SSH 被自己停掉了

两盘迁移和开机挂载都稳定后,我准备处理原来的 XFS 盘,将它清空后加入 bcachefs,成为第三个成员。

因为这是不可逆操作,脚本做了大量身份核对:

WWN
序列号
PV UUID
VG UUID
LV UUID
文件系统 UUID
挂载状态
当前 bcachefs 状态

确认无误后,脚本先停止 Docker 和 Incus,再删除旧 LVM。

问题是,我当前的远程管理链路本身也依赖服务器上的某个 Incus 容器或相关服务。

执行:

incus stop --all

后,SSH 连接突然断开:

client_loop: send disconnect: Connection reset

重新连接后检查日志,发现脚本实际上已经继续执行,并且已经完成:

lvremove
vgremove
pvremove
wipefs
sgdisk --zap-all
创建新 GPT 分区

因此这时不能简单重新执行整个脚本,否则会因为旧 LVM 已不存在而进入不可预测状态。

正确做法是:

  1. 先确认原脚本是否仍然运行;
  2. 检查执行日志,确认进行到了哪一步;
  3. 根据当前磁盘状态编写继续脚本;

以后写这种脚本,应优先选择:systemd-runnohup,或使用IPMI。


十一、旧 XFS 签名没有完全消失

旧盘的 LVM 和 GPT 被清理后,重新创建了一个覆盖整盘的新分区。

但新分区出现后,lsblk 仍然识别出了旧 XFS:

FSTYPE=xfs
UUID=b075dc6c-673c-42c6-96ee-f21f547d0b68

原因是,之前清理的是整盘设备上的 LVM 和分区表签名,但旧 XFS 的超级块本来位于逻辑卷起点。

重新创建分区后,这个区域再次被映射成 /dev/sdc1,于是旧 XFS 超级块又被识别出来。

脚本安全检查发现:

STOP: unexpected signature remains on new partition

最终确认 UUID 正是旧 XFS UUID 后,才对第三分区执行:

wipefs --all --force \
  /dev/disk/by-id/wwn-0x5000c500c47939ce-part1

清除后再次执行:

wipefs -n

确认没有任何签名,才继续加入 bcachefs。


十二、旧版 libblkid 阻止添加设备

第三分区已经完全空白后,执行:

bcachefs device add /data <third-device>

工具仍然拒绝执行:

Refusing to format when using libblkid 2.39.3
libblkid >= 2.40.1 is required to check for existing filesystems

服务器系统自带:

libblkid 2.39.3

而 bcachefs-tools v1.38.8 要求至少 2.40.1,才能可靠识别所有文件系统签名。

我不希望为了这一次操作,替换 Ubuntu 系统级的 util-linux 和 libblkid,因为这可能影响系统中的其他工具。

因此,在已经完成以下检查后:

  • 固定 WWN 正确;
  • 序列号正确;
  • 分区归属正确;
  • 旧 LVM 已消失;
  • wipefs -n 无输出;
  • 目标分区为空;
  • 当前 bcachefs 状态正常;

使用:

bcachefs device add \
  --force \
  --label hdd-c \
  --state rw \
  --rotational \
  /data \
  /dev/disk/by-id/wwn-0x5000c500c47939ce-part1

--force 在这里不是无脑跳过安全检查,而是:

在我自己已经完成比 libblkid 更严格的目标盘核对后,允许 bcachefs-tools 跳过旧版 libblkid 的版本保护。

第三盘最终成功加入:

Device index: 2
Label: hdd-c
State: rw
Devices: 3

十三、最终状态

迁移完成后,文件系统状态为:

Filesystem: f463f76b-cc52-4504-8233-6f0464922edf
Size: 29.8T

undegraded
2x: 4.41T

三个成员:

hdd-a (device 0): 10.8T
hdd-b (device 1): 10.8T
hdd-c (device 2): 10.8T

三个成员均为:

State: rw
read errors: 0
write errors: 0
checksum errors: 0

Docker、Incus 和所有关键容器也已恢复。

最终再次重启服务器,开机日志显示:

Waiting for all bcachefs members...
bcachefs /data mounted and validated

随后 Docker 和 Incus 正常启动。

最终验证结果:

THREE_DISK_BOOT_VERIFICATION=PASSED

十四、为什么第三块盘几乎还是空的

第三盘刚加入时,使用量只有几 MB:

hdd-a: 2.20T
hdd-b: 2.20T
hdd-c: 4.75M

这不是问题。

原有数据已经在前两块盘上满足:

data_replicas=2

因此 bcachefs 没有必要为了让三块盘的数字看起来平均,立即把所有健康数据重新搬迁一次。

reconcile status 显示:

replicas: 0
checksum: 0
target: 0
pending: 0

说明没有副本不足或需要修复的数据。

后续新写入会逐渐开始使用第三块盘。随着数据增长,整体分布会逐步变化。

不应该为了追求“立刻均衡”,随意修改副本数或者触发全盘重写。机械盘上搬迁数 TB 数据本身会产生大量 I/O,也会增加不必要的风险。

Castronaut的头像

作者 Castronaut

行走在地狱边缘,狂舞于悬崖之巅。

发表回复