- 灾备
- CLI
- 存储
【免费下载链接】bup
Very efficient backup system based on the git packfile format, providing fast incremental saves and global deduplication (among and within files, including virtual machine images). Please post problems or patches to the mailing list for discussion (see the end of the README below).
bup 是一款以 Git packfile 格式为核心的高效备份系统,本文基于仓库根目录的 DESIGN.md 展开,系统讲解其源码布局、hashsplit 分块去重、多索引(midx)与布隆过滤器(bloom)、目录树拆分、详细元数据(.bupm)、文件系统索引等核心设计,并逐一对标 lib/bup 下的真实实现与测试,帮助想要为 bup 添加新功能、修复 Bug 或深入理解其工作原理的开发者快速建立完整认知。
从源码布局开始:bup 的代码组织
bup 的代码以 Python 为主,仅在性能敏感处使用 C 实现(如bupsplit.c、_helpers.c)。理解代码的第一步是掌握它的目录组织方式。
主程序:一个启动 Python 解释器的 C 程序
bup 的主程序是一个相当小的 C 程序,其主要职责是初始化正确的 Python 解释器,然后运行bup.main.main()。之所以采用这种"薄壳 + Python 主体"的架构,DESIGN.md 给出了三条理由:
- 规避 Python 对非常规命令行参数的崩溃问题:路径本质上是任意字节序列,而 Python 对部分 Unicode 不友好的参数会崩溃(对应
src/bup/io.c中直接操作进程 argv 的实现思路); - 灵活应对上游变更:例如 Python 3.9 系列中对进程参数列表操作能力的破坏;
- 摆脱
#!/...路径绑定:不再受/usr/bin/python、/usr/bin/python3等路径在./configure后发生变化的影响。
bup 使用的 Python 版本由./configure选择的python-config程序决定;它会尝试选择一个合适的默认值,除非环境中设置了BUP_PYTHON_CONFIG。从源码结构看,C 侧入口位于 src/bup/io.c,Python 侧主入口为 lib/bup/main.py,二者通过bup.main.main()衔接。
内部与外部子命令
bup 同时支持内部与外部两种子命令:
- 内部子命令:最常见,全部位于 lib/bup/cmd。它们必须是名为
lib/bup/cmd/COMMAND.py的 Python 模块,且必须包含一个接收二进制(bytes 而非 str)命令行参数的main(argv)函数。子命令名中的连字符在文件名中要写成下划线(例如bup save对应lib/bup/cmd/save.py); - 外部子命令:位于 lib/cmd,例如
bup-import-rdiff-backup、bup-import-rsnapshot。
Python 代码与包命名
所有 Python 代码都位于lib/bup下,lib/bup/*.py即 bup 依赖的 Python 模块。这个目录名看上去冗余("bup/bup"),但 DESIGN.md 解释了原因:放在顶层会与bup命令本身冲突,换成其他名字又"从根本上就是错的",因此必须让程序能写出from bup import index且正确工作,最终形成了这个略长的路径。
仓库结构:bup 仓库本质上就是 Git 仓库
bup 的核心使命可以概括为在两种数据结构之间"复制"数据:
- 计算机的文件系统;
- bup 仓库(它同样驻留在文件系统上)。
从文件系统复制到仓库即"备份",通常由bup save发起;反向操作(从仓库恢复到文件系统)则有bup restore、bup ftp、bup fuse、bup web等多种选择。
关键在于:一个 bup 仓库就是一个 git 仓库。blob、tree、commit、ref 这四种 git 构建块在 bup 中格式完全一致,因此你可以直接用 git 操作 bup 仓库而基本不会破坏任何东西——这意味着数据可以通过 git 抢救出来,开发时也可以借助git rev-list、git log、git diff、git show等工具探索仓库、辅助调试。
不过 bup 在运用这些工具时与纯 git 略有不同,目的是弥补 git 用于大型备份时的三个缺陷:
- a) git 面对超大文件会崩溃——它用 xdelta 做增量,一般要把整个文件加载进内存;
- b) git 面对海量文件太慢;
- c) git 不存储详细的文件系统元数据。
接下来的章节逐一剖析 bup 如何解决这三个问题。
处理大文件:hashsplitting 分块去重
git 无法处理超大文件的根本原因是 xdelta 的"一次加载整个文件"策略;而 bup 换了一条完全不同的路——hashsplitting(哈希分块)。
滚动校验和:从 stupidsum 到 rollsum
hashsplitting 的思路看似简单:逐字节读文件,对最近 64 字节计算滚动校验和。64 这个窗口大小是作者"Avery 凭空挑出来的";最初的算法叫 "stupidsum"(作者只记得入门级校验和算法),后来被替换为基于 librsync 代码的 "rollsum",与 rsync 的做法本质相同,只是 bup 使用固定窗口。
rollsum 的实现位于 lib/bup/bupsplit.c 与 lib/bup/bupsplit.h。从源码可以确认关键常量:
BUP_WINDOWBITS (6)、BUP_WINDOWSIZE (1<<6),即 64 字节滚动窗口;ROLLSUM_CHAR_OFFSET 31:注释说明作者非科学地尝试了 0 与 7919,最终沿用 librsync 的 31;- 校验和内部状态为
s1、s2两个分量,摘要为(s1 << 16) | (s2 & 0xffff),得到一个 32 位整数。
rollsum_sum()之外还附带bupsplit_selftest()自检逻辑,用于在启动时验证算法一致性。
分块判定:13 位全 1 即断点
算法取 rollsum 的最低 13 位,若全部为 1 则视为一个块的结束。因此平均每2^13 = 8192字节出现一次断点,平均块大小约 8 KB;同时将最大块大小封顶为平均值的 4 倍(4 * 2^13),避免出现任意大的块。13 这个位数同样是随手选的——DESIGN.md 甚至在正文中邀请读者去邮件列表论证更优取值。
值得注意的细节:尽管平均块大小是 8192 字节,实际块大小分布并不均匀,而是偏向小端(大致呈"指数分布"形态):对块内每个字节而言,它是块末字节的概率都是 1/8192。
为什么这样分块效果好:块边界稳定性
分块完成后,每个块以 sha1 为索引单独存为 git blob。这种做法的妙处在于边界只由内容决定:
假设你有一个约 100 MB 的 mysqldump 导出的 SQL 文本,第二天在文件中间插入了 100 行(变成 100.01 MB)。若用朴素算法(固定 8192 字节切块),第一个字节变化后所有后续块边界全部错位,几乎要重新存储全部数据;而 hashsplitting 下,无论你在中间增删改多少数据,受影响块之前和之后的所有块都完全不变——一次改动最多只影响一个分隔序列(或两个分隔序列之间的字节)。因为约每 8192 个 64 字节序列才有一个是分隔序列,算法无需知道上次如何切分,也能以完全相同的方式切分文件。
块序列的组织:fanout 与树状结构
接下来的问题不那么大但同样棘手:把一串块存成 git blob 后,如何记录它们的顺序?每个 blob 有 20 字节的 sha1,一个简单的块列表占文件长度的20/8192 ≈ 0.25%。对 200 GB 文件,这就是 488 MB 的"序列数据"。作为百分比这无关紧要,但 488 MB 是必须常驻内存的列表开销;更糟的是,第二天备份一个几乎相同的文件,又会多出 488 MB 几乎一样却不完全相同的列表。
bup 没有按相同方式切分这个列表(那样不够"git 化",因为希望用 git 的tree对象承载列表,从而让 git 的引用计数与可达性分析不混乱,也让你能直接git checkout数据)。bup 的做法是把 hashsplit 算法再延伸一步——fanout(扇出):
- 除最低 13 位外,再检查额外的高位位段。注意(很可能因实现 Bug)第 14 位(标记为
x)被忽略,fanout 从x左侧开始; - 以 4 位 fanout 为例:当滚动校验和的接下来 4 位也为全 1 时,就把一串块并入一个 tree 对象。因为最低 13 位已为 1,块的组边界必然同时是某个块的边界;
- 组太多时,再用另外 4 位把组聚成"超级组",如此递推,最终得到一个真正由 blob 构成的树——git
tree对象正是为此设计的。
举例:模式...... '1011'1111'1111' 'x1'1111'1111'1111中,被忽略的第 14 位左侧有 8 个(两组 4 个)连续 1,于是产生一棵两层深的树。
树的妙处与块列表相同:树结构对文件修改同样稳定。任意一次修改只影响包含该修改的块、以及包含该块的组,沿树向上类推——发生变化的 git 对象数量是 O(log n)(n 为块数)。log 200 GB(底数约 16)并不是很大的数,而每个"未改变"的 git 对象都可以在下次备份中直接复用,因此去重效果惊人。
此外,hashsplit 树格式还带来了两个附带优势:支持随机访问而非顺序访问(bup fuse即是例证),以及快速展示大文件间差异(虽然 bup 自己尚未实现,但可用git diff -M -C -C backup1 backup2 -- filename一试)。
以上算法对应的 Python 封装在 lib/bup/hashsplit.py:其中BUP_BLOBBITS = 13对应分块位数、fanout = 16对应 4 位 fanout、MAX_PER_TREE = 256限制每棵树的条目上限,split_to_shalist()通过分层栈_squish()将 blob 逐级合并成 tree,split_to_blob_or_tree()则在块数足够少时直接返回单个 blob 或 tree。
处理海量文件:直接写 packfile
第二个 git 缺陷是对象太多时性能崩溃。git 向仓库添加新对象的方式是每个 blob 一个文件,之后再git gc合并(同时做高效 xdelta 压缩、丢弃无关文件)。对源码仓库来说,git gc虽慢但换来超高效存储与快速访问是值得的;对备份则相反——备份数据几乎从不访问,存储时间优先,检索时间无所谓。
按"每个对象一个文件"的方式备份 200 GB 文件意味着:先创建 2400 万个 8 KB 小文件,再复制进 200 GB 的 packfile,然后删除这 2400 万个文件——需要约 400 GB 磁盘、大量随机磁盘寻道、数据过两遍。bup 不这么做,而是直接把 packfile 写出来。这些 packfile 依然是 git 格式,git 在它们写好之后可以正常访问。对应实现为 lib/bup/git.py 中的PackWriter(即 DESIGN.md 所指的git.PackWriter)。
海量 packfile 的检索优化:midx 与 bloom
idx 文件的检索模型与瓶颈
每个 git packfile(*.pack)配有一个*.idx:一份"对象哈希 + 文件偏移"的有序表。查找对象时打开 idx、二分查找哈希、取偏移、seek 到 packfile 中读取内容,复杂度约 O(log n),优化首步后可到 O(log(n)-7)。
问题出现在 pack 数量很多时:假设 2400 万对象(约 200 GB 数据)分布在 200 个 1 GB 的 packfile 中,一次查找需遍历每个 pack,ceil(log2(122000)-7)=10 次搜索/包,约 3-4 个 4K 页/文件 × 200 文件 = 600-800 个 4K 页(2.4-3.6 MB)——每次查找对象都要付出这个代价。
git 用户有 MRU(最近使用)优化:相关对象往往聚在同一 pack 中,实际只需 3 页左右。但 bup 没这么幸运:git 用户大部分时间在检索已存在对象(看日志、生成 diff、检出分支),而 bup 大部分时间在查找仓库中不存在的对象(以便备份它们)。对"确定不存在"的查找没有好的优化手段,只能穷举所有 pack 逐一确认。
midx:多 pack 联合索引
bup 引入 midx(multi-idx,读作 "midix")文件来解决上述问题。midx 的结构与 idx 类似:开头一个查找表(lookup table)先缩小搜索范围,再做二分查找;与 idx 固定 256 项的表不同,midx 使用变长查找表,保证整个二分查找可被约束在 midx 文件的单个页内——表告诉你加载哪个页,然后在该页内二分。典型查找因此只需内核换入两个页,优于单个大型 idx;RAM 充足时查找表常驻内存,每次查找只需一页。
midx 文件用bup midx生成(详见 Documentation/man1/bup-midx.1.md)。缺点是生成耗时,且每增加几个 pack 就需重新生成。实现位于 lib/bup/midx.py,PackMidx类包装多个 idx 文件,头部为MIDX、当前版本MIDX_VERSION = 4,exists()通过extract_bits+ fanout 表 + 插值二分定位对象。
midx 是 bup 特有的优化,git 不认识它;但由于它是独立文件,不会妨碍 git 读取仓库。
bloom:可增量更新的布隆过滤器
DESIGN.md 的更新说明指出:Brandon Low 贡献了布隆过滤器实现,对某些用途比 midx 特性更优;他还把 midx 与 bloom 的关键部分用 C 重写以大幅提速。布隆过滤器最大的优点是可以随每个新 idx 增量更新,无需从头重建,这让更新阶段快得多,也使得 midx 可以不必频繁重新生成。
布隆过滤器的参数推演完整记录在 lib/bup/bloom.py 的文件头注释中:通过分析 k(每个条目设置的位数)、过滤器大小与条目数的比值(m/n)、假阳性率 pfalse 的关系,并结合"用 160 位 SHA1 寻址、每对象 8192 字节"的仓库规模估算,最终选择k=4 或 k=5;k=5 空间更省且假阳性率整体更好,在能代表仓库时优先。BloomReader.exists()的语义值得注意:返回假则对象肯定不存在,返回真则可能存在,仍需二次确认。过滤器当前版本BLOOM_VERSION = 2,文件头为BLOM。
处理大目录:tree splitting(目录树拆分)
与超大文件类似,git 对频繁变化的大型目录同样不友好:目录存储在单个 tree 对象中,每个条目占用 28 字节加文件名长度;文件一多 tree 对象就非常大,且目录中每新建一个文件都要求存储一个全新的 tree 对象——想象一个含数万文件的活跃 Maildir。
若通过bup.split.trees配置项允许树拆分,bup 就不会把一个目录写成一个可能巨大的 git tree 对象,而是拆成"叶子"为 git tree 对象的子树,每棵叶子包含原 tree 的一部分。注意:对bup on HOST ...场景,bup.split.trees必须在HOST端设置。该配置的读取逻辑在 lib/bup/hashsplit.py 的configuration()中(config_get(b'bup.split.trees', opttype='bool'))。
树拆分方式与文件 hashsplitting 类似,但限制拆分只能发生在 tree 条目之间(不能跨越条目)。一个大型目录:
dir/{.bupm,aa,bb,...,zz}可能变成:
dir/.b..1.bupd/{.bupm,aa,...,ii} dir/j..1.bupd/{jj,...,rr} dir/s..1.bupd/{ss,...,zz}命名规则需要仔细理解:
..1.bupd后缀表示dir/的树被拆分了,数字(此处为 1)表示创建的层级数;- 中间层级的名字(如
.b、j、s)由每个子树包含的第一个文件名派生,缩写到最短的唯一合法前缀; - 任意层级中,子树内包含的名字总是大于等于该子树自身的名字、小于同一层级下一个子树的名字——这使得按文件名查找时可以在每一层确定该读哪个拆分子树;
- 拆分树中每个名字都是
PREFIX..DEPTH.bupd格式,DEPTH表示到"叶子"前剩余的层级数。解析时..是PREFIX与补充信息之间的分隔符;补充信息(如DEPTH)总是紧挨着已有信息(从右向左)追加,且永不包含..,这为将来扩展(如PREFIX..SOMETHING_NEW_WITHOUT_DOTS.3.bupd)留有余地;DEPTH恒为[0-9]+序列; - 拆分树的顶层含一个
.bupm文件,其中只有原目录的一个条目;中间层级没有.bupm;叶子层含各自内容的.bupm条目。叶子.bupm中本该属于目录自身的第一个元数据条目为空。
详细元数据:.bupm 与索引元数据存储
git 仓库本质上只存文件内容外加少量信息(如符号链接目标、可执行位),其余文件系统元数据需要另想办法。除早期版本外,bup 在每个 tree 中以.bupm文件存储更完整的元数据:其中每个文件一个条目,第一个.bupm条目对应目录自身("."),名字为空字符串 ""。
.bupm条目本意与 git tree 顺序一致以便并行增量遍历,但目前并未做到:条目按对应 tree 条目"真实(未变形)名字"排序,而非 tree 中的实际名字;不过.bupm排序考虑到了 git 对树的排序方式(名字末尾如同加了 "/",所以fo在fo.之后当且仅当fo是目录)。
每个.bupm条目包含一段变长记录序列,每条记录保存一种元数据:常见记录类型(普通 stat 信息)、符号链接目标、硬链接目标、POSIX1e ACL 等。完整列表见 lib/bup/metadata.py——源码中可看到Metadata类的_add_common()/_encode_common()/_load_common_rec()等记录 mode、uid/gid、用户名/组名、rdev、atime/mtime/ctime(纳秒时间戳)、size 的编解码逻辑,以及_rec_tag_posix1e_acl_v2 = 11等记录标签常量。该文件还注释强调"元数据编码尚不稳定(caveat emptor)",开发者应知晓其演进性质。
.bupm文件是可选的:缺失时 bup 将退回到加入元数据之前的旧行为,仅凭 tree 信息恢复文件。
由于.bupm内容应与被索引时的文件系统状态一致,bup 必须把详细元数据记入索引:索引中为每个路径记录四个值——atime、mtime、ctime(timespec 格式)以及一个指向次级"元数据存储"的整数偏移;该存储与索引同名但追加.meta后缀,内含每个路径对应的编码后Metadata对象。为减少存储占用,bup 只写入唯一值,在索引中复用偏移——其有效性依赖于"大量重复元数据记录"的预期;把完整时间戳放入索引则让次级存储不必记录这些值(编码前先清空),时间戳差异不参与元数据唯一性判定。
硬链接记录
bup 支持记录与恢复硬链接:索引时跟踪对应相同 dev/inode 对的路径集合,信息存放在与索引同名但以.hlink结尾的可选文件中。若多次索引运行之间硬链接关系发生变化,bup 会在其被要求重新索引的子树内注意到并更新.hlink信息。当前实现有两个限制:拒绝链接到恢复树之外的文件;若恢复树与保存树跨越不同的文件系统集合,完整硬链接组可能无法恢复。
仓库分类学(Repository Taxonomy)
由于有意改动与早期 Bug,仓库中可能出现的数据格式随版本有所演变。DESIGN.md 列举如下:
- 无 bup 元数据的 tree 对象:可能没有
.bupm文件(由 git 或支持元数据之前的 bup 版本创建)。未来也可能源于修复操作,但眼下尚不可能;abridgement 修复(见下)接近于此,但最终会留下一个除 "." 外全为空白条目的.bupm; - 被"删节"(abridged)的
.bupm:因 0.25 引入的 Bug(commit16f9f9829038f25aec80ebfae3c882a66281e145,"save-cmd.py: don't crash when a path disappears between index and save")导致条目缺失,0.30.1 由 commit47891d8951a95b8e0d9ca94387107cdf12ca3d3c("save: add empty metadata if reading fails")修复。相关命令:bup-validate-refs --bupm与bup get --repair; - 含"空"条目的
.bupm:某路径的条目是没有任何属性的Metadata()对象编码。可能是"合成"目录(通过 save strip/graft 操作创建)的 "." 条目,也可能是上述删节问题的修复所致,还可能来自修复操作(参见bup-get(1)); - 拆分树出现之前创建的仓库:自然没有拆分树;设置了
bup.split.trees为 false 的当前仓库同样如此。
语义约定:目录的空元数据条目是预期的(如目录在其父.bupm中的条目、--graft创建的合成目录);非目录的空条目则表明元数据丢失(如上述删节修复)。VFS 层打算对元数据丢失的路径提供受限权限(如 umask 077)。
文件系统交互:索引与保存
存储只是备份的一半,决定"存什么"是另一半。
最朴素的方案是把tar的输出管道给bup split——让 tar 做判断。若喜欢 tar 文件,这完全可行,但有两大问题:
- 结果不易"随机读取":tar 没有索引,要从 200 GB 备份中恢复单个文件可能要先读完 200 GB(tar 即 "tape archive",磁带时代别无选择);
- tar 不记得上次备份过哪些文件:每次都要重新读取全部内容生成 tar 包,即使大部分随后会被 bup 去重跳过——远比必要慢得多(GNU tar 的增量模式可部分缓解,但仍需手动决定哪些备份是增量的、哪些是完整的,更易出错)。
因此 bup 把备份分为两大步骤:a) 索引文件系统;b) 把文件内容存入仓库。
索引文件系统:bupindex
把索引阶段独立成单独的程序(bup index)是非传统的做法,但带来多重优势:
- 索引远比备份快:先快速生成索引(
.bup/bupindex),再获得一个可靠、不撒谎的完成进度条——百分比是"真实"的完成度,而不是像 rsync 那样按文件个数计数(结果一个 80 GB 虚拟机镜像 + 1000 个小文件会给出荒谬的进度); - 易于调试与测试:可以只摆弄索引而不真正备份文件;
- 可替换性:可以用别的程序替换
bup index而不必改动bup save——例如用 inotify(Windows/macOS 也有类似服务)驱动的实现会快几个数量级; - git 也这么做(git 很优秀)。
重要澄清:.bup/bupindex不是 git 的.git/index。后者在 bup 中不被使用——bup 仓库对 git 而言是"裸仓库",无工作树也无索引。但 bupindex 与 git index 目的一致(因此仍叫"index"),只是针对 bup 场景重新设计:git 的索引只能线性搜索文件,在 git 量级飞快、在 bup 的百万文件量级就太慢了。
bupindex 的关键特性:
- 遍历它远快于遍历真实文件系统(如
find); - 删掉它可以通过重新索引文件系统重建(只是等待很烦),它本身对仓库并非关键;
- 可以只遍历特定子树;
- 一个文件系统无需多个索引:它不存备份相关内容,只存文件元数据,本质是文件系统现有元数据的缓存。可在多个仓库间、多个用户间共享;备份到多个远程仓库时也可共用一个 bupindex,因为文件系统始终相同;
- 索引中的文件名是绝对路径,以确保只需一个 bupindex 且彼此可互换。
关于文件"脏"的判定
bup save的概念很简单:遍历索引,备份任何"脏"(dirty,即仓库中尚不存在)的文件。但脏的判定比听上去复杂。
索引存储文件列表、属性及其 git 对象 id(sha1,若已知);"若已知"就是IX_HASHVALID标志(lib/bup/index.py 中IX_HASHVALID = 0x4000):
bup save备份文件后设置 sha1 并置位IX_HASHVALID;bup index发现文件变化时保留 sha1 但复位IX_HASHVALID。
因为索引可在用户、仓库、备份之间共享,IX_HASHVALID不意味着你的仓库里真有那个 sha1——它只表示"如果你有它,就不需要备份该文件"。所以bup save必须逐一确认每个文件哈希确实存在,而不只是"有效"。
不过存在一个关键优化:如果某个 tree 的哈希有效且存在(如/usr),就无需再检查其所有子项——由 git tree/blob 的性质决定,仓库有效且拥有该 tree 对象就意味着拥有它指向的所有 blob;你不会先备份 tree 再漏掉它的 blob,所以下次无需重复检查(深究这种二次校验属于bup fsck/git fsck的职责)。因此bup save面对"干净"索引(全部IX_HASHVALID)可以极快:只需确认顶层IX_HASHVALID的 sha1 存在于仓库,存在即完成。
同理,即使索引并非全有效,只要某个子树IX_HASHVALID且其 sha1 在仓库中,就可以避免递归进入该子树。净效果是:只要不丢索引,bup save总能飞快运行。
另一个有趣的技巧:即使IX_HASHVALID未置位,只要该文件 sha1 已在仓库中,也可以跳过备份——这意味着你选择了不备份该文件的最新版本,新备份集只包含"最近已知有效"的版本。这适合对小文件频繁备份、对文件大且低频的备份组合:每次备份都"完整"(包含所有小文件与大文件),但中间几次备份携带的是大文件的过期副本。注意 DESIGN.md 明确说明该技巧当前尚未实现,且bup save --smaller是完全不存储更大文件。
最后,恢复场景也能玩出花样:恢复某目录后可立即更新 bupindex,再在其上恢复另一份备份时就能对比索引与备份集、只更新变化的文件(若用户在已恢复的系统上使用文件而索引未更新,还能得到"非冲突文件的自动合并"——穷人版分布式文件系统)。可惜bup restore尚未实现该功能。
关于 'bup save' 如何工作
DESIGN.md 用一句玩笑带过:"这个章节太无聊了,已省略。一旦你理解了索引,bup save 就没有什么特别的了。"(对应实现仍可在 lib/bup/cmd/save.py 中查看。)
检索备份:bup VFS 层
与多数备份工具相比,bup 存储格式的一个优点在于容易读取单个文件、甚至文件的某一部分,因此只读虚拟文件系统容易实现且性能良好。基于 git 的 commit 结构甚至可以实现事务性读写文件系统(分支、合并),但那已超出 bup 范围。
bup ls、bup ftp、bup fuse三个命令都通过 VFS(虚拟文件系统)层访问仓库,核心实现为 lib/bup/vfs.py。两点须知:
- 这些命令完全不使用 bupindex;
- 为方便用户,它们把 refs/commits/trees 呈现为单一层级(文件系统形态)——这并非 git 仓库的真实组织方式,切勿混淆。
处理 Python 3 对字符串的坚持:二进制 argv 与字节安全
在 Python 2 中字符串即字节,bup 用它处理各种数据;Python 3 做了全面的不兼容改动,把所有字符串视为 Unicode('foo'等同于u'foo')。尤其糟糕的是,Python 3 起初坚持把很多"并非字符串"的东西当字符串:用户名、组名、文件系统路径等,在多数平台上都无法保证总能表示为 Unicode。
Python 3 后来逐步让步,补充了os.environb、允许open(b'foo'...)等字节参数接口;进而提出 bytesmuggling(PEP 383 概念)作为更全面的方案。理论上这或许足够,但 bup 的随机化测试发现部分二进制参数会在 Python 启动时崩溃,表现为:
Fatal Python error: _PyMainInterpreterConfig_Read: memory allocation failed ValueError: character U+134bd2 is not in range [U+0000; U+10ffff](该问题最初在 Python 3.7 中发现,其他版本亦有观察。最终 Johannes Berg 追查到 glibc 侧的根因,但 bup 团队判断修复普及仍需很长时间。)
在追查到该 Bug 之前,bup 曾尝试通过操纵LC_CTYPE来完全绕开问题,但该方案较复杂;弄清崩溃原因后,bup 决定让 Python 3 "正常运行"并就地绕开问题:
- 自行包装若干错误返回 Unicode 字符串的东西(libacl、libreadline、hostname 等);
- 为规避
sys.argv导致的致命崩溃,把 bup 从 Python 脚本改成二进制可执行程序,以直接访问进程 argv(这同时让 bup 在系统中显示为 "bup" 而非 python)。
这一决策与本文开头"主程序是启动 Python 解释器的 C 程序"的架构完全呼应——bup二进制在进入bup.main.main()之前就已处理好了原始字节 argv,从根上规避了 Python 启动期对非常规字节参数的崩溃。
结语
DESIGN.md 以一句谦逊的"我们希望你能享受 bup 带来的乐趣,期待你的补丁"收尾。通读全文可以看到,bup 的每一层设计(hashsplit 分块去重、直写 packfile、midx/bloom 检索、目录树拆分、.bupm 元数据、bupindex 索引、二进制 argv 处理)都是针对"git 原生能力 × 大规模备份需求"这一矛盾的精准回应。想进一步深入,可以从 lib/bup/hashsplit.py 的分层栈合并、lib/bup/midx.py 的变长查找表二分、lib/bup/bloom.py 的 k=4/k=5 参数推导,以及 test/int/test_bloom.py、test/int/test_midx.py 等测试入手,对照 Documentation/man1 中的命令手册逐项验证。
- 灾备
- CLI
- 存储
【免费下载链接】bup
Very efficient backup system based on the git packfile format, providing fast incremental saves and global deduplication (among and within files, including virtual machine images). Please post problems or patches to the mailing list for discussion (see the end of the README below).
相关推荐
Bup备份系统:基于Git格式的高效增量备份解决方案
Bup备份系统:基于Git格式的高效增量备份解决方案 Bup是一个基于Git包文件格式的创新备份系统,通过滚动校验和算法实现智能数据分块和全局去重,解决了传统备
灾备CLI存储Pencil2D新手避坑实战指南:一文搞定9个高频问题,告别反复故障排查
Pencil2D新手避坑实战指南:一文搞定9个高频问题,告别反复故障排查 深夜十一点,你刚装好 Pencil2D,满怀期待地打开软件,画布雪白、工具栏亮着,可不
图形学BUP - 分布式备份系统
BUP 分布式备份系统 BUP是一个基于git的分布式备份系统,它可以帮助您轻松地保护您的数据安全,并允许您在任何时候恢复您的文件。 什么是BUP? BUP是基
灾备CLI存储
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考