restic 加密增量备份实战:内容寻址去重、异地仓库与恢复演练完整流程

为什么 rsync 之外你还需要 restic

用 rsync 加硬链接做快照,是个人站长最熟悉的备份方式,也确实好用:增量快、占用小、恢复就是复制文件。但它有三个绕不过去的短板。

第一,没有加密。rsync 到异地的那份数据是明文的。如果对方是对象存储、是廉价的主机商、是朋友闲置的服务器,你就得完全信任对方。第二,校验不足。rsync 校验的是传输过程中的一致性,它不保证一年后再去读的时候数据还完好。磁盘静默损坏(bit rot)它发现不了。第三,去重是按文件粒度的。一个 500MB 的数据库文件,明天只改了一个字节,rsync 也要整个再传一遍。

restic 正好补上这三块:它把数据切成变长的块做内容寻址去重(改一个字节只上传变化的块)、每个块都用加密哈希命名、整个仓库用 AES-256 加密、每次备份写一个快照并做完整性校验。和它同类的还有 borg(本地优先)和 kopia。这篇文章讲 restic,因为它纯 Go 写的、单个二进制无依赖、对 S3 兼容的对象存储支持最好,最适合个人站做"本地 + 异地"双层备份。

核心概念:仓库、快照、去重

理解三个词就够了。

仓库(repository)是备份数据存放的地方,可以是一个本地目录、一块外挂盘、一个 SFTP 路径,或者 S3/Backblaze B2/兼容对象存储。仓库本身是一堆加密后的 pack 文件,你无法直接进去翻文件,必须通过 restic 命令访问。

快照(snapshot)是一次备份的完整记录。每个快照有一个 ID,记录了"这次备份包含了哪些文件、它们的内容块在哪"。快照之间共享未变化的数据块,所以十个快照不会占用十倍空间。

去重是内容寻址的:文件被切成平均 1MB 左右的变长块,每块的 ID 是它内容的 SHA-256 前若干位。同样的内容只存一份,无论它在多少文件、多少次快照里出现。这意味着如果你的备份里有很多相似的大文件(比如多次导出、多个版本的镜像),实际占用会远小于文件总大小。

一个关键提醒:仓库密码丢了,数据就永远拿不回来,没有任何后门。restic 的加密密钥完全由你设的密码派生。所以密码必须存进密码管理器,并且单独记一份离线记录。这是用 restic 唯一的、不可协商的铁律。

初始化与第一次备份

先装:

# Debian/Ubuntu
apt install -y restic

# 或者用单二进制(不依赖系统包管理,更新更自由)
curl -L -o /tmp/restic.bz2 https://github.com/restic/restic/releases/latest/download/restic_linux_amd64.bz2
bunzip2 /tmp/restic.bz2 && install -m 755 /tmp/restic /usr/local/bin/restic
restic version

接着定义一个密码文件,后续所有命令都用它,避免密码出现在命令行和进程列表里:

# 权限一定要收紧,600 只有 root 能读
umask 077
echo '一个足够长的随机口令,建议 20 位以上' > /root/.restic-pass
chmod 600 /root/.restic-pass

初始化本地仓库:

export RESTIC_PASSWORD_FILE=/root/.restic-pass
export RESTIC_REPOSITORY=/mnt/backup/restic-repo

restic init

第一次备份,把站点目录、Nginx 配置、数据库导出文件都纳进去:

restic backup \
  /var/www/blog \
  /etc/nginx \
  /etc/letsencrypt \
  /var/backups/db \
  --exclude='*/node_modules' \
  --exclude='*/cache' \
  --exclude='*.log' \
  --exclude-caches \
  --tag auto

--exclude-caches 会跳过带有 CACHEDIR.TAG 标记的目录,很多应用(Composer、部分 CDN 工具)会自动打这个标记,省得你一个个排除。--tag auto 给快照打标签,之后可以用 --tag auto 过滤,把自动备份和手动备份区分开。

第一次跑会慢,因为要读全部数据并建索引。之后就快了,因为只传变化的块。

把仓库放到异地:S3 兼容对象存储

本地盘坏了本地备份一起没,所以必须有一份异地。restic 对 S3 兼容协议支持最好,MinIO、Backblaze B2、Cloudflare R2、阿里云 OSS 都能用:

export RESTIC_REPOSITORY=s3:s3.amazonaws.com/my-backup-bucket/restic
export AWS_ACCESS_KEY_ID=你的key
export AWS_SECRET_ACCESS_KEY=你的secret
export RESTIC_PASSWORD_FILE=/root/.restic-pass

restic init
restic backup /var/www/blog --tag offsite

换成其他服务商只需改 RESTIC_REPOSITORY 的前缀和 endpoint,比如 MinIO 自建的是 s3:https://minio.example.com/bucket/restic。凭证最好放在 root 的 ~/.aws/credentials 或者用 restic 的环境变量文件,别写进 crontab 命令里——crontab 文件是可读的。

如果只有两台服务器,用 SFTP 后端更简单,不需要任何对象存储:

export RESTIC_REPOSITORY=sftp:backup@10.0.0.5:/srv/restic-repo

# 配合 SSH 密钥免密,restic 会直接复用 ~/.ssh/config
restic backup /var/www/blog --tag offsite

SFTP 模式的好处是数据在传输前就已经加密,对端只看到一个加密的仓库,看不到任何文件名或内容。对端甚至不需要装 restic,只要是个能写文件的 SSH 账号即可。

快照管理:保留策略与 forget --prune

备份做多了会堆积快照,需要一个保留策略。restic 的 forget 命令支持一套很直观的规则:

restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --keep-yearly 2 \
  --prune

这套规则的意思是:保留最近 7 天的每日快照、最近 4 周的每周快照、最近 6 个月的每月快照、最近 2 年的每年快照,其余删除。--prune 是关键——只加 forget 只是把快照记录删掉,实际的 pack 文件还占着磁盘,必须加 --prune 才会真正回收空间。

几点经验:不要每天都跑 --prune。prune 需要重写 pack 文件,对对象存储来说会产生大量 API 调用和流量,跑一次可能要几分钟到几十分钟。合理的节奏是每天 forget(快,只改元数据),每周跑一次带 --prune 的清理。--keep-* 规则是从"当前时间往前"匹配的,不是自然月/自然周,所以不必纠结日历边界。

另一个有用的命令是 restic snapshots,它会列出所有快照的 ID、时间、标签和涉及路径。用 --tag 过滤、用 --path 限定路径,出问题时能快速定位到"哪次备份包含了哪个文件"。

恢复:三种粒度,越熟越好

恢复能力才是备份的真正价值。restic 支持三种粒度:

整快照恢复——把某个快照的全部内容还原到指定目录:

restic restore latest --target /tmp/restore-test

单文件恢复——最常用的场景:"我不小心删了 config.php":

# 先看看快照里有什么
restic ls latest /var/www/blog

# 直接把文件 dump 到 stdout,重定向到目标位置
restic dump latest /var/www/blog/config.php > /var/www/blog/config.php

交互式挂载——把整个仓库挂成一个只读文件系统,像浏览目录一样翻历史版本:

mkdir -p /mnt/restic
restic mount /mnt/restic
# 挂载后结构是:/mnt/restic/snapshots/<时间戳>/<原路径>
# 浏览完卸载
fusermount -u /mnt/restic

挂载模式对"想找回三个月前某个版本的模板文件"这类需求极其方便,而且完全不占用额外磁盘——文件内容是按需从仓库里读出来的。注意挂载需要 FUSE 支持,容器环境里可能要额外加 --cap-add SYS_ADMIN 和设备映射,所以恢复演练最好在宿主机上做。

完整性校验:check 是备份的体检

restic 提供了两个层级的校验命令,务必定期跑:

# 快速校验:检查仓库结构、快照引用是否完整
restic check

# 深度校验:把每个数据块读出来重新计算哈希,验证与仓库记录一致
# 会读全量数据,慢,但对对象存储会消耗流量
restic check --read-data

# 只抽查一部分(比如 5%),在流量和可信度之间折中
restic check --read-data-subset=5%

restic check 只检查元数据和索引,很快,适合每周跑。--read-data 才是真正发现"静默损坏"的手段,但它要把整个仓库下载一遍——如果你备份了 50GB 到对象存储,这一趟就是 50GB 的出站流量。务实的做法是每月跑一次 --read-data-subset=10%,一年下来能把整个仓库抽查一遍以上,流量可控。

如果 check 报告某个块损坏,说明底层存储出了问题(对象存储的静默损坏、磁盘坏道)。这时候要立刻从另一份备份重建,而不是试图"修复"——restic 仓库本身没有冗余,坏块就是真丢了。这正是为什么要做"本地 + 异地"两份的原因:一份坏了好歹还有另一份。

放进 cron:一个可靠的自动备份脚本

把上面这些串成一个脚本,注意每个细节都有原因:

#!/bin/bash
set -euo pipefail

export RESTIC_PASSWORD_FILE=/root/.restic-pass
LOG=/var/log/restic-backup.log
exec >>"$LOG" 2>&1

echo "===== $(date '+%F %T') 开始备份 ====="

# 1. 先导出数据库,再备份(保证数据库文件是自洽的)
mysqldump --single-transaction --routines --triggers \
  blog > /var/backups/db/blog-$(date +%F).sql

# 2. 本地仓库备份
export RESTIC_REPOSITORY=/mnt/backup/restic-repo
restic backup /var/www/blog /etc/nginx /var/backups/db \
  --exclude='*.log' --exclude-caches --tag auto
restic forget --keep-daily 7 --keep-weekly 4 --tag auto

# 3. 异地仓库备份
export RESTIC_REPOSITORY=s3:s3.amazonaws.com/my-bucket/restic
restic backup /var/www/blog /etc/nginx /var/backups/db \
  --exclude='*.log' --exclude-caches --tag offsite
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --tag offsite

echo "===== $(date '+%F %T') 完成 ====="

几个关键点。set -euo pipefail 让脚本在任何一步失败时立刻退出——否则 mysqldump 失败了脚本还会照常备份,而备份进去的是半截文件,比没有备份更危险。exec >>"$LOG" 2>&1 把全部输出追加到日志,cron 环境下没有终端,不重定向的话出错了你只能去翻系统邮件。

每周的 prune 单独放一个 cron,别混在每日任务里:

# 每天 3:10 备份
10 3 * * * /usr/local/bin/restic-backup.sh

# 每周日 4:30 清理 + 完整性检查
30 4 * * 0 export RESTIC_REPOSITORY=/mnt/backup/restic-repo \
  && restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune \
  && restic check

注意 cron 环境里 PATH 很短,restic 要用绝对路径,RESTIC_PASSWORD_FILE 这类环境变量也要在脚本里显式 export,不能指望从 shell 配置文件继承。

恢复演练:不做这一步等于没备份

备份系统最讽刺的地方是:它平时都在"成功",直到你需要它的那天才发现恢复不了。所以必须定期做真恢复演练,而且要在另一台机器或者一个干净的临时目录里做,绝不能在线上原地覆盖。

# 1. 挑最新的快照恢复到临时目录
restic restore latest --target /tmp/restore-test

# 2. 校验关键文件是否完整
diff -r /var/www/blog/wp-config.php /tmp/restore-test/var/www/blog/wp-config.php
ls -la /tmp/restore-test/var/backups/db/   # 确认数据库导出也在

# 3. 更进一步:把数据库导入一个临时库,跑一次查询验证
mysql -e "CREATE DATABASE restore_test" 
mysql restore_test < /tmp/restore-test/var/backups/db/blog-2026-09-30.sql
mysql -e "SELECT COUNT(*) FROM restore_test.wp_posts"

# 4. 清理
rm -rf /tmp/restore-test
mysql -e "DROP DATABASE restore_test"

第 3 步是灵魂:能导入、能查询、行数对得上,才证明这份备份真的有内容。很多人演练只做到第 1 步"恢复了没报错"就结束了,但一个 truncate 成 0 字节的 mysqldump 文件也能"成功恢复"——它只是恢复了一个空文件。

把这个演练也写成一个每月跑一次的脚本,输出结果邮件或推送到通知渠道。演练失败就说明备份链路已经坏了,必须立刻查。

小结

restic 解决的是 rsync 解决不了的三件事:加密、块级去重、可验证的完整性。它对个人站的正确用法是"本地仓库 + 异地仓库"双写,本地保证恢复速度,异地保证容灾。forget --prune 负责控制体积,check --read-data-subset 负责发现静默损坏,每月一次的真恢复演练负责证明备份真的能用。最容易犯的错是"配好 cron 就不管了"——备份和恢复是两件事,只有演练过恢复的备份才叫备份。最后再说一遍那条铁律:仓库密码存进密码管理器并留一份离线记录,丢了就永远找不回来。

Last modification:September 30th, 2026 at 10:26 pm

Leave a Comment