append-only 仓库里 keep-weekly 会删掉合法快照:restic forget 的保留策略与 --keep-within
restic 0.19.1。删除备份不是一条命令的事:forget 把快照从列表里摘掉,prune 回收只被这些快照引用的数据。两步分开,各有各的坑。
项目地址:restic.net / github.com/restic/restic
两阶段:forget 摘快照,prune 回收数据
手动删一个快照:
restic -r /srv/restic-repo snapshots
restic -r /srv/restic-repo forget bdbd3439
此时快照引用的数据仍留在仓库里。要清掉无引用数据,必须再跑 prune:
restic -r /srv/restic-repo prune
输出会依次经历:加载所有快照、加载索引、查找仍在使用中的数据、搜索使用中的 pack、收集待删除与待重打包的 pack,随后打印 to repack / to delete / total prune / remaining / unused size after prune,再 repacking packs、rebuilding index、删除过时索引文件、removing 3 old packs。以上为文档示例输出,未实测。
forget 的 --prune 选项把第二步自动化,只在确实删掉快照时才触发 prune:
restic forget --keep-last 1 --prune
prune 耗时取决于快照数量和数据量。执行期间仓库被加锁,备份无法完成,要留出能跑完的时间窗口,别和常规备份撞车。prune 之后建议跑 restic check,仓库内部数据结构一旦损坏能及时告警。
策略选项
--keep-last n:保留最近 n 个快照。--keep-hourly n:最近 n 个有快照的小时里,每小时只留最新一个。--keep-daily n、--keep-weekly n、--keep-monthly n、--keep-yearly n:同样的规则,按天/周/月/年。--keep-tag:保留至少匹配一个 tag 列表的快照。每写一次定义一份列表,列表内逗号分隔的标签是 AND;多个--keep-tag之间是 OR。如果某个分组内所有快照都因不满足 tag 策略而被删,forget 会以错误退出。--keep-within duration:保留时间戳落在「最新快照往前这段时间」内的所有快照,duration 形如2y5m7d3h。--keep-within-hourly/-daily/-weekly/-monthly/-yearly:同样的思路,按小时/天/周/月/年。
日历相关选项走自然时间边界,不相对 forget 运行时刻计算:周为周一 00:00 到周日 23:59,天为 00:00 到 23:59,小时为 :00 到 :59。只统计有快照的小时/天/周。值 unlimited 解释为永远。
duration 相关选项会忽略时间戳在未来的快照(相对于 forget 运行时刻),这些快照不会被删除。
如果快照数量不足以让每个 duration 选项都保留一个,最旧的快照会额外保留,并在输出中标为 oldest。
--keep-tag '' 只匹配没有标签的快照。
分组语义
带策略运行 forget 时,restic 先加载全部快照,按主机名和路径分组。分组方式用 --group-by 指定,例如 --group-by paths,tags。策略对每个分组单独生效——这是防止误删不相关备份集的安全特性。用 --group-by ''(空值)关闭分组。backup 命令通常设成同一个值,保持口径一致。
用 --host 或 --tag 收窄策略范围。多个 --tag 之间是 OR,逗号是 AND:
restic forget --tag foo --keep-last 1
restic forget --tag foo --tag bar --keep-last 1
restic forget --tag foo,bar --keep-last 1
restic forget --tag '' --keep-last 1
两个策略例子
每天备份、连续 100 年:
restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 75
保留最近 7 个 daily,外加 4 个周最后一天(7 个 daily 里已包含 1 个 weekly)、12 或 11 个月最后一天、75 或 74 年最后一天,其余全部删除。
备份时间不规则时改用 within 系列:
restic forget --keep-within-daily 7d --keep-within-weekly 1m --keep-within-monthly 1y --keep-within-yearly 75y
注意 1w 不是被识别的 duration,要写 7d。
所有 --keep-* 选项一起参与评估,快照只要匹配其中任意一个就保留,结果是 OR。
举个 11 个快照跨两周、缺最后一个周三的例子:
restic forget --keep-daily 5 --dry-run
只保留最近五个「有快照的日子」每天一个。因为周三备份缺失,前一个周五的快照会被保留。换成 --keep-within-daily 7d,则是最近七天内每天最多留一个,第一周的备份一个都不剩。
删除全部快照
restic 拒绝空策略。--keep-last 0 一个快照都不删。确实要全删,先用 --keep-last 1,再按 ID 手动删最后一个。从 0.17.0 起,--unsafe-allow-remove-all 可以删掉特定 host、tag 或 path 下的所有快照,但必须配合过滤器:
restic forget --tag example --unsafe-allow-remove-all
append-only 模式下的投毒场景
TL;DR:在 append-only 仓库里删快照,专门用 --keep-within。
append-only 模式下仓库只可写、可读,删除和覆盖被拒绝。rest-server 支持 append-only,其他后端可以在中间放一层 rclone 实现。forget 和 prune 需要完整的读、写、删权限,攻击者一旦拿到这些权限,append-only 保护就失效了。推荐给 forget/prune/维护单独准备一台加固过的客户端。
即便如此,能往仓库里塞垃圾快照的攻击者,仍可以把快照列表推到某个状态,让不知情的管理员用某些 --keep-* 选项跑 forget 时把合法快照删光。例子:策略保留三个 weekly 快照;攻击者给最近三周各加一个空快照,--time 设得比现有快照稍新、但仍落在目标周内;下一次 forget 每周只留最新那个,合法快照被删。不跑 prune 的话恢复很麻烦,还会丢一部分元数据。
用 --keep-within 可以避开这个局面:它同时保留攻击者的快照和合法快照;在系统时间正确的前提下,管理员能注意到快照数比平时多,或时间戳可疑。
这个防护受 duration 范围限制:如果 forget --keep-within 7d 在最后一次好快照之后 8 天才运行,攻击者仍能利用这个窗口删掉全部合法快照。
prune 的重打包与 --max-unused
prune 扫描所有快照和目录,判断哪些数据仍在使用。对每个文件分成三种:完全使用、部分使用、完全未使用。完全未使用的标记删除;完全使用的保留;部分使用的按选项保留或标记重打包。重打包要把文件从仓库存储下载、再把需要的数据重新上传,远程仓库上非常耗时。之后才执行重打包、修改索引、删除过时文件。
--max-unused:允许的未使用数据上限。可写绝对大小(200M)、相对仓库总大小的百分比(10%)或unlimited(部分使用的文件不设限,元数据仍会重打包)。默认 5%。restic 会尽量少重打包,同时保证不超过这个上限。--max-repack-size:限制待重打包文件的总大小。prune 先把所有重打包文件写好、最后才删过时文件,所以预计要重打包很多、又担心空间不够时有用。--repack-cacheable-only:只重打包包含元数据且存于缓存的文件;速度快,但未使用数据可能超过--max-unused。默认 false。--repack-smaller-than:重打包所有小于该大小的 packfile,从更小的--pack-size压缩。--dry-run、--verbose。
磁盘满时的救援
仓库膨胀到填满磁盘或配额时,prune 可能腾不出空间。prune 的设计是任何中断都让仓库保持可用,代价是需要临时空间。多数情况下:
restic prune --max-repack-size 0
0.13.0 之前的版本用 --max-repack-size 1。这条路径的前提是此前已经用 forget 删掉若干快照,否则没有可回收的数据。