如何对一个有大量文件(>100GB)的本地目录做历史版本管理?

问题背景

我在给一个大型游戏(解压后体积 > 100 GB)做修改。

由于游戏文件散乱在各个子目录,我必须在不同目录中增删内容。而且,因为大部分目录都有 10~1000 个项目,一旦我离开某个目录,再回来的时候就很难记住自己刚才添加/删除了什么。

有的时候我需要应用一些实验性的补丁(也就是一组覆盖到游戏目录的文件)。运气好的话,补丁会正常工作,但更多时候补丁没法工作,所以我需要撤回刚才的补丁。另一方面,因为我本身在玩这个游戏,和别人联机时我也不得不暂时撤回大量的补丁更改,并在事后加回来。

问题描述

我厌倦了每一次应用后手动撤回的过程,想到了 Git 这个版本管理系统提供的功能。是否有办法随时对这个目录高效地做历史记录存档,使我能随时撤回到某个状态?

我现在在 Arch Linux 下,测试 Windows 的解决方案比较麻烦,不过并不是对我来说没用,如果有的话也很欢迎。

一些要求

  1. 能够对当前目录打快照,记录为一个时间点,或者像 Git 一样记录为一次提交;
  2. 随时可以回到某个时间点的位置,将目录内全部内容恢复成当时的样子;
  3. 尽可能地节省空间,例如做文件级别甚至二进制级别的差分(只储存变化的内容)、压缩。

一些不太行的解决方案

1. 手动复制几份游戏

嗯……我的硬盘空间有限,不能支撑复制游戏四五遍。

2. Git + Git LFS

Git LFS 是我最先想到的,但我稍做研究后发现它的目的并非完全符合我的需求。首先,Git LFS 是一个服务器扩展,也就是说它需要被安装在远程 Git 服务器上,纯本地使用会有困难;其次,Git LFS 储存文件的方式简单粗暴,不做任何压缩,而且实际上很慢(参考 《Git LFS 很糟糕》)。

3. Windows 文件历史记录 / Onedrive

首先我并不在 Windows 下……好吧,我也试了试 Windows 原生的文件历史记录,果不其然地在初次备份时卡住了。

而且 Windows 文件历史记录以文件为单位记录每一次变动,我并不需要这么细致的历史记录:只要能让我回到每次打补丁前后的状态就可以了。

4. BTRFS Snapshot

我的游戏并不在 BTRFS 文件系统上,而是 NTFS,所以用不了这个解决方案。

5. Restic(以及类似的多版本备份软件)

备份软件通常都只能提取备份(也就是将某一版本的备份恢复到一个新的目录),而不是切换版本(在同一个目录下切换不同版本的文件树),所以不太符合需求。


欢迎各位集思广益。

已有的解决方案比较

列出了下面已经提到的各类方案的比较,供参考。

  • 使用何种增量差分方法:按二进制>按磁盘块>按文件项>不增量
  • 是否能快速切换的解释:即是否能快速切换到一个版本并运行游戏。备份软件提供的只读挂载不考虑,因为游戏可能需要在目录下读写和创建临时文件,会导致游戏无法运行
  • 额外空间开销:越小越好
方法名 使用何种增量差分方法 有无数据压缩 是否能快速切换 版本管理 读写性能 额外空间开销
手动复制 不增量 N Y 仅快照
SVN 按二进制 Y Y 冲突合并 2倍体积(服务器仓库+客户端.svn目录)
Git-LFS/Git-Annex 按文件项 N Y 冲突合并 1倍体积(.git目录)
BTRFS 按磁盘块 Y Y 仅快照
备份软件(Restic/Kopia) 按二进制 Y N(只能挂载只读,无法修改) 仅快照 1倍体积(备份目录)
rsync 按二进制 N Y 仅快照 1倍体积(备份目录)
VHD/QCOW 按磁盘块 Y(NTFS 提供) Y 仅快照
文件历史记录/Onedrive 按文件项 未知 N(只能恢复单个文件) 仅快照 1倍体积(备份目录)
4 Likes

这个问题挺好的,不过,你现在是什么系统呢?既然你提到了不在 WIndows 下面。另外如果你可以接受什么样的解决方案也很重要。

版本控制不只有Git,你这种情况适用于SVN。
另外,还有个比较偏门也比较复杂的方法:把你需要和朋友联机的版本做成vhd,然后在这个vhd的基础上做差量vhd,再在差量vhd上做SVN等的版本控制。这样的好处是,当你有需要和朋友联机时直接挂载基础vhd就行了(记得只读,或另创一个差量),速度比临时回退版本快得多得多,和朋友玩完了再挂载差量vhd

3 Likes

你都想到了git,为啥不考虑svn,这么适合

1 Like

另外搞个目录,把所有文件做个硬链接,新目录就相当于一个副本了。但是,不能覆写文件,这样会把硬链接指向的原始文件本身给修改了,只能删除原链接再替换。

Kopia

1 Like

感谢提醒,我在问题里补充了。

感谢二位指点。我尝试一下,稍后回来报告结果。

感觉差量 vhd 是个思路啊 :joy: 我也研究一下。

这个没了解过啊,是 Restic 那种吗?Restic 应该只能提取备份,没法方便地变更一个目录为某个版本。

玩差量vhd前记得备份,省得把base玩坏了找不回原来的数据

git-annex

如果是 btrfs 的话,复制文件时可以使用 cp -r --reflink auto 不占用磁盘空间。windows 得用 refs 才支持,而且不提供命令不好用。

所以你可以考虑弄个 linux 的虚拟机,文件存里面。然后用 nfs 或者 sshfs 提供给 windows 机器使用。不过因为中间转了一下,你玩游戏的时候读写文件肯定会比较卡的。

Kopia有快照后版本管理功能,有自带压缩算法,可以直接挂载查看该版本

不过另外如果NTFS有开文件系统的压缩功能,应该重复文件会透明压缩掉的(可以对抗司马小:dragon_face:【记忆有误,感谢@ hgoldfish 指正】),不过不知道Linux下实现如何

1 Like

不, ntfs 的文件压缩不是你理解的这样。只是把内容用压缩算法压缩一下。

你的想法叫做去重,只有 linux btrfs, linux xfs, bsd zfs, win11 refs, macos apfs 支持。

收到,搜索了下你说的没错。我是记错了

我看了一下,功能上其实是和 Restic 类似的,所以不适合的原因请参照一楼的解释:仅仅挂载作为备份工具已经很足够了,但在我这个场景下用处确实不大,更何况这些挂载实现的性能一般都不高。

我今天测试了一下楼上几位朋友提到的方法,基本都可行。下面介绍一下各自的优点和问题:

1. SVN

SVN 相比 Git,有原生的大二进制文件差分支持,而且有优秀的数据压缩去重机制。

优点:

  1. 都是版本管理系统,用起来和 Git 差不多;
  2. 相比 Git 思想更简单,有良好的二进制差分压缩支持,非常适合全是二进制文件的项目(比如办公文档项目、音视频工程等)。

缺点

  1. 和 Git(分布式版本管理)不同,SVN 是集中式版本管理,因此必须有一个服务器仓库,而不像 Git 那样只会创建一个 .git 目录。如果所有文件都在本地,则工作时需要占用 3 份空间。以我的游戏文件为例(原目录共 160GB),会创建下面几个目录:
    • 服务器仓库:数据压缩过,约 120GB;
    • checkout 出的仓库的工作区:也就是游戏本体,160GB;
    • checkout 出的仓库的暂存对象(在 .svn 子目录下):170GB。

关键就是这里的「暂存对象」,由于 SVN 的设计缺陷,必须存在。因此在开始工作前,450GB 的硬盘就给出去了,比较大的项目里不太能接受。

2. 虚拟硬盘文件(VHD/QCOW2…)

@cloudskytian 提出的想法。先说结论,这是目前我看到的最符合需求的解决方案。

优点:

  1. 配合 NTFS 文件系统可以做到透明压缩(实测 160GB 游戏仅占用 145GB 空间);
  2. 可以随时挂载不同的 Overlay 镜像,达到版本管理的效果;
  3. 只占用一份空间,随时可挂载为可读写的镜像。

缺点

  1. 本不是用来版本管理的,所以没有高级功能(分支、合并等),只能单线发展,而且需要自己记录不同版本的区别;
  2. 对操作系统强依赖。VHD 文件只支持 Windows,QCOW2 文件对 Linux 支持更好;
  3. 因为是挂载方式访问,速度较慢(实测 NTFS 带压缩文件系统下,顺序读 500 MB/s,写 100 MB/s);
  4. 不保证安全性。断电可能导致镜像损坏。

然而瑕不掩瑜,目前这种方式的切换版本的速度最快、占用体积最小,而且我不需要复杂的版本管理,小概率的数据丢失也可以接受,非常符合我的需要。

作为补充信息供参考,下面是我建立的过程:

过程(点击展开)
# 开始前,请安装 QEMU;
# 下面均假设原游戏在 /game 下。

# 1. 创建基础镜像
qemu-img create -f qcow2 base.qcow2 1T # 注意这里的 1T 是最大大小,镜像不会真的占用 1TB

# 2. 准备挂载镜像
sudo modprobe nbd max_part=16 # 启用网络块设备内核模块
sudo qemu-nbd --connect=/dev/nbd0 base.qcow2 # 连接 base.qcow2 镜像到 /dev/nbd0

# 3. 格式化为 NTFS 分区,启用压缩
sudo mkfs.ntfs --quick --enable-compression /dev/nbd0

# 4. 挂载镜像
sudo mount /dev/nbd0 /mnt # 这里直接挂载到 /mnt,也可选择其他目录

# 5. 复制文件
rsync --archive --human-readable --info=progress2 /game /mnt

# 6. 解除挂载
sudo umount /mnt
sudo qemu-nbd --disconnect /dev/nbd0

这样就完成了。以后挂载只需做上面的 2、4 两步即可。

如果需要新增一个版本,则需要选择下面两种方法之一(区别见 2015 年的 QCOW2 幻灯片):

a. 创建覆盖镜像

# 创建覆盖镜像 base.qcow2 -> version1.qcow2
qemu-img create -f qcow2 -b base.qcow2 -F qcow2 version1.qcow2

然后直接按 2、4 两步挂载新镜像,新的变更就会写到 version1.qcow2 中,而 base.qcow2 保持不变。

同理可以在 version1.qcow2 上创建覆盖镜像 version2.qcow2,形成一条镜像链。

注意:一旦创建了覆盖镜像,原镜像就不可以轻易修改了,原因见上面的幻灯片。

b. 创建快照

我后续查询发现,QCOW2 文件支持在一个文件内包含多个快照,使得多版本管理变得更加简单。可以用以下命令直接创建当前时刻的快照:

# 注:使用前请取消挂载镜像!

qemu-img snapshot -c base base.qcow2 

# ……做一些事之后……

qemu-img snapshot -c version1 base.qcow2

恢复到之前的快照:

qemu-img snapshot -a base base.qcow2

这样就完美符合要求了。撒花。


如果有更好的方案欢迎继续提出,我也会来研讨可行性。

3 Likes

突然想起了虚拟机的增量备份(

https://www.evercraft.co/lite 这个行不行啊

直接在虚拟机里面作业,然后创建快照,要管理的话直接回退到指定的快照就行。这个应该是比较方便简单的办法了,但是对于硬盘来说可能占用比较高

谢谢二位,不过 VHD/QCOW2 其实就是虚拟机快照的底层技术,15 楼总结的解决方案已经包含了这种技术的使用了。问题也比较明显,一是性能不太高,二是不保证数据安全(突然断电可能导致损坏)。

这个我还真没了解过。看了一下好像大体挺符合需求的,不过因为要付费而且没有试用版,不是很清楚他们的实现方法。

从文档里的说法来看,可能和 Git LFS 差不多?按文件进行差分,然后储存历史文件。您有深度测试过性能、速度和占用吗?

也可以试试 rsync

参考:rsync用于数据迁移/备份的几个细节
重点看一下【 已经备份过的文件不再重复拷贝】这一节