AIO Ep17. 冷饭再炒 - ZFS 池的备份方案

 

第 N 次谈数据备份

之前已经专门写过两篇关于数据备份的博客了: 全设备备份解决方案 以及 灾难恢复测试 & 个人备份方案更新, 但之后随着 HomeLab 系统更换, 我转而使用了 PVE 以及 ZFS 存储池.

BTW, 一段时间使用下来, 我对新的 PVE 系统非常满意, 相比之下 unRAID 都更像是一个玩具或者爱好者的产物了(却还卖的不便宜), 很多在 unRAID 里不得不用一堆脚本糊起来的功能都在 PVE 中原生支持了, 存储/权限控制/虚拟机及容器配置/防火墙等功能在 PVE 中都完善的多, Bug 还少很多.

订阅了 PVE 以示对优秀开源软件的支持(当然不订阅显然也是可以使用完整功能的~)订阅了 PVE 以示对优秀开源软件的支持(当然不订阅显然也是可以使用完整功能的~)

关于我手机&电脑的数据备份没什么更新, 所以本篇主要讨论 HomeLab 的数据备份.

ZFS & zrepl (本地副本)

之前使用的 unRAID 的 parity 机制虽然可以算是一种本地的数据冗余, 但在 SSD 上会带来非常严重的性能问题, 故被我弃用. 于是我开始寻求 ZFS 下的本地备份方案.

作为新世代的文件系统, ZFS 提供了一系列高级的功能: Copy-on-Write, 数据损坏的自动检测与修复, RAID-Z, 快照等… 我这里主要用到了快照功能进行数据的备份.
我们可以像这样创建, 查看和销毁一个快照:

1
2
3
4
5
6
# zfs snapshot pool/data@now
# zfs list -t snapshot
NAME USED AVAIL REFER MOUNTPOINT
pool/data@now 540M - 540M -
(此时你可以直接访问 pool/data/.zfs/snapshot 直接读取到快照下的所有内容)
# zfs destroy pool/data@now

我们可以将一个池的快照全量发送到另一个池, 以下的例子中会创建 pool2/backup 这个新 dataset, 并将 pool/data@now 中的所有数据复制到其中:

1
# zfs send pool/data@now | zfs recv pool2/backup

我们还可以增量发送快照, 以下的例子中, 会将 pool/datasnap1snap2 之间的文件修改同步到 pool2/backup, 这要求 pool2/backup 在接收之前已经和 pool/data@snap1 同步:

1
# zfs send -i pool/data@snap1 pool/data@snap2 | zfs recv pool2/backup

对于我的备份需求, 我之前编写了一个 shell 脚本, 用于每周在 SSD 上创建一个快照, 并将其发送到我的 HDD 存储池上. 第一次需要全量发送, 后续只需要增量发送就行了.

脚本开始跑的还行, 但后来就遇到了一些小问题: 例如没法灵活的确定 retention 周期, 如果因为各种原因 send 失败容易失去同步等. 后来我发现了 zrepl, 它自称为 One-stop ZFS backup & replication solution. 我试用了一下感觉还不错, 就弃用了我原来的脚本, 转而使用 zrepl.

zrepl 基本上就是帮我自动处理了上述快照和备份的过程, 有着不错的 tui, 还有自动删除旧快照的功能, 同时通过 hold 上一次 send 时快照的方法保证了每次都能成功 incremental send. (详见其文档)

zrepl status 展示的内容zrepl status 展示的内容

它的文档整体也是比较清晰的, 具体用法我就不再赘述, 读者自行去看官方文档吧.

Restic (云端备份)

之前我使用的 Duplicati 和 Duplicacy 都性能不佳, 对于我数据中可能含有的大量小文件, 他们俩都需要花大量的 CPU 时间进行扫描, 浪费电的同时还不稳定, 容易失败. (Duplicati 的本地数据库还会巨大无比很容易损坏)

zfs send 发送的是 ZFS 文件系统的底层结构 (Merkle tree), 几乎不需要什么计算, 也是顺序读取能发挥出磁盘的全部性能.
我们注意到上面的 zfs sendzfs recv 是直接用 pipe 连接的, 所以理论上我们可以…

1
zfs send pool/data | ssh [email protected] zfs recv remote/backup

事实上, 这种用法也是官方认可的做法, zrepl 也直接支持了到多种远端的 replication 设置. 但有个直接要求就是: Remote 也得支持 ZFS. 为了符合 3-2-1 备份的要求, 这就让我必须再在异地维护一台机器, 不能使用各类对象存储或者网盘了, 成本++. 我想到的相对比较方便的方法是和其他有 NAS 的小伙伴约好, 相互提供存储空间, 互为异地备份, 但后来还是懒了.

那如果强行将 zfs send 保存到文件, 稍后再恢复行不行呢:

1
2
3
zfs send pool/data > backup.zfs
# save backup.zfs to somewhere else (Backblaze, S3, Onedrive...)
zfs recv remote/recovery < backup.zfs

理论上没什么问题, 我也写了一个脚本 zclone 用于 zfs send 的压缩, 分块, 加密及上传. 但搜索一些资料之后发现了问题, 参考这个帖子.

You are somewhat out of luck on this one. The zfs send stream gets validated on receive - until you receive, there is no way to make sure it is sound; and if it isn’t the entire receive will fail and you will lose the entire stream rather than just the affected files.

One of the things I have been pondering writing is a system based on zfs diff to identify files that changed between snapshots, and using that list to incrementally upload changed files between snapshots.

The reason I haven’t done it yet is because zfs diff was broken when I last had a need for such a thing. It has since been fixed, but I haven’t had a chance to go back to that project yet.

Obviously, this is nowhere nearly as efficient as incremental zfs send - if a single byte changes in a 1TB file, you would have to transfer the whole file.

You could potentially make this more storage and transfer efficient by generating a patch between files using bsdiff.


This is an interesting discussion and I have done some research on this topic a while back.

I came to the conclusion that while snapshots, combined with the send/receive, functionality in ZFS allows to efficiently backup datasets (i.e. filesystems) to local or remote storage, it requires ZFS on both ends in order to take full advantage of ZFS’ features. Part of that effectiveness stems from the fact that ZFS knows exactly which data is on either end and thus allows to consolidate the snapshots on the target/remote storage. Tools like ZnapZend [1], Sanoid/Syncoid [2], pyznap [3] or zrepl [4] take advantage of this, but require both the local and remote to be an actual ZFS filesystem and thus allow to consolidate snapshots and reclaim unused storage space.

On the other hand, tools like z3 [5] or ZFSBackup [6] essentially pipe the data stream from a zfs send command through other utilities and level it off to some kind of a ‘passiv’, remote data storage (i.e. a non-ZFS filesystem), with all the disadvantages already discussed here. To reclaim storage space on the remote only occupied by snapshots that are no longer needed, the utility would need to keep track of the individual records (i.e. blocks) in the data structure send to the remote storage, which is unfeasible since this is essentially what ZFS is doing in the first place.

The currently best option for ZFS dataset backups in the cloud is the offer by rsync.net [7], who allow for special zfs send capable accounts to access their underlying ZFS filesystem directly and take full advantage of the ZFS send/receive capabilities. For a great review of this service see Jim Salter’s article on arstechnica [8].

Personally, I rely on local ZFS backups to my NAS/external HDD using zrepl [4] and use restic for remote backups as it is specifically designed for this use case.

概括一下帖子中提到的问题:

  • ZFS send 发送的流只会在 recv 时得到验证, 这个过程是一个 online 的设计, 把 send 流暂存起来不能保证数据没有损坏, 一旦 send 流有任何损坏, recv 时会完全无法接收, 丢失所有数据
  • 多次增量 send 会导致占用空间越来越大, 想要删除旧快照, 必须全部重新全量发送 (远端并不能理解 ZFS 的结构, 因此不能帮我们完成删除一个快照的操作)
  • ZFS 本身不能很好的处理压缩和 dedup 的场景

相比之下 zfs send 备份的优点:

  • 可以使用 --raw send 在没有加密密码的情况下快照以及全量/增量备份数据, 并在备份后保持加密的状态
  • 很小的额外 CPU 开销, 很高的连续读取性能, 能高速备份大量小文件

最终, 我还是稳妥起见, 暂时回归了久经考验的传统的备份方案, 之后如果能有更成熟的 ZFS 备份方案我再考虑.

但我没有继续使用一直不太稳定的 Duplicati 和 Duplicacy, 转而使用了 restic. 之前我也考察过 restic, 不过因为它没有 WebUI 没有选用它, 现在测试下来当时的决定似乎是错误的, restic 的稳定性和性能远超前两者.

我做了个简单的测试, 使用 --dry-run 运行 restic backup, 备份 510w 个总计 2.2TiB 的数据集时, 大约需要 3 个多小时用于读取, 压缩和去重, 还算勉强可以接受, 不过全程进度展示很流畅, 不像原来的两者可能出现卡死不动的情况, 可能也和我现在升级了全闪存 NAS 有关.

restic 备份中...restic 备份中...

restic 也有参数用于调整块大小, 对于文件数有上限的云存储还是很有用的. 我自己目前是备份到我自己购买的 OneDrive 365 家庭版账号中, 我用 rclone union 合并了几个账号的 1TB 存储空间, 能稳定跑满我宽带 120M 的上传. (虽然实际备份过程中我手动限速到了 60M)


最终在我首次备份过程中, 备份了 5170993 个共 2.237 TiB 文件, 去重后 1.834 TiB, 压缩后实际存储上云的是 1.627 TiB, 花费 66 小时 37 分钟, 算一下平均上传速度大概在 7MiB/s 多, 基本达到了我设置的限速.

1
2
3
4
5
6
7
8
Files:       5170993 new,     0 changed,     0 unmodified
Dirs: 379805 new, 0 changed, 0 unmodified
Data Blobs: 5888669 new
Tree Blobs: 309126 new
Added to the repository: 1.834 TiB (1.627 TiB stored)

processed 5170993 files, 2.237 TiB in 66:37:09
snapshot ca3bccd5 saved

最终使用 rclone 检查远端存储情况, 这些数据存储为了 13627 个文件.

1
2
3
# rclone size o365:restic202407
Total objects: 13.627k (13627)
Total size: 1.627 TiB (1789074427475 Byte)

在国内机器测试恢复的时候发现下载反而比上传慢, 合理怀疑是 restic 下载没做并行优化+国内网络问题. 在海外 4C16G 机器执行 restic check --read-data 结果如下, 耗时约 3 小时 40 分钟. (会读取所有已经存储的数据块并校验 Hash)

1
2
3
4
5
6
7
8
9
10
11
12
13
# restic check --read-data
using temporary cache in /tmp/restic-check-cache-3826684933
create exclusive lock for repository
load indexes
check all packs
check snapshots, trees and blobs
[0:39] 100.00% 1 / 1 snapshots
read all data
[1:00] 0.42% 55 / 13228 packs
[2:00] 0.88% 116 / 13228 packs
............
[3:41:14] 100.00% 13228 / 13228 packs
no errors were found

校验也没有问题, 后续我尝试了一次增量备份, 速度也非常快, 在如此大的 base 上进行增量只花费了 17 分钟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
repository 30907562 opened (version 2, compression level auto)
lock repository
using parent snapshot ca3bccd5
load index files
[0:08] 100.00% 396 / 396 index files loaded
start scan on [...]
start backup on [...]
scan finished in 113.786s: 5300878 files, 2.240 TiB

Files: 130863 new, 2773 changed, 5167308 unmodified
Dirs: 157 new, 777 changed, 379027 unmodified
Data Blobs: 133178 new
Tree Blobs: 933 new
Added to the repository: 7.919 GiB (6.171 GiB stored)

processed 5300944 files, 2.241 TiB in 16:58
snapshot 8fa17113 saved

小结

对于备份方案, 肯定还是期待它能稳定可靠. 希望目前的 setup 能多支撑一会儿, 不要再有下一篇讲我怎么更新备份的文章了.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/17-zfs-repl-and-backup/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬