坑边闲话:ZFS 快照任务是非常关键的,依赖手动快照在大多数情况下无法避免意外事件,借助自动化的快照机制,可以有效提高抗风险能力。本文基于 Debian bookworm 系统介绍开源快照管理软件 Sanoid 的使用方法。

1. 快照的自动管理·

此前笔者的快照也是自动化编排的,只是行为比较简单。笔者编写了一个自动执行快照的 Systemd Timer,然后定期执行某个脚本:

  • 在脚本里定义了执行快照的逻辑;
  • 在 Systemd Timer 和 Service 定义执行时间和执行路径。

然而,自己重复造轮子的代价就是会翻车,然后重复解决前人已经解决的问题。因此推荐使用开发多年的工具 Sanoid. 该工具原理与笔者此前的开发工作类似,只是它以 TOML 配置文件对快照任务进行了抽象,而且增加了对快照生命周期的支持,能够自动销毁过期的快照。

2. 安装 Sanoid·

Sanoid 的主要开发语言是 Perl,因此不需要经过复杂的编译即可使用。目前的 Sanoid 没有集成到 ZFS 工具链上游,也没有集成到 Debian 发行版的软件包仓库,用户需要安装 Debian 打包工具,创建自己的 .deb 安装文件。执行下列代码即可创建 .deb 文件并本地化安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 下载编译打包工具
sudo apt update
sudo apt install debhelper libcapture-tiny-perl libconfig-inifiles-perl pv lzop mbuffer build-essential git

# 下载代码库
git clone https://github.com/jimsalterjrs/sanoid.git
cd sanoid

# 切换到 stable tag
git checkout $(git tag | grep "^v" | tail -n 1)

sudo bash -c '
ln -s packages/debian .
dpkg-buildpackage -uc -us
apt install ../sanoid_*_all.deb
'

3. 配置 Sanoid 任务·

1
2
3
4
5
6
[zroot]
recursive=zfs # 递归创建快照
hourly=48 # 保留最近两天的小时级快照
daily=14 # 保留最近两周的日级快照
autosnap=yes # 自动快照
autoprune=yes # 过期快照自动销毁

图 1. Sanoid 任务生效,自动生成快照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
[zroot]
recursive=zfs
hourly=48
daily=14
autosnap=yes
autoprune=yes

#############################
# templates below this line #
#############################

# name your templates template_templatename. you can create your own, and use them in your module definitions above.


[template_production]
frequently = 0
hourly = 36
daily = 30
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes

[template_backup]
autoprune = yes
frequently = 0
hourly = 30
daily = 90
monthly = 12
yearly = 0

### don't take new snapshots - snapshots on backup
### datasets are replicated in from source, not
### generated locally
autosnap = no

### monitor hourlies and dailies, but don't warn or
### crit until they're over 48h old, since replication
### is typically daily only
hourly_warn = 2880
hourly_crit = 3600
daily_warn = 48
daily_crit = 60

[template_hotspare]
autoprune = yes
frequently = 0
hourly = 30
daily = 90
monthly = 3
yearly = 0

### don't take new snapshots - snapshots on backup
### datasets are replicated in from source, not
### generated locally
autosnap = no

### monitor hourlies and dailies, but don't warn or
### crit until they're over 4h old, since replication
### is typically hourly only
hourly_warn = 4h
hourly_crit = 6h
daily_warn = 2d
daily_crit = 4d

[template_scripts]
### information about the snapshot will be supplied as environment variables,
### see the README.md file for details about what is passed when.
### run script before snapshot
pre_snapshot_script = /path/to/script.sh
### run script after snapshot
post_snapshot_script = /path/to/script.sh
### run script before pruning snapshot
pre_pruning_script = /path/to/script.sh
### run script after pruning snapshot
pruning_script = /path/to/script.sh
### don't take an inconsistent snapshot (skip if pre script fails)
#no_inconsistent_snapshot = yes
### run post_snapshot_script when pre_snapshot_script is failing
#force_post_snapshot_script = yes
### limit allowed execution time of scripts before continuing (<= 0: infinite)
script_timeout = 5

[template_ignore]
autoprune = no
autosnap = no
monitor = no
1
sudo systemctl enable --now sanoid.timer

3.1 时区问题·

在 Debian 上,查看文件 /usr/lib/systemd/system/sanoid.service,可以发现 Sanoid 使用的时区是 UTC.

1
2
3
4
5
6
7
8
9
10
11
12
13

[Unit]
Description=Snapshot ZFS Pool
Requires=zfs.target
After=zfs.target
Wants=sanoid-prune.service
Before=sanoid-prune.service
ConditionFileNotEmpty=/etc/sanoid/sanoid.conf

[Service]
Environment=TZ=UTC
Type=oneshot
ExecStart=/usr/sbin/sanoid --take-snapshots --verbose

什么是 UTC 时间

UTC(协调世界时,Coordinated Universal Time)是全球通用的时间标准,用于统一协调世界各地的时间。它以原子钟的精确时间为基础,并与地球自转的天文时间(UT1)保持协调,通过添加闰秒进行微调。UTC不随时区变化,是国际时间基准,广泛应用于通信、导航、计算机网络和科学研究中。

简而言之,使用了 UTC 之后,Sanoid 生成的快照名字中所含的时间戳不再具有本地意义。英国等国家存在冬令时,即每年的 10 月的最后一个星期日,凌晨 2:00 的时钟将会被回拨到 1:00,也即 1:00 之后的下一个小时依然是 1:00,这会导致 Sanoid 出现一定的异常。使用 UTC 时间之后,不会再有类似的问题。当然,缺点就是没办法通过快照名字与本地时间做对应,好在这个很好克服。

4. 与 TrueNAS 的 Zettarepl 模块集成·

Zettarepl 是 TrueNAS 开发的一套基于 Python 语言的中间件,其使命就是进行 ZFS 文件系统 Replication.

在中文语境里 Replication 很难找到匹配的词汇,直接翻译为「复制」则难以与 Copy 区分,毕竟 Copy 和 Replication 是大不相同的。Replication 通常带有以下含义:

  • 忠实重现:强调对原始对象、数据或现象的精准复现。
  • 分布和同步:在数据库、存储系统等领域,指的是数据在多个节点之间的同步。
  • 验证或验证性实验:在科学研究中,指重复实验以验证原始结果的可靠性。

Copy 则更偏向于直接的、单次的复制行为,没有强调“忠实”或“同步”的含义。因此本文直接用英文词汇,不做翻译。

4.1 错误示范与问题解决·

此前笔者使用基于名字的快照匹配策略进行待发送快照的选取,过滤模式如下所示。然而,Zettarepl 在这种模式下会遇到问题。

1
2
3
4
# 基于 date 命令的匹配字符串
autosnap_%Y-%m-%d_%H:%M:%S_hourly
autosnap_%Y-%m-%d_%H:%M:%S_daily
autosnap_%Y-%m-%d_%H:%M:%S_monthly

Sanoid 会按照小时、日、月进行快照,由于 ZFS 快照很快,所以一秒钟之内即可完成。很多时候会生成如下命名的相邻快照:

1
2
zroot/home@autosnap_2025-01-04_00:00:04_daily
zroot/home@autosnap_2025-01-04_00:00:04_hourly

两者的字面时间完全一致,只有 monthly 和 hourly 差别。使用下面的命令检查创建时间也无法区分。

1
2
date -u -d @$(zfs get -p creation zroot/home@autosnap_2025-01-04_00:00:04_daily -o value -H)Matching regular expression
date -u -d @$(zfs get -p creation zroot/home@autosnap_2025-01-04_00:00:04_hourly -o value -H)

其实,每个快照都有创建日期和 GUID,快照创建顺序在 zfs list 的输出中有严格显示。问题在于 TrueNAS Zettarepl 故意忽略快照元数据。Zettarepl 是一个严格的基于名称的工具,它需要“可解析的”快照名字。iXsystems 这样做的原因大概是为了做性能优化,毕竟备份所需的时间越短越好。解析快照名称比读取和比较快照元数据要快得多!特别是当生产环境中有大量的快照时。

解决的方案很简单:将 Matching naming schema 改为 Matching regular expression 即可。比如使用正则表达式 autosnap_\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2}_(hourly|daily|monthly) 替代原先的三个名字匹配模式。

图 2. 使用正则表达式替换名字匹配。

4.2 Sanoid 递归配置·

在使用 Sanoid 进行递归创建时,建议设置为 recursive=zfs 而非 recursive=yes,前者性能更好而且不易出错。

4.3 备份目的地存储池的属性设置·

Zettarepl 需要先在 TrueNAS 上创建备份数据集。笔者推荐使用如下命令进行创建并设置属性:

1
2
DESTINATION_DATASET=DapuStor_R5100_RAID-Z1/machines/4950-debian
sudo zfs create -o compression=lz4 -o canmount=off -o readonly=on ${DESTINATION_DATASET}
  • compression=lz4 一般不用设置,毕竟 zfs receive 会按照源数据集的压缩模式进行本地存储。这里手动设置,是为了防止后期出现潜在问题。
  • canmount=off 在一般情况下也不用设置。然而,当待备份数据集和本地数据集的所属 pool 名字、数据集名字完全一致时,自动挂载会产生混淆,甚至引起系统异常。为了保证安全需要关闭自动挂载。然而,在关闭 canmount 之后,在 TrueNAS 界面点击相应数据集会提示找不到本地路径的警告,忽略即可。
  • readonly=on 很重要,它可以防止本地数据集被修改。开启 readonly 之后,对数据集的任何写操作均会被拒绝,但是不影响 zfs receive 快照接收.

4.4 备份的保留策略·

在 TrueNAS 中,建议将备份保留策略设置为与源数据集相同。

如果需要保留特殊备份,也可以手动管理。

总结·

本文详细介绍了 ZFS 数据备份中的一些细节,作为一个存储系统,能把数据安全存储起来是至关重要的,但与此同时,能把数据稳定高效安全地 Replicate 到其他系统、介质上也是关键的。