在恢复一个 12TB 多设备 BTRFS 池时编写的自定义工具,该池存在严重的 extent tree 损坏,原生命令(btrfs check --repair、--init-extent-tree 等)无法修复。
请参阅 INCIDENT-ANALYSIS.md,了解恢复过程的结构化案例研究、根本原因分类以及一系列建设性建议,供上游 btrfs-progs 改进之用,这些改进本可避免使用大部分这些工具。
仅在 btrfs check --repair 出现段错误、进入无限循环或使文件系统比之前更糟时使用这些工具。
已知有帮助的案例:
btrfs check --repair 在 [3/8] checking extents 时段错误(Issue #525)btrfs check --init-extent-tree 死锁btrfs check --repair 进入无限循环,重复相同的修复操作rescue=all,ro 挂载,无法读写挂载这些工具不适用于轻度损坏。 对于正常损坏,请先尝试 btrfs check --repair。
--write 的工具前,先备份元数据:
for DEV in sda1 sdb1 sdc1; do
sudo dd if=/dev/$DEV of=sb_${DEV}.bin bs=4096 count=1 skip=16
done
--write 为选择性启用)这些工具使用 btrfs-progs 内部 API,必须在 btrfs-progs 源码树内构建:
# 1. 克隆 btrfs-progs
git clone --depth 1 --branch v6.19.1 https://github.com/kdave/btrfs-progs.git
cd btrfs-progs
# 2. 应用 EEXIST 补丁(批量 backref 注入所需)
patch -p1 < path/to/btrfs_fixes/patches/alloc_reserved_tree_block_eexist.patch
# 3. 配置并构建基础 btrfs-progs
./autogen.sh
./configure
make -j$(nproc)
# 4. 将本仓库中的 .c 文件复制到 btrfs-progs 目录
cp path/to/btrfs_fixes/programs/*.c .
# 5. 对于每个程序,在 Makefile 中添加:
echo '
PROGNAME: PROGNAME.o $(objects) $(libs_shared)
@echo " [LD] $@"
$(Q)$(CC) -o $@ PROGNAME.o $(objects) $(libs_shared) $(LDFLAGS) $(LIBS)
' >> Makefile
# 6. 构建
make PROGNAME
建议执行顺序:
scan_and_fix_all_backrefs.c(最重要)最重要的工具。 递归遍历文件系统中的每个树(ROOT、CHUNK、EXTENT、FS、DEV、CSUM、UUID、FREE_SPACE),检测在 extent tree 中缺失 METADATA_ITEM backref 的元数据块。在单个事务中注入所有缺失的 backref,以避免“根树在提交之间移动”的问题。
用法:
sudo ./scan_and_fix_all_backrefs /dev/sdX # 仅扫描
sudo ./scan_and_fix_all_backrefs /dev/sdX --write # 扫描+注入
fix_owner_refs.c修复内联 TREE_BLOCK_REF 中的 owner,当它不匹配块的实际 btrfs_header_owner() 时。当块在失败的修复期间被重新分配到其他树时,会发生不匹配。
sudo ./fix_owner_refs /dev/sdX # 扫描
sudo ./fix_owner_refs /dev/sdX --write # 修复
fix_bad_levels.c修复具有错误 level 的 METADATA_ITEM 和 EXTENT_ITEM 条目。损坏的 level(例如 50、55、237)是由 btrfs check --repair 进入循环留下的垃圾。根据块的真正 btrfs_header_level() 进行验证。
sudo ./fix_bad_levels /dev/sdX # 扫描
sudo ./fix_bad_levels /dev/sdX --write # 修复
fix_duplicate_extents.c删除重复的 METADATA_ITEM(相同的 bytenr,但键中的 level 不同)。保留 level 与 btrfs_header_level 匹配的条目,删除另一个。
sudo ./fix_duplicate_extents /dev/sdX # 扫描
sudo ./fix_duplicate_extents /dev/sdX --write # 删除重复项
remove_stale_ptrs.c扫描 FS_TREE 的每个 level-1 节点。使用三项检查检测过时的子指针:owner 不匹配、first_key 不匹配,或 first key 的类型对于 FS_TREE 无效(例如 BLOCK_GROUP_ITEM)。使用 btrfs_del_ptr 删除它们。
sudo ./remove_stale_ptrs /dev/sdX # 扫描
sudo ./remove_stale_ptrs /dev/sdX --write # 删除
fix_uuid_tree.c / fix_csum_tree.c分别为 UUID 树 / CSUM 树创建一个空的叶子节点。当 ROOT_ITEM 指向一个已被重新分配到其他树的块时非常有用。内核在读写挂载时自动重新生成 UUID 树。使用空的 CSUM 树时,标记为 NODATASUM 的文件不会验证失败。
sudo ./fix_uuid_tree /dev/sdX
sudo ./fix_csum_tree /dev/sdX
set_nodatasum.c在常规文件 inode 上设置 BTRFS_INODE_NODATASUM 标志。如果 csum 树为空但文件仍然带有预期的校验和(这会导致读取错误),则使用此工具。启用 NODATASUM 后,内核会跳过 csum 查找。
sudo ./set_nodatasum /dev/sdX # 扫描
sudo ./set_nodatasum /dev/sdX --write # 应用
fix_fstree_node.c包含一个硬编码的过时块列表的版本。优先使用 remove_stale_ptrs,它能自动检测这些块。仅当需要手动控制要删除哪些特定块时使用。
add_backrefs.c包含一个硬编码的缺失 backref 列表的初始版本。优先使用 scan_and_fix_all_backrefs,它能自动检测它们。
当上述基础工具不足时(池中有 200K+ 错误,分布在多个树上),构建了这些附加工具:
scan_fstree_extents.c + scan_extent_tree.c第一遍和第二遍扫描器,分别遍历 FS_TREE 和 extent tree,生成包含每个 ref/extent 映射的 TSV 文件。用于构建 rebuild_extent_tree_apply 的输入,当需要从头重建 extent tree 时。
rebuild_extent_tree_apply.c(重写者)主要的第3阶段写入器。接收一个预合并的 ref 列表(来自 scan_fstree_extents + scan_extent_tree 的差异),并以每个事务 5000 个的块将 300 万以上的 EXTENT_DATA_REF 注入 extent tree。每 50K 个项目进行节流以避免 DM-SMR 重排序停滞。经过验证,在 3× WD40EFAX SMR 磁盘上成功完成 3,248,617 次插入,耗时约 34 分钟。
sudo ./rebuild_extent_tree_apply /dev/sdX1 refs_folded.txt to_insert.txt watermark.txt --dryrun
sudo ./rebuild_extent_tree_apply /dev/sdX1 refs_folded.txt to_insert.txt watermark.txt --write
patch_block_group_used.c当第3阶段写入器由于预先存在的重叠 file_extent_item 导致特定 bg 计算超限时,用于 BLOCK_GROUP_ITEM.used 的手术式单字段修补器。使用 btrfs_set_block_group_used 直接设置器,以避免 btrfs_update_block_group 的 space_info 计算(我们这里不想要)。预验证 flags & BTRFS_BLOCK_GROUP_DATA。
sudo ./patch_block_group_used /dev/sdX1 <bg_bytenr> <bg_length> <new_used> --write
remove_extent_items_by_key.c从 extent tree 中删除一个硬编码的 (bytenr, num_bytes, expected_inode) EXTENT_ITEM 列表。用于清理单个叶子中阻止 RO 挂载的重叠过时 extent。删除前对每个项目进行完整性检查(7个不变量,包括 inode 白名单)。以 rebuilding_extent_tree=1 + reinit_extent_tree=true 运行,以跳过空间计算(调用者首先通过 patch_block_group_used 手动修补 used)。
clean_orphan_dir_entries.c清理 FS_TREE 中的孤立 DIR_ITEM + DIR_INDEX 条目。每个事务处理 100 个条目。更新父 INODE_ITEM 的 i_size(减少 namelen × 2:关键错误已修复:v1 仅减少 namelen,导致目录处于无效状态)。针对关键顶级目录名称(例如 pelis、series、music、backups、homestorage)的硬编码排除列表。切勿按原始 namelen 减少 i_size:BTRFS 存储的是 namelen × 2 计算。
clean_orphan_inode_refs.c遍历 FS_TREE 中的 INODE_REF 项,其 key.offset(父 inode)位于孤立父列表中。跳过 INODE_EXTREF 以避免误报(EXTREF 的 key.offset 是哈希值,而非父 ID)。每个事务处理 32 个。
fix_dir_inode_counts.c为受先前孤儿清理错误影响的 DIR inode 重新计算 i_size = sum(name_len × 2) 和 nlink = 1。安全关键:如果任何 DIR 具有 nlink = 2,则对其路径执行一次 rm -rf 将静默删除数千个子目录(rmdir 炸弹)。遍历 DIR_INDEX 条目,交叉检查 DIR_ITEM 以检测哈希冲突(经经验验证,冲突为 0)。
remove_orphan_inode_subtrees.c从 FS_TREE 中删除孤立 inode 子树(DIR 家族 + 独立 REG)。对于每个目标:遍历并删除 EXTENT_DATA、INODE_REF、INODE_EXTREF、XATTR,最后是 INODE_ITEM。每个 DIR 家族一个事务(每个子树原子性),独立 REG 每 50 个一块。硬编码的偏执性排除列表。
⚠️ 主要安全警告:请参见下面的“防弹子集标准”。
remove_stale_ptrs_v2.cremove_stale_ptrs 的改进版本:检测带有 parent expected_key 的空叶子(v1 跳过了这种情况)、递归两级扫描(根→level1 + level1→叶子)、动态缓冲区(无 512 限制)、容忍 read_tree_block 失败。
insert_one_extent_poc.c带验证的单个 extent 插入的概念验证。用于在运行 rebuild_extent_tree_apply 之前验证 API 路径。
在 2026-04-05 会话期间,remove_orphan_inode_subtrees 在同一个 BUG_ON 断言上崩溃了两次,原因不同:
崩溃向量 1:在混合叶子(gen 3601,同时包含孤立和活跃 inode)上直接 btrfs_cow_block(leaf) → update_ref_for_cow 遍历子节点 → __btrfs_mod_ref(inc=1) 对过时的兄弟子节点操作 → btrfs_free_extent(phantom) 返回 -ENOENT → BUG_ON → SIGABRT。
崩溃向量 2(后来发现,通过过滤避免):在清理后,btrfs_del_items 将叶子内容减少到 LEAF_DATA_SIZE/4 = 4096 字节以下 → 调用 push_leaf_left(sibling) 或 push_leaf_right(sibling) → 如果兄弟节点具有 gen ≤ last_snapshot = 3701,则 btrfs_block_can_be_shared 返回 1 → update_ref_for_cow 进入 refs > 1 路径 → btrfs_inc_ref(cow_sibling, 0) → __btrfs_mod_ref(cow, level=0, inc=1) → 遍历过时兄弟节点的所有 EXTENT_DATA → btrfs_inc_extent_ref(phantom_bytenr) → BUG_ON(err) 在 extent-tree.c:1302 → SIGABRT。
标志 fs_info->rebuilding_extent_tree = 1 和 trans->reinit_extent_tree = true 并不能拯救 INC 路径:它们仅豁免 BTRFS_DROP_DELAYED_REF(在 extent-tree.c:3885 中验证)。BTRFS_ADD_DELAYED_REF(来自 btrfs_inc_ref)是致命的。
对于任何将要删除的目标 inode 的防弹标准:
INODE_ITEM 的叶子具有 gen > 3701(崩溃后)gen > 3701used 字节数 > 4096(不触发重新平衡)gen > 3701(即使条件 3 失败,重新平衡到崩溃后的兄弟节点也是安全的)disk_bytenr)在当前 extent tree 中可解析(backref 查找时不返回 -ENOENT)违反条件 3+4 中的任何一个都会触发崩溃向量 2。条件 5 通过 reinit_extent_tree 对 DROP 豁免,但对 INC 不豁免(而 push_leaf_left 调用的正是 INC)。
对于任何候选孤立 inode 集,遍历 FS_TREE 转储并根据5个防弹标准对每个目标叶子进行分类。示例模式(匿名化):