最近我把服务器上的 /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 再检查一次,确保没有剩余差异。
这种迁移方式的核心是:
- 在线完成大部分数据复制;
- 最后只需要较短停机时间;
- 停止写入后执行最终同步;
- 验证无差异;
- 再切换挂载。
六、第一次切换成功,但重启后 /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
这个服务的逻辑是:
- 等待所有成员的 WWN 路径出现;
- 使用
udevadm settle等待设备初始化完成; - 检查每个成员的 bcachefs superblock;
- 使用三个稳定 WWN 路径挂载;
- 检查挂载后的 UUID;
- 检查文件系统是否为
undegraded; - 检查关键目录是否存在;
- 验证完成后才退出成功。
挂载命令实际使用的是:
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 已不存在而进入不可预测状态。
正确做法是:
- 先确认原脚本是否仍然运行;
- 检查执行日志,确认进行到了哪一步;
- 根据当前磁盘状态编写继续脚本;
以后写这种脚本,应优先选择:systemd-run 或 nohup,或使用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,也会增加不必要的风险。