资讯动态

bup备份系统分层异常处理与数据恢复实战

发布时间:2026/10/29 12:27:43 来源:尧图企业网站定制
凌晨两点被备份告警叫起来大概是做数据保护这一行最不想遇到的场景。我手上这套 bup 备份系统跑了三年多仓库 3TB 出头四千多个 pack 文件平时靠定时任务每天做一次增量 save顺带每周一次 fsck 巡检。某天突然发现bup ls能正常列目录可一读文件内容就抛error: git cat-file换个分支又能正常读。这种“一半好一半坏”的表现恰恰是理解 bup 高可用异常处理机制的入口——它不是一个整体黑盒而是由若干个可独立损坏、也可独立重建的层叠起来的仓库。bup 备份系统的异常处理设计说到底就是在回答一个问题当某一层坏了怎么用剩下的层把这一层重新造出来而不是让整个备份作废。这套机制适合谁看正在用 bup 做长期归档、想搞清楚“坏到什么程度还能救”的人也适合刚接触这套系统、想提前知道“哪些文件千万别手贱删”的人更适合那些手上已经有一套备份、但从来没验证过恢复链路的人——毕竟备份系统的可用性从来不是由写入成功决定的而是由异常状态下能不能还原决定的。1. 先把仓库结构摊开看哪些坏了能救哪些坏了认命聊异常处理之前必须先把这个仓库里到底躺着什么讲清楚。很多人用 bup 用了半年脑子里还是“一堆文件”一旦出事就只能重装重灌白白丢掉几 TB 的数据。实际上 bup 的仓库设计思路非常明确数据本身和查找数据用的索引被彻底分开存放前者不可再生后者随时可以扔。1.1 一个 pack 文件里到底塞了什么bup 不是把一个文件存成一个文件而是把很多个文件的内容分块chunk然后把一堆块打包进一个大文件这个打包产物就是objects/pack/pack-40位十六进制.pack。每个块在写入时用哈希算出唯一标识所以同一个内容块在仓库里只会存一份——这就是 bup 能对多个版本的整机做去重、把 3TB 源数据压到 800GB 仓库的根本原因。关键在于 pack 文件的内部布局。前面是连续的对象数据每个对象带自己的长度前缀文件尾部则保存了一张完整的偏移表包含 fanout 数组、按哈希排序的对象 ID 表、对应的文件偏移和 CRC 校验值。这张尾部表的存在非同小可它意味着只要 pack 文件本身完整即使旁边所有的辅助文件全丢光理论上也能把索引重建出来。这是我判断“能不能救”的第一条基准线。除了.pack同目录下还有几类伴随文件pack-hex.idx单个包的索引记录这个包里有哪些对象、在什么位置是读数据的快速通道。objects/pack/bup.midx把所有.idx合并成一张总表避免打开几千个 idx 文件。仓库越大midx 的价值越明显。objects/pack/bup.bloom布隆过滤器用来快速回答“这个对象大概在不在仓库里”。它是概率性的允许误判为“存在”不允许误判为“不存在”所以它只是个加速器。refs/heads/分支名分支指针内容是一个 commit 的哈希值指向这次备份的元数据快照。BUP_DIR/index文件系统扫描索引记录“上次扫描时哪些文件变了、mtime 是多少、大小是多少”这是增量备份能跑起来的基础。再补一句很多人忽略的objects/pack/下还可能有.par2冗余文件和.tmp临时文件前者是救命用的校验块后者是写入中断留下的残骸两者的处理方式完全相反第四章会细讲。1.2 再生能力的分类表决定你出事时先干什么我把仓库里的东西按“坏了能不能自己造回来”做了个分类这张表我直接贴在运维手册第一页出事的时候照着看能省掉大量瞎猜的时间。文件类型是否可重建重建代价出事时的第一动作.pack对象数据不可重建源端已删就是没了无价立刻停止写入评估损坏范围pack-hex.idx可重建依赖 pack 尾部表中随包大小线性增长先确认 pack 完整再重索引bup.midx完全可重建低到中取决于包数量直接删掉重跑bup midx -abup.bloom完全可重建中需要遍历全部 pack直接删掉重跑bup bloom -crefs/heads/*逻辑上可重建若知道 commit 哈希极低从历史记录或副本里找回哈希BUP_DIR/index完全可重建高要重扫源文件系统删掉重跑bup index -u.par2冗余块可重建高要读全量 pack 重算跑bup fsck -g补生成这张表的核心结论只有一句唯一真正不可再生的是 pack 里的对象数据本身。其余全部属于“缓存”或“加速结构”坏了只是性能问题不是数据问题。理解了这一点你在故障现场的心态会完全不一样——不会因为看到一个红色报错就慌了神而是能冷静判断现在坏的是哪一层。也正因为这个分层我给这套系统的异常处理总结了三条防线从上到下分别是pack 层的冗余校验修复、索引层的丢弃式自愈、提交层的原子性与回滚。三条防线对应三类完全不同的故障形态处理思路也完全不同一条条拆。2. 第一道防线pack 层的冗余校验与就地修复pack 层是唯一承载真实数据的地方也是唯一需要“提前投资”的地方。这里的异常处理机制不是事发时才启动的而是在平时写入完之后就悄悄布好了局——靠的就是 par2 冗余块。2.1 par2 冗余块是怎么算出来的占多少空间bup fsck -ggenerate的本质是调用外部的 par2 工具对每个 pack 文件计算一组恢复块写到同目录的.par2文件里。它的数学基础是里德-所罗门纠删码把原文件切成若干个等长的数据块对每个块算出一个校验值再通过范德蒙矩阵生成若干“恢复块”。恢复块本身不含原始内容但只要丢失的数据块数量不超过恢复块数量就能通过解线性方程组把缺失块反算出来。举个具体例子。假设有一个 512MB 的 pack-b参数指定把它切成 1024 个数据块那么每块正好 512KB。如果冗余比例设成 10%就会生成 102 个恢复块理论上能修复任意位置的 102 个数据块也就是 51MB 的散落损坏。这里有个非常关键的取舍很多人栽在这上面恢复能力取决于“块的数量”不是“字节的数量”。同样是 10% 冗余如果块切得很大比如 16MB 一块那只有 32 个块遇到一个 60MB 的连续坏区比如某段扇区整体失效需要 4 个块就能补但如果遇到 100 个小文件各自坏了一点点散落在 100 个不同的块里32 个恢复块就不够用了。反过来块切得小抗散落损坏的能力强但数据块数量膨胀会导致 par2 文件头变大、计算变慢。我自己的经验配置是单包大小控制在 1GB 以内-b给到 1024 到 2048 之间也就是单块 512KB 到 1MB冗余比例 10%。这套参数在我们的实际介质上扛住过两次磁盘部分坏道每次都是几十个块散落损坏全部就地修复没有触发整包替换。空间代价是仓库体积增加约 10%我认为完全值得——毕竟一次整包失效意味着从源端重跑全部备份那是按天算的成本。实际执行时bup fsck -g只对还没有 par2 文件的包生成冗余所以可以安全地反复跑不用担心重复生成。比较稳妥的做法是把它挂在备份任务后面新增的包当天就把冗余补上而不是等攒了半年再统一跑一遍。# 为所有缺失冗余的包生成 par24 路并行 bup fsck -g -j 4 -v # 想调整冗余比例和块数通过 --par2-options 传给底层工具 bup fsck -g -j 4 --par2-options-r10 -b1024注意--par2-options的具体可用参数取决于你系统上装的 par2 版本动手前先跑一次par2 -h确认支持哪些开关。另外这一步是纯 CPU 密集型的我在 8 核机器上实测吞吐大约 100MB/s2TB 的仓库跑下来接近 6 小时务必放在业务低谷期别和备份任务抢 IO。顺便说一个容易被误解的点生成 par2 的耗时和冗余比例关系不大和仓库总量关系极大。因为计算过程必须把全部原始数据读一遍并算出校验值10% 和 30% 的区别只在于最后那一步矩阵运算的规模。所以千万别想着“我少设点冗余能跑快点”省不了多少时间只是白白降低抗损能力。2.2 修复现场从告警到救回数据的完整过程bup fsck -rrepair是我最不愿意用到、但这三年里确实用过两次的命令。它的工作流程是先校验每个 pack 的 CRC 和数据块完整性发现损坏后尝试调用par2 repair就地修复修复完重新校验。第一次遇到的场景我记得很清楚某块盘的 SMART 报了几个重映射扇区紧接着bup fsck开始报某个 pack 的 CRC 不匹配。当时的第一反应是“完了这个包废了”但跑完-r之后日志显示repaired 37 blocks随后校验通过。整个过程大约 20 分钟——注意修复耗时远小于生成耗时因为修复只需要读取损坏的那个包以及对应的 par2 文件不需要全量扫描。这里有一套我总结出来的处置顺序照做基本不会出错立刻停止所有写入任务。这不是保守是必须。bup 在写入新包时不会去动老包但如果你同时还在跑bup fsck -g或者别的维护脚本可能和修复过程争夺同一批文件句柄问题会变得难以判断。记录故障包的完整文件名和哈希。这个信息后面要用它判断影响范围也就是“这个包里到底装的是哪次备份的哪些文件”。先跑一次只读校验看清有多少个包有问题、每个包坏在什么位置。只读校验不会修改任何东西是安全的。对损坏包执行修复一个一个来还是批量来都行但建议先挑最小的那个包试一次确认工具链本身没问题。修复后重新校验通过之后再做一次实际的bup restore抽样验证。这一步很多人会跳过但校验通过只代表块结构完整不代表内容语义正确抽样恢复一个已知文件是最踏实的确认方式。# 第一步只读校验列出所有有问题的包 bup fsck -v 21 | tee /tmp/fsck-before.log # 第二步对有问题的包执行修复 bup fsck -r -v -j 2 21 | tee /tmp/fsck-repair.log # 第三步再次校验确认 bup fsck -v 21 | tee /tmp/fsck-after.log注意-r会真实修改磁盘上的文件跑之前务必确认备份仓库所在文件系统还有足够剩余空间。par2 修复的过程中会生成临时文件最坏情况下需要预留一整个包大小的空闲空间。还有一条逃坑经验必须说修复前先把损坏的 pack 和对应的 par2 文件复制一份出来。听起来很冗余但我就遇到过一次修复过程中断电结果原包被部分重写、par2 也被消耗掉一部分最后是靠着那份副本才救回来。这个习惯后来救过我至少两次成本只是几十 GB 的临时空间。2.3 兜底方案当 par2 也救不回来的时候必须承认一个现实par2 不是万能的。以下几种情况它就是束手无策——损坏的数据块数量超过恢复块数量、损坏位置恰好落在 par2 文件自身的头部、或者 pack 尾部那张索引表被整段覆盖。这时候的兜底思路是缩小损失范围而不是追求完整恢复。具体做法是先用bup ls逐层遍历这个包对应的分支目录看哪些路径还能列出来再用bup cat-file尝试读取单个对象把还能读出来的对象先bup join导出成普通文件最后把那些读不出来的对象的哈希整理成一份清单拿去和源端的文件清单比对确定到底丢了哪些文件。这套操作听起来很土但在实际场景里非常有用一个 1GB 的包可能装着几千个文件的内容块真正损坏的往往只是其中一小部分。我第二次遇到故障时一个包里 2800 多个对象只坏了 60 多个通过逐个导出的方式救回了 97% 的内容剩下的从源端重新做了一次全量。相比整包作废这个结果好太多了。3. 第二道防线索引层的丢弃式自愈如果说第一道防线是“花钱买保险”那第二道防线的思路完全相反——根本不设防坏了就直接扔掉重建。这种设计看起来粗暴实际上是最省心的因为它的前提是前面说过的索引全部是缓存原始数据在 pack 里。3.1 为什么索引文件坏了不用慌先讲清楚这三层索引的关系很多故障判断失误都是因为没搞清楚谁依赖谁。最底层是pack-hex.idx它属于单个包记录这个包内的对象位置。往上一层是bup.midx它把多个 idx 合并形成跨包的总查找表。再往上是bup.bloom布隆过滤器回答“这个对象在不在仓库里”的概率性问题。三个文件构成了一个从精确到模糊、从慢到快的查找层级。这里的关键认知是midx 和 bloom 都是纯派生产物删除它们不会丢任何数据。你甚至可以在一个正常运行的仓库上直接把bup.midx和bup.bloom删掉系统会退化到逐个打开 idx 的慢速模式功能完全正常。这个特性在故障排查时特别有用——遇到“读数据报奇怪的错”第一招永远是把这两个文件挪走看问题是不是消失了。如果消失了说明是索引缓存过期或损坏重建就行如果没消失那问题在更深的 pack 层得上第一道防线。我自己给这套逻辑总结了一句话索引层的自愈策略就是把“修”换成“删”。因为修一个二进制索引文件的成本远高于重新生成一遍而且修的过程本身还可能引入新的不一致。3.2 midx 与 bloom 的重建步骤与耗时估算重建操作本身很简单难点在于判断该在什么时候重建以及重建需要多久。bup midx -a会扫描objects/pack/下所有 idx 文件合并成一张总表。耗时主要取决于包的数量和仓库总大小关系不大。四千个包的规模我实测大约 2 到 4 分钟主要瓶颈在随机读元数据。所以这个操作可以放心地放进日常维护脚本比如每周跑一次。bup bloom -c就重多了它需要遍历所有 pack 里的每一个对象来填充位数组耗时和对象总数成正比。3TB 仓库的规模一次完整重建大约 40 分钟到 1 小时。这就不能随便跑了得挑时间。我一般只在两种情况下重建一是 bloom 文件本身损坏或缺失二是仓库经历了大规模的包合并或删除比如清理了某个已经废弃的分支。# 重建 midx几乎无风险随时可跑 bup midx -a -v # 重建 bloom耗时较长建议低峰期执行 # k 值取 4 到 6 比较均衡太大哈希开销高太小误判率上升 bup bloom -c -k 5 -v关于-k这个参数多解释一句。它是布隆过滤器的哈希函数个数决定了同一个对象会被映射到几个位上。取值过小位冲突概率高误判率上升取值过大每次查询要算多次哈希CPU 开销上去了。经验公式是最优值约为0.69 × (位数组长度 / 对象数量)但手工算这个没意义直接按通用建议取 4 到 6 就行实测差异不明显。真正的性能瓶颈在随机读 pack不在哈希计算。还有一点值得说bup 支持在写入时自动维护 bloom通过配置项开启后每次 save 完成会自动更新不需要事后完整重建。具体配置项在不同版本里命名略有差异用bup config -l看当前版本支持哪些键找到和 bloom 自动维护相关的那一项设置成启用状态。开启之后日常几乎不用再手动跑完整重建省了很多事。3.3 文件系统索引的重建最贵但也最彻底BUP_DIR/index是唯一一个“重建代价可能很高”的索引。它记录的是文件系统层面的扫描结果重建过程必须重新遍历源端所有文件读取它们的 mtime、大小、类型等元数据。在源端是几百万个小文件、又挂在网络存储上的场景里这一步可能要跑几个小时。所以处理它的时候策略和前面不同——不是“坏了就删”而是先尝试从备份副本恢复。实际做法是给BUP_DIR/index单独做一份每日快照。这个文件不大几百万条目也就几百 MB成本极低但能在关键时刻省掉几个小时的完整重扫。恢复之后只需要跑一次增量bup index -u校验一下改动项就能接着正常备份。如果连快照都没有那就只能老老实实重扫。重扫的时候有个小技巧bup index -u支持指定子目录可以按目录分批跑每跑完一批就落盘一次避免中途失败前功尽弃。# 分批重建文件系统索引降低单次失败的影响面 bup index -u /data/part1 bup index -u /data/part2 bup index -u /data/part3注意重建文件系统索引期间不要同时跑bup save。两者都会读写同一个 index 文件虽然 bup 内部有文件锁但锁竞争会导致 save 直接失败退出白白浪费时间。4. 第三道防线提交层的原子性与中断回滚前两道防线处理的都是“坏了之后怎么修”这一道处理的是“坏在中间怎么办”。备份系统最怕的不是明确失败而是写到一半断电、写到一半进程被杀留下一个状态不明的残局。bup 在这一层的设计思路是用临时文件加原子重命名保证要么完整生效要么完全没发生。4.1 写入顺序与临时文件命名规则搞懂 bup 一次 save 的内部顺序是判断“中断后能不能直接重跑”的前提。整个流程大致分四步扫描与比对。根据BUP_DIR/index里的记录找出与上次相比发生变化的文件只对变化的文件重新分块。这是增量的来源。写对象到新包。新产生的块追加写进一个新的 pack 文件写入过程中文件带.tmp后缀。这一步可能持续很久是中断最容易发生的阶段。落成正式包。数据写完、尾部索引表刷盘之后把.tmp文件重命名为pack-hex.pack同时生成同名的.idx。重命名是原子操作这一步要么完成要么没完成不存在中间态。提交元数据。生成一个 commit 对象记录本次备份的目录树然后把refs/heads/分支名的内容更新为这个 commit 的哈希。同样是先写新文件再原子重命名。这个顺序设计的精妙之处在于commit 指针的更新是整个流程的最后一步。也就是说只要分支指针还没更新这次备份在逻辑上就等于没发生过你可以放心地把它全部丢掉重来。反过来如果指针已经更新说明数据已经全部落盘这次备份是完整的。理解了这一点中断处置的判断就变得非常清晰——看指针不看文件数量。很多人看到objects/pack/下多了一堆.tmp就慌了其实只要分支指针没动这些残骸全都是垃圾删掉重跑就行。4.2 中断后的现场判断什么该删什么千万别动我处理过的最典型的中断场景是备份跑到第 40 分钟时机器重启任务被强行打断。第二天上班一看objects/pack/下多了三个.tmp文件总共占用 12GB分支指针还是前一天的。这种状态该怎么处理我的操作序列是这样第一步确认分支指针的当前值。直接读refs/heads/分支名拿到那个 40 位哈希再用bup ls看这个 commit 对应的目录树是不是能正常遍历。能正常遍历说明上一次备份是完整可用的中断没有破坏它。第二步检查.tmp文件的大小和修改时间。如果大小一直是零或者停在某个很小的值说明刚开写就被打断内容毫无价值。如果大小已经到了几百 MB 甚至 GB 级别说明写了很久但尾部索引表大概率没有落盘这种残包是恢复不了的——因为缺了尾部那张表你根本不知道里面有哪些对象。第三步直接删掉所有.tmp文件。这一步不用犹豫。bup 的正式读路径只认.pack和.idx.tmp文件对读操作完全透明留着只会占用空间、扰乱后续的容量统计。第四步重跑一次同样的 save。因为上一次的 commit 没有提交index 里的扫描记录可能已经更新了取决于中断发生在第几步所以重跑时可能会发现“没东西可备”或者“备少量东西”。这种不确定性是正常的处理办法是加一个bup index -u强制重新比对一遍。# 确认分支指针状态 cat $BUP_DIR/refs/heads/nightly # 验证该 commit 是否可正常遍历 bup ls nightly/ # 清理所有写入残骸 find $BUP_DIR/objects/pack -name *.tmp -mtime 0 -delete # 重新比对并重跑 bup index -u /data bup save -n nightly /data注意清理.tmp之前一定要先确认没有正在运行的 save 进程。多个备份任务并行运行时一个任务留下的临时文件可能是另一个任务的活跃写入目标误删会导致那个任务写出一堆无法识别的东西。用ps加文件句柄检查确认一下比事后排查省事得多。还有一种极端情况.tmp文件被重命名成了正式.pack但对应的.idx没来得及生成。这种“有包无索引”的状态会导致读该包时直接报错。处理方法不是删包那样数据就真没了而是重建索引——这正是第二道防线要解决的问题也说明了三道防线不是孤立的实际故障经常需要跨层操作。4.3 分支指针异常的回退与比对refs/heads/分支名这个文件小到只有 41 个字节但它是整个备份的逻辑入口。它出问题的方式主要有两种内容被改成不存在的哈希或者文件整个消失。第一种情况往往发生在手工操作失误或者某个异常脚本上。处理办法是去仓库里找最近一次可用的 commit。bup 没有提供像 git 那样的 reflog但你可以通过遍历包里所有对象来反查——每个 commit 对象的内容里有时间戳和父提交信息按时间排一下就能找到目标。第二种情况更少见但更麻烦。如果连分支对应的 commit 对象本身也丢了比如所在 pack 损坏那就只能从上一个可用版本开始重新做一次全量。这时候第一道防线的价值就体现出来了如果那个包有 par2 冗余并且能修复你就还能把 commit 捞回来。实际操作中我建议给关键分支的指针值做定期记录比如每次备份成功后把refs/heads/*的内容追加到一个日志文件里。这个操作成本几乎为零但能让你在指针丢失时立刻知道该恢复到哪个哈希。# 每次备份成功后记录指针成本极低 for f in $BUP_DIR/refs/heads/*; do echo $(date -Iseconds) $(basename $f) $(cat $f) done $BUP_DIR/../refs-history.log提示这个日志文件务必放在仓库目录之外否则它会被自己的备份任务扫进去随着历史不断累积白白占用仓库空间。还有一点值得强调分支指针指向的 commit 对象本身也在 pack 里所以“指针完好”和“指针指向的内容完好”是两件事。判断后者需要实际跑一次bup ls加bup cat-file光看指针文件是不够的。这个检查我一般放在每周巡检里两分钟的事能提前发现很多潜在问题。5. 三类故障叠加时的实战恢复顺序单点故障好处理麻烦的是三种机制的问题同时出现。我在第二年就遇到过一次一个 pack 报 CRC 错误同时bup.midx引用了不存在的包分支指针指向的 commit 还恰好落在那个坏包里。当时三个报错一起刷屏很容易判断失误去做一些破坏性的操作。5.1 从最底层开始自下而上恢复这种叠加故障的处理原则只有一条严格自下而上先修数据层再修索引层最后修提交层。顺序颠倒会做无用功甚至会把能救的数据搞丢。具体原因很好理解索引层依赖数据层提交层依赖索引层。你如果在数据层还没修好的时候就去重建 midx重建过程会读到损坏的包生成一份同样不可靠的索引而在索引不可靠的情况下修复指针等于把指针指向一个你验证不了的位置。那次的实际操作顺序是这样的第一步把所有备份任务停掉给整个objects/pack目录做一次快照我们用文件系统级快照秒级完成。这一步是保命符后面无论怎么操作错了都能回退。第二步跑bup fsck -v拿到完整的问题清单。那次的结果是 4120 个包里坏了 3 个其中 2 个只是 CRC 不匹配1 个是尾部表损坏。第三步对那 2 个 CRC 不匹配的包跑bup fsck -r两个都修复成功。对尾部表损坏的那个包par2 也无能为力——因为尾部表本身就是靠 par2 恢复的而恢复块数量不够覆盖整个尾部区域。第四步在确认损坏包无法修复之后把它的.pack、.idx、.par2一起移到隔离目录。注意是移动不是删除后面可能还要从里面捞对象。第五步重建索引。删掉bup.midx和bup.bloom跑bup midx -a再跑bup bloom -c。这一步之后bup ls已经能正常遍历大部分分支了。第六步处理指针。那个指向坏包的 commit 已经不可用了从之前的指针日志里找到上一个可用版本把refs/heads/分支名改回去。第七步做抽样恢复验证。随机挑了 20 个路径覆盖不同的时间点逐个bup restore出来和源端比对哈希。整个流程跑了大概 4 个小时其中 3 小时花在重建 bloom 上。5.2 恢复后的验证清单这里要重点说验证因为它是区分“看起来好了”和“真的好了”的唯一手段。我在那次事故之后整理了一份固定清单每次异常处理完都照着跑一遍。验证项具体操作判定标准包结构完整性bup fsck -v无任何 CRC 或尾部表告警索引可读性bup ls 分支/遍历三层目录无报错、条目数量与预期一致对象可读性bup cat-file -p 哈希抽样 20 个全部能输出内容内容正确性bup restore抽样文件后比对哈希与源端一致增量可用性跑一次bup index -u加bup save正常完成且新增包数量合理冗余覆盖检查新生成的包是否有对应.par2无遗漏最后一项特别容易被忽略。故障恢复之后往往急着恢复业务忘了给新写入的包补冗余结果下次再出事时发现这些新数据完全没有保护。我的做法是把bup fsck -g直接串在恢复流程的最后一步不跑完不算收工。6. 常见问题速查与几条踩过坑的经验三年下来攒了不少问题记录挑高频的整理成表剩下的用文字讲因为有些坑不解释清楚光看结论没用。6.1 高频问题速查表现象最可能的原因处置动作bup ls正常但读文件报 cat-file 错误单个 pack 数据区损坏bup fsck -v定位坏包再-r修复所有操作都变慢无明显报错midx 过期或缺失退化为逐个开 idx跑bup midx -asave 报“对象已存在”但恢复找不到bloom 过期误判为存在删除 bloom 后bup bloom -c重建save 中途退出留下.tmp文件进程被杀或机器重启确认无活跃任务后删除重跑分支指针指向不存在的 commit指针被误改或对应包已损坏从指针日志回退到上一可用版本fsck 报大量包 CRC 错误底层存储介质问题不是软件问题先换盘再谈修复恢复速度极慢仓库过大导致 idx 查找放大建 midx必要时开启 bloom关于最后一条展开说一句仓库规模上去之后“慢”本身就是一个可用性问题。备份系统的价值在于能恢复如果一个 3TB 仓库恢复一个 100MB 目录要跑半小时那在真实故障场景里是没法接受的。所以索引优化不是可选项是必做项。6.2 几条只有踩过才知道的经验第一条不要相信“备份任务成功退出”这个信号。我见过太多次任务日志全绿、但实际恢复失败的情况。原因五花八门——写到一半磁盘满了但错误被吞掉、包写完了但 idx 没落盘、指针更新了但 commit 对象不完整。唯一可信的信号是定期的恢复演练。我现在固定每季度抽一个分支做完整恢复恢复出来的数据跑一遍校验这个习惯帮我提前发现过两次隐患。第二条冗余要跟着数据一起长不能一次配完就不管。早期我的做法是初始化时对所有包跑一次bup fsck -g后来新数据不断进来新包根本没有 par2。直到某次抽查才发现最近半年的数据完全裸奔。正确做法是把它做成增量任务每次 save 之后扫一遍看有没有缺冗余的包。第三条损坏的包不要急着删。哪怕 fsck 说它彻底没救了也先移到隔离目录放一段时间。我救回来的数据里至少有两成是从“已判死刑”的包里通过逐个对象导出的方式捞出来的。删掉就真没了,留着只占点空间。第四条记录比记忆可靠。分支指针、每次备份的包清单、每次 fsck 的结果全部落成日志。故障现场人是慌的靠回忆判断“上次是不是也这样”极不可靠。有了日志很多判断可以在几分钟内做完。第五条先确认存储介质再怀疑软件。我遇到的 pack 损坏里超过八成最终都追溯到磁盘或阵列的问题。遇到成批的 CRC 错误第一反应应该是查 SMART、查阵列日志,而不是去翻 bup 的源码。这个顺序搞反了会浪费大量时间。第六条fsck 要定期跑但别在业务高峰期跑。全量校验是纯读操作看起来无害实际上会把存储的读缓存全部冲掉导致同时在跑的业务出现明显的 IO 抖动。我把它固定在凌晨的低峰窗口而且加了nice和ionice压低优先级。关于最后这条补一个具体的调度技巧bup fsck支持-j并行但并行度不是越高越好。在机械盘阵列上超过 4 路并行反而会让磁头来回寻道总吞吐下降。我在 SSD 上给到 8 路在机械盘上只用 2 到 4 路实测差距能到一倍以上。这个参数没有通用最优值得在自己的存储上实测一遍才能定。最后分享一个我最近才加上去的小习惯给每个 pack 文件记一份“出生档案”也就是它由哪次备份、什么时间、从哪个源目录生成的。这份档案不进仓库单独放一个小数据库里。用处是在包损坏时能立刻知道“这个包对应的是哪台机器的哪个时段”从而判断影响范围、决定是从冗余修复还是直接从源端重跑。以前没有这份档案的时候每次都要靠文件修改时间反推经常推错。这个成本几乎为零的附加记录在真实故障里省下的排查时间是以小时计的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑