Borg 去重备份实战:块级去重原理、增量归档、SSH 远程备份与任意时间点文件恢复

为什么"每天全量备份"会把你的硬盘和带宽吃光

做站长的人迟早会经历这一刻:某天心血来潮去看备份目录,发现三块硬盘都被塞满了——"每日全量备份"积累了三十份几乎一模一样的网站数据和数据库导出,每份几个 G,一个月就是上百 G。更糟的是,备份任务把出口带宽占满,白天网站访问变慢,后台还能看到备份传输的 GB 数字一路飙升。

问题的根源是:传统备份把"备份"理解成"复制",而每天真正变化的文件可能只有几百 KB。Borg(borgbackup)就是为了解决这个问题而生的——它做的是去重(deduplication):相同的数据块只存一次,无论你备份多少次、多少个目录,重复内容都会被自动跳过。这篇文章我从原理讲到实操,带你搭一套"每天备份、占用极小、随时能恢复到任意一天"的站长级方案。

Borg 的核心原理:块级去重 + 加密 + 压缩

Borg 不是文件级备份,而是块级去重。它把文件切成一堆变长的数据块(chunk),对每个块算哈希。备份时只上传"没见过的块",已经存在过的块直接引用。这带来几个直接的好处:

  • 同一份日志文件每天追加几行,Borg 只传新增的块,旧的块复用
  • 整个目录树重命名 / 移动,Borg 不会重传,因为块内容没变
  • 多个网站共用的框架文件(比如多个 WordPress 的 wp-includes)只存一份
  • 数据库导出的 SQL 文件,相邻两天的内容 99% 相同,也只增量传

此外 Borg 还自带:

  • 加密:AES-256,支持密钥文件模式或纯密码模式,备份存到不可信的远端也安全
  • 压缩:lz4(快)、zstd(平衡,推荐)、zlib、lzma(高压缩)、auto
  • 完整性校验:SHA-256 / BLAKE2b,随时可 check 验证仓库没坏
  • 挂载:能把任意一次备份直接挂载成文件系统(FUSE),像浏览 U 盘一样恢复单个文件

安装:各发行版一行命令

# Debian / Ubuntu
apt install -y borgbackup

# RHEL / AlmaLinux / Rocky(需要 EPEL)
yum install -y epel-release
yum install -y borgbackup

# 验证版本(建议 1.2 以上)
borg --version

如果你要在备份脚本里自动化,记得备份端和恢复端的 Borg 版本尽量一致——数据库格式在 1.1 和 1.2 之间有过不兼容,具体见官方 changelog。

初始化仓库与加密模式选择

Borg 的存储单位叫"仓库"(repository)。你可以存在本地盘,也可以存到远端(通过 SSH)。先看加密模式的三个选项,这一步一旦选错就改不了:

  • repokey:密钥存在仓库里,用密码解锁。方便,但仓库泄露 = 密码强才有意义。个人站长常用。
  • keyfile:密钥单独存在本地 ~/.config/borg/keys/,仓库里没有。更安全,但你必须把密钥文件单独备份,丢了密钥 = 数据永远打不开。
  • none:不加密。只在你备份到完全私有、物理可控的本地盘时才考虑。

我推荐 repokey + 强密码,并且在初始化后立刻把密钥导出备份:

# 设置环境变量(避免密码出现在命令行历史里)
export BORG_REPO=/backup/borg-www
export BORG_PASSPHRASE='一个足够长且复杂的口令'

# 初始化仓库,加密模式 repokey-blake2(哈希算法 blake2,比 sha256 快)
borg init --encryption=repokey-blake2 "$BORG_REPO"

# 导出密钥,离线保存(这一步千万别省!)
borg key export "$BORG_REPO" /root/borg-key-backup-$(date +%F).txt
# 然后把导出的文件拷到 U 盘 / 密码管理器 / 另一台机器

第一次备份:把网站目录和数据库一网打尽

假设你的网站数据在 /var/www/html,数据库每天导出到 /backup/sql。一个完整的备份脚本长这样:

#!/bin/bash
set -Eeuo pipefail

export BORG_REPO=/backup/borg-www
export BORG_PASSPHRASE='你的口令'

# 备份归档命名带上日期和主机名
ARCHIVE="www-$(date +%Y-%m-%d_%H%M)"

# 1. 先把数据库导出到临时目录(mysqldump 本身不做增量,但 Borg 会去重)
mkdir -p /backup/sql
mysqldump --single-transaction --quick --databases wordpress \
  | gzip > /backup/sql/wordpress-$(date +%F).sql.gz

# 2. 用 Borg 备份网站目录 + SQL 目录
borg create \
  --stats \
  --compression zstd,6 \
  --exclude '/var/www/html/wp-content/cache' \
  --exclude '*.log' \
  --exclude '*/node_modules' \
  "$BORG_REPO::$ARCHIVE" \
  /var/www/html \
  /backup/sql

# 3. 保留策略:7 天日备 + 4 周周备 + 6 月月备,其余删除
borg prune --list --stats \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  "$BORG_REPO"

# 4. 压缩碎片(可选,定期做,见下文)
# borg compact "$BORG_REPO"

几个关键参数解释:

  • --stats 让 borg 输出本次备份的原始大小、去重后大小、压缩比——这是最能体现 Borg 价值的数字。
  • --exclude 排掉缓存、日志、node_modules 这类"每天变但没备份价值"的目录,能显著减少首次备份体积。
  • --compression zstd,6:zstd 级别 6 是速度与压缩率的甜点,比默认快很多而压缩率损失很小。
  • prune 的保留策略把归档数控制在合理范围,防止无限增长。

把脚本存为 /usr/local/bin/borg-backup.sh,加执行权限,然后丢进 crontab 每天凌晨跑:

chmod +x /usr/local/bin/borg-backup.sh
# crontab -e 加入:每天 3:15 执行
15 3 * * * /usr/local/bin/borg-backup.sh >> /var/log/borg-backup.log 2>&1

恢复:从"昨天"到"上个月任意一天"

备份不能恢复等于没备份。Borg 的恢复有三种方式,覆盖不同场景。

场景一:整个目录回滚到某个归档

export BORG_REPO=/backup/borg-www
export BORG_PASSPHRASE='你的口令'

# 列出所有归档
borg list "$BORG_REPO"

# 列出某个归档里有哪些文件
borg list "$BORG_REPO::www-2026-10-01_0315"

# 提取到指定目录(恢复到 /tmp/restore 而不是原地覆盖,先检查)
borg extract --list "$BORG_REPO::www-2026-10-01_0315" \
  --target /tmp/restore

场景二:只恢复一个文件。用 --extract 路径过滤,直接拿到单个文件:

# 恢复归档里的 wp-config.php 到当前目录
cd /tmp/restore
borg extract "$BORG_REPO::www-2026-10-01_0315" var/www/html/wp-config.php

场景三:挂载后像浏览文件一样找。这是我最喜欢的功能,找不到文件在哪时特别有用:

mkdir -p /mnt/borg
borg mount "$BORG_REPO::www-2026-10-01_0315" /mnt/borg
ls /mnt/borg/var/www/html/
cp /mnt/borg/var/www/html/某个文件 .
umount /mnt/borg

borg mount 依赖 FUSE,装好 fuse3 即可。挂载后整个归档就是一棵只读目录树,用 find、grep 随便找,比在命令行里翻 list 高效得多。

远程备份:备份到另一台机器(3-2-1 原则)

备份的黄金法则是 3-2-1:三份数据、两种介质、一份异地。只把备份放在同一台机器上,机器炸了或者被勒索软件加密了,备份一起完蛋。Borg 通过 SSH 直接支持远端仓库,非常干净:

# 在备份服务器(192.168.1.50)上初始化远端仓库
export BORG_REPO=ssh://backup@192.168.1.50/backup/borg-www
export BORG_PASSPHRASE='你的口令'
borg init --encryption=repokey-blake2 "$BORG_REPO"

# 备份时和本地完全一样,Borg 走 SSH 自动传输增量
borg create --stats "$BORG_REPO::www-$(date +%F)" /var/www/html /backup/sql

为了不让 SSH 每次问密码,建议在备份机上生成专用 SSH key,并限制这把 key 只能执行 borg serve,防止 key 泄露后被拿去 shell 登录:

# 备份服务器上的 ~/.ssh/authorized_keys 前面加限制
command="borg serve --restrict-to-path /backup",restrict ssh-ed25519 AAAA... backup@web

这样即使攻击者拿到了备份机的 SSH key,也只能对 /backup 目录做 borg 操作,无法获得 shell。

完整性校验:确保备份真的能用

备份最怕"看起来在、其实是坏的"。定期做健康检查,可以在灾难真正来临时不慌:

# 快速检查(只校验仓库结构,几分钟)
borg check --repository-only "$BORG_REPO"

# 深度检查(校验所有归档和数据块,慢,建议每周或每月)
borg check --verify-data "$BORG_REPO"

把这两种检查也放进 cron:快速检查每天跑,深度检查每周跑一次。更好的做法是做"恢复演练"——每月挑一个归档真的 extract 到临时目录,看看文件能否正常打开、数据库 dump 能否导入。备份没演练过,就不算有备份。

日常维护:compact 与空间回收

Borg 1.2 之后,删除归档(比如 prune 删掉的旧归档)不会立即释放磁盘空间,存放在仓库里的块要显式 compact 才回收。建议每周在 prune 之后执行一次:

borg compact "$BORG_REPO"
du -sh /backup/borg-www   # 对比 compact 前后大小

同时监控仓库大小和归档数量,避免它悄悄膨胀:

# 查看仓库信息(归档数、去重后大小、唯一块数)
borg info "$BORG_REPO"

# 列出归档及其占用
borg list --format '{archive} {size} {numfiles}' "$BORG_REPO"

如果你只想验证"去重到底省了多少",看 borg create --stats 的这几行就够了——通常首次全量 5GB 的网站,之后每天增量只有几十到几百 MB,稳定运行一周后每天可能只剩几 MB。

⚠️ 几个必须避开的坑

  • 密钥/口令丢了就打不开:repokey 模式下,忘了口令、或者丢了 keyfile 模式下的密钥文件,数据就永久锁死了。把口令写进密码管理器,把密钥导出到离线介质。这是 Borg 用户最惨痛的教训。
  • 不要用 --exclude 排掉正在备份的数据库文件:MySQL 的 .ibd 文件在运行时是撕裂的,直接备份会得到坏数据。正确做法是 mysqldump 导出逻辑备份,或者用 xtrabackup 做物理备份,再让 Borg 备份这些产物。
  • 远端备份机的 ssh key 一定要用 restrict 限制,否则"备份"变成了"送一个 shell 给攻击者"。
  • prune 要和 create 分开、且有先后顺序:先 create 再 prune,否则可能把当天的归档一起删掉。策略里的 keep 数量留足冗余(别只留 1 天)。
  • 首次备份耗时长:全量去重阶段要读一遍所有文件、算哈希、压缩上传,大站点第一次可能跑几小时。建议单独跑一次、观察 --stats,再放进凌晨 cron。
  • 恢复前先恢复到临时目录,确认无误再覆盖生产。borg extract 默认覆盖同名文件且不删除多余文件,直接原地恢复可能留下"新旧混合"的诡异状态。

小结

Borg 是个人站长手里性价比最高的备份工具之一:它用块级去重把"每天全量"的成本压到几乎可以忽略,用加密保证远端存储也安全,用 mount 和 extract 让恢复变得像操作普通文件。一套合理的方案是:本地仓库 + 远端仓库各一份、每天 create + prune、每周 compact 和 verify、每月做一次真实恢复演练。搭好之后,你每天要做的只是看一眼备份日志,然后就可以安心睡觉了——因为你知道,无论哪台机器明天出事,昨天的一切都还在。

Last modification:October 2nd, 2026 at 12:24 pm

Leave a Comment