所有系统运行正常 阿姆斯特丹 · 巴黎 · 雷克雅未克 +5 支付方式 加密货币
安全加固进阶阅读约 40 分钟更新于 2026-09-23

为 VPS 做异地加密备份

你的备份,是服务器的第二份完整副本,放在一个你当初匆忙选定的地方。这篇讲的是:怎样让这份副本在离开服务器之前就已加密,让你自己的服务器也删不掉它,并且——这是人人都会跳过的那一步——确认它真的能恢复出来。

为 VPS 做异地加密备份
本页目录
  1. 为什么备份往往是保护最差的那份副本
  2. 3-2-1 备份原则,以及后来加上的两位数字
  3. restic 还是 Borg:真正起决定作用的差别
  4. 仓库放在哪里,以及谁有权删除它
  5. 分步操作
  6. 保留策略:为一个你还没察觉的问题留足历史
  7. 监控,因为备份失败时悄无声息
  8. 真正让人丢掉数据的那些失败方式
  9. 备份做不到的事
  10. 常见问题

我们的文档里只用一句话讲这件事:备份由你自己负责。对任何给你完整 root 权限的主机商来说,这都是老实话,也是大多数人读到这里就不再往下读的地方。真正有意思的,是这句话没说完的部分,因为给服务器做备份,其实不是该跑哪条命令的问题,而是第二份副本放在哪里、谁能销毁它,以及你每晚兢兢业业产出的那个东西,到底能不能真的还原成一台能用的机器。

大多数备份方案都会在这三关中的某一关栽跟头,而且几乎总是栽在同样的三处:副本和原件放在同一家服务商、同一个账户下;能写入备份的凭据同样能删除它;以及从来没有人用它做过恢复。这份指南把这三处都补上。它面向的是一台跑着真实业务的 Linux VPS — 一个网站、一个数据库、一套 Nextcloud、一个节点 — 由 restic 或 BorgBackup 来干活,加密在你自己的机器上、在任何数据离开之前完成,而另一端,是一台同样归你所有、位于另一个国家的廉价服务器。

为什么备份往往是保护最差的那份副本

人们往往把难的部分做完,却把容易的部分晾在一边。服务器做过一遍加固,用密钥代替密码登录,防火墙默认拒绝,也许还上了全盘加密,关机之后磁盘上的内容毫无意义。然后每天晚上,这块磁盘上所有内容的一份完整明文副本,被推送到一个几年前用银行卡开的存储账户里,放在一个没人认真想过的国家,由一个同时也存在浏览器里的密码守着。

副本值得和原件同样的重视,因为它就是原件,只不过没在运行而已。值得把你究竟在防什么说具体一些,因为几种情况的答案各不相同,而人们脑子里想到的,通常只有其中一种:

  • 机器挂了。硬件、hypervisor,或者一个重启后变成只读的文件系统。少见,平淡,也是所有备份都能应对的情况。
  • 你自己把它搞坏了。一次跑了一半的迁移、一条通配符写得过于豪迈的 rm、一处直到重启前都好好的配置改动。这是人们做恢复最常见的原因,没有之一。
  • 别人拿到了 root。这是决定你整个设计的那种情况。想要筹码的攻击者会先删备份,再加密服务器;如果你的服务器上存着一个能抹掉备份仓库的凭据,那你搭建的就是一套帮他们干活的系统。
  • 账户没了。一笔没到账的付款、一场争议、一次封停。如果原件和副本都在同一个登录之后,它们就共享同一个单点故障,而且这个故障跟技术无关。
  • 有权力的人找上门来。硬件可能被收走,账户可能被冻结,靠的是程序而不是攻击。这时唯一管用的,是一份放在另一个司法辖区的副本,而且前提是你还能访问到它。

留意一下:由你自己的服务商、在你自己的账户下、在同一个数据中心里拍的快照,对上面这些情况里的多少种根本无能为力。

3-2-1 备份原则,以及后来加上的两位数字

这条原则来自摄影,而不是系统管理 — Peter Krogh 当初是为手里握着无法替代的底片的人写下它的 — 它能流传下来,是因为够短:数据保留三份,放在两种不同的系统上,其中一份在异地。每一位数字防的都是上面清单里不同的一项,这让它不只是一句口号。

三份应对的是普通的数据丢失:线上数据加两份备份,这样发现某份备份坏了,只是件烦心事,而不是世界末日。两种不同的系统应对的是关联故障 — 同一台主机、同一个阵列或同一个账户里的两份副本,不过是一份副本换了身伪装。而异地那份,应对的是一切发生在某个地点、而非某块磁盘上的事:火灾、查扣、封停,还有一个再也联系不上的服务商。

它的现代扩展版是 3-2-1-1-0,而在 2026 年真正要紧的,恰恰是多出来的这两位。多出来的那个 1,是一份不可篡改或离线的副本 — 一个拿着你凭据的攻击者也删不掉的东西,而这正是仅追加模式给你的。0 指的是校验零错误:一份你检查过的备份,而不只是一份你做过的备份。

放到一台小型 VPS 上,这条原则落地起来比听上去朴素得多。线上服务器是第一份。另一个国家第二台机器上的仓库是第二份:异地、不同系统,而且因为是仅追加,它同时也是那份不可篡改的副本。每月一次恢复到临时服务器上的完整恢复,就是那个 0。整条原则就这么满足了,代价只是多一台小服务器,而这份指南余下的部分,讲的就是具体怎么做。

restic 还是 Borg:真正起决定作用的差别

这两个工具做的是同样的四件事,而且都做得很好:按内容给数据分块,所以移动或重命名过的文件不会被重新上传;跨所有快照去重,所以一年的每日备份,占用的空间远比一年的完整拷贝少;压缩;以及 — 这里最要紧的一点 — 在客户端完成加密和认证,赶在任何一个字节进入网络之前。两种情况下,存储端保存的都是它读不懂、也无法在不被察觉的前提下篡改的密文。正因为有这个特性,这份指南对“谁来运营你的存储”没有立场。

真正的差别,都在另一端。

  • restic 是一个静态编译的 Go 单文件程序,对目标端没有任何要求。它能写入 SFTP、S3 兼容的对象存储、它自己的 REST 服务器,或者任何 rclone 能访问到的地方。加密用的是 AES-256 配合 Poly1305 认证器;仓库格式 2 加入了压缩。如果你的目标是一个存储桶,或者一台你没法装软件的机器,答案就是 restic,没什么好争的。
  • Borg 要求两端都装上 borg,通过 SSH 和自己对话。作为交换,它提供了 restic 需要借助辅助程序才能做到的东西:一个真正由服务器强制执行的仅追加模式,通过远端账户 authorized_keys 里的 command= 选项锁定。客户端 — 包括已经被攻破的客户端 — 可以添加归档,却无法删除。对上一节的威胁模型而言,这是两个项目中最有价值的一项功能。

所以决定起来很简单。如果另一端归你控制,就用开启仅追加模式的 Borg。如果不归你控制,就用 restic,再通过在目标端运行 rest-server --append-only,或者使用开启了对象锁定的存储桶,把同样的保障找回来。两条路下面都会讲到。

在选定仓库之前还有一点要提醒:Borg 的命令接口在不同大版本之间并不稳定 — 下文用到的 repo::archive 简写属于 1.x 系列,到了 2.x 就变了 — 所以要拿 borg --version 去对照你正在读的文档,而不是反过来。restic 在这方面要稳定一些,代价是它自己也经历过一次仓库格式迁移。

有一件事不要做:以“两份备份总比一份安全”为由,两个都跑。两套维护得半吊子的系统,只会给你两个没验证过的仓库,外加一套在压力之下想不起来的恢复流程。选那个适合你另一端的工具,把省下的精力花在测试它上。

仓库放在哪里,以及谁有权删除它

上面这些都是标准建议。下面才是通常被略过的部分。

备份服务器的常规做法,是给它一个凭据,让它往外推送。而这个凭据几乎总是也能删除 — 因为清理旧快照需要删除权限,而发一把什么都能干的密钥更省事。后果就是:拿下你服务器的攻击者,也一并拿下了你的备份,而在你最需要备份的那个场景里,它恰恰一文不值。出路只有三条:

  • 在目标端启用仅追加。不管客户端提出什么请求,另一端都只接收新数据、拒绝删除。borg serve --append-only 和 rest-server --append-only 都能做到,并且由服务器端强制执行,删除则变成你在备份主机上有意为之的操作。
  • 对象锁定。S3 兼容存储可以让对象在保留期内不可更改。有效,只不过它把信任转移到了一家服务商和一段付费关系上。
  • 用拉取代替推送。由备份主机主动连进来读取数据。生产服务器上的任何东西都碰不到仓库 — 但现在备份主机手里握着一把进入生产环境的钥匙,所以你只是把信任关系反转了,而不是消除了它。如果备份主机确实被锁得更严,这样做是合理的;否则并不见得更好。

仅追加这条路最便宜,前提条件也最少,所以下面搭建的就是它。

然后是放在哪里的问题。备份是你服务器的一份完整副本,它受所在地的法律管辖,挂在某个人名下的账户里。精挑细选了服务器的司法辖区,然后每晚把副本推送到另一个完全不相干地方的存储桶里,这是一种常见而又不声不响的错误 — 副本在哪个司法辖区,数据就在哪个司法辖区。客户端加密能在很大程度上缓解这一点:向你的存储服务商发出的调取要求,拿到的只是密文。它保护不了的是可用性,也掩盖不了仓库存在这一事实、它有多大,以及它最后一次变动是在什么时候。

正因如此,一次解决最多问题的方案,是第二台同样归你所有的小服务器,放在和第一台不同的国家,用和第一台相同的方式租下:不绑定任何身份,免实名,用 Monero 支付。两端都是你的,两端都便宜,传输从不经过第三方,而且无论原服务器所在的司法辖区或账户出了什么事,都波及不到这份副本。

给它选配置,比给一台真正干活的服务器选配置容易,因为备份目标说白了就是一块带着 CPU 的磁盘。它真正需要的,是足够的内存,好让清理(prune)和完整性检查跑起来不至于疯狂换页,这是唯一真正吃资源的环节 — 所以 1 GB 偏紧,2 GB 就宽裕了。对照套餐列表:

  • Cub — 1 vCPU / 2 GB / 40 GB NVMe,$5.00/月。默认之选。一台普通的 Web 或应用服务器,线上数据 10–15 GB,保留一年的每日备份,放在这里绰绰有余,因为大部分工作都由去重和压缩完成了。
  • Scout — 2 vCPU / 4 GB / 70 GB,$9.00/月。适合把几台服务器备份到同一个仓库,或者一套存着照片的 Nextcloud。
  • Runner 和 Hunter — 100 GB 售 $14.00,140 GB 售 $19.00。邮件存储、媒体库或好几年的历史记录,最后都会落到这一档。
  • Pup — 1 vCPU / 1 GB / 25 GB,$3.50/月。只备份配置和数据库转储的话够用。别把一棵庞大的文件树丢给它,还指望维护操作能一直顺顺当当。

磁盘容量有一条好用的经验法则:按单次完整备份大小的两到三倍来规划,你就能留住一年的历史,还有余地容你估错。把它放在一个不是原服务器所在地的地方 — 阿姆斯特丹、巴黎、布加勒斯特和索非亚都是基础价,雷克雅未克乘以 1.20,苏黎世乘以 1.35,也就是说一台放在瑞士的 Cub 每月 $6.75。首次上传是你唯一一次大流量传输,而且 1 Gbps 端口流量不限,所以初次填充仓库不会在月底给你带来一张账单。

分步操作

  1. 分清哪些东西不可替代,其余的列成清单

    在碰任何工具之前,先花十分钟把服务器上的东西分成三堆。光这一步,就能省掉大部分成本,以及几乎所有的困惑。

    不可替代 — 在别处都不存在的东西:数据库、上传的文件、应用状态、无法重新签发的 TLS 材料、钱包文件、SSH 主机密钥,以及整个 /etc,因为 /etc 是一份书面记录,记下了你做过、然后又忘掉的每一个决定。

    费点功夫就能重建 — 已安装的软件包、容器镜像,以及任何可以用脚本重建的东西,前提是脚本本身在第一堆里。

    可以免费重新获取 — 软件包缓存、区块链数据、发行版安装介质、构建产物。这些永远不要备份。在一个节点上,这就是 3 GB 的仓库和 300 GB 的仓库之间的区别。

    现在就把第三堆写成一个排除文件,这样从第一次运行起它就能生效:

    cat > /etc/backup-excludes <<'EOF'
    /dev
    /proc
    /sys
    /run
    /tmp
    /var/tmp
    /var/cache
    /var/lib/apt/lists
    /var/lib/docker/overlay2
    /swapfile
    /var/lib/bitcoind/blocks
    /var/lib/bitcoind/chainstate
    **/node_modules
    **/.cache
    EOF

    Docker 值得单独说一句。备份 /var/lib/docker 是一种错误的直觉:那些镜像层既能重建,又体积庞大。要备份的是你的 compose 文件和命名卷 — /var/lib/docker/volumes — 镜像则让它们从镜像仓库重新拉回来。

  2. 搭建备份目标机,只让它干一件事

    部署第二台服务器 — 默认选 Cub,Debian 13,放在和被备份服务器不同的国家。和其他任何机器一样,给它做一遍十分钟加固:只用密钥登录 SSH、禁用 root 登录、默认拒绝的防火墙。这台机器上存着另一台机器的完整副本,所以绝不是可以掉以轻心的地方。

    然后创建一个用户,它存在的全部意义就是接收备份,没有交互式登录权限,也做不了其他任何事:

    adduser --disabled-password --gecos '' --home /srv/backup --shell /bin/bash backup
    install -d -m 700 -o backup -g backup /srv/backup/.ssh
    apt install -y borgbackup      # or: apt install -y restic

    这里的 shell 是特意设成真实 shell 的。SSH 强制命令是由该账户的登录 shell 来执行的,所以在这里设成 nologin — 这是人的本能反应 — 非但不会加强、反而会破坏你在第七步里要加上的限制。真正起约束作用的是那行 command=,而不是 shell 字段。

    在生产服务器上生成一把专用于此、别无他用的密钥,这样以后撤销备份访问权限时,永远不必碰到你自己的登录方式:

    ssh-keygen -t ed25519 -f /root/.ssh/id_backup -C 'backup@prod' -N ''

    把公钥复制到目标机的 /srv/backup/.ssh/authorized_keys 里,属主设为 backup,权限设为 600。暂时让它保持为一条普通条目 — 等你确认整条链路都能跑通之后,第七步才会给它加上限制。

  3. 让数据库可以被安全地拷贝

    有没有这一步,决定了你得到的是一份备份,还是一个看起来像备份的文件。数据库引擎把状态保存在内存里,并按它自己的顺序写入磁盘;从外部拷贝那个目录,抓到的是引擎从未打算让人看到的某个瞬间,恢复出来的东西要么启动不了 — 要么更糟,能启动,却藏着细微的错误。

    先转储,并且安排在备份之前运行。对于 MariaDB 或 MySQL,--single-transaction 能在不把应用锁在门外的情况下,为 InnoDB 表生成一份一致的快照:

    install -d -m 700 /var/backups/db
    mariadb-dump --single-transaction --quick --all-databases \
      | zstd -T0 > /var/backups/db/all-$(date +%F).sql.zst

    对于 PostgreSQL,以 postgres 用户身份运行;pg_dumpall 还会把角色一并导出,而很多人都是到了恢复那天才发现自己需要它们:

    su - postgres -c 'pg_dumpall' | zstd -T0 > /var/backups/db/pg-$(date +%F).sql.zst

    SQLite 有它自己的陷阱:在预写日志(WAL)仍处于活动状态时直接拷贝文件,可能抓到一个撕裂的中间状态。改为让 SQLite 库自己来做:

    sqlite3 /var/lib/app/app.db ".backup '/var/backups/db/app.db'"

    磁盘上只保留最近几天的转储 — 保存历史是仓库的事 — 并且记住,/var/backups/db 现在包含了你数据库里的一切,所以权限 700 可不是摆设。如果你根本不想让明文转储落到磁盘上,两个工具都可以从管道读取:borg create ::db-{now} - 配合 --stdin-name,或者 restic backup --stdin --stdin-filename。

  4. 初始化仓库,把口令存到服务器弄丢不了的地方

    生成一个具有足够熵的口令,因为在仓库和任何拿到仓库的人之间,挡着的只有它:

    openssl rand -base64 32

    在使用之前,先把它存进你的密码管理器,并抄在纸上。这份指南里的其他一切,犯了错都还能补救;唯独这一步不行。一个口令只存在于它所保护的那台服务器上的仓库,是你能做出来的最完整、也最无用的备份。

    然后初始化。用 Borg 的话,通过 SSH 连接第二步创建的那个用户,选用带认证、密钥存放在仓库中的模式 — 并立刻把这把密钥导出到别处:

    export BORG_REPO='ssh://[email protected]/srv/backup/prod'
    borg init --encryption=repokey-blake2
    borg key export :: /root/borg-key.txt   # then move it OFF this machine

    用 restic 的话,等价的做法是走 SFTP:

    export RESTIC_REPOSITORY='sftp:[email protected]:/srv/backup/prod'
    export RESTIC_PASSWORD_FILE=/root/.restic-pass
    install -m 600 /dev/null /root/.restic-pass && nano /root/.restic-pass
    restic init

    没错,口令文件就放在生产服务器上 — 要让无人值守的任务跑起来,只能这样。也正因为如此,真正要紧的是密码管理器里和纸上的那两份,而接下来的几步,也正是为了让这台服务器无法销毁任何东西。

  5. 运行第一次备份,读懂它告诉你的东西

    第一次运行是最慢的:一切都是新的,所以没有东西能去重,也没有任何缓存。在一个你能盯着看的终端里手动运行它,如果文件树很大,要做好等上一阵子的准备。

    borg create --stats --progress --compression zstd,3 \
      --exclude-from /etc/backup-excludes ::'prod-{now}' \
      /etc /home /root /srv /var/www /var/lib/docker/volumes /var/backups/db
    restic backup --verbose --exclude-file /etc/backup-excludes \
      /etc /home /root /srv /var/www /var/lib/docker/volumes /var/backups/db

    然后做那件几乎没人做的事,它只花两分钟,却能抓住首次尝试时的大部分错误 — 看看你实际备份下来的到底是什么:

    borg list ::prod-... | less
    restic ls latest | less

    你要检查的是方向相反的两种失误。一种是漏掉了重要的东西,原因是路径写错了,或者某条排除规则比预想的更贪婪。另一种是混进了不该有的庞然大物 — 一个缓存目录、一条区块链、一堆日志归档 — 因为这决定了你的仓库是能在那块磁盘上安稳放上好几年,还是撑不到来年春天就把它塞满。趁仓库还小、重跑一遍不花什么成本,现在就把列表改好。

  6. 用 systemd 定时器实现自动化,而不是一行 cron

    把整套流程 — 转储、备份、清理、汇报 — 放进一个脚本里,这样出了问题时,要读的东西只有一个:

    install -m 700 /dev/null /usr/local/sbin/backup-run
    nano /usr/local/sbin/backup-run   # set -eu; dumps; borg create; ping the monitor

    接着是一个 service 和一个 timer。在这里为 systemd 多写两个文件是值得的:退出状态会被记录下来,输出进入 journal,而不是进入一封没人看的邮件;Persistent=true 能把机器休眠期间错过的那次运行补上;而且失败可以直接触发一个告警单元。

    # /etc/systemd/system/backup.service
    [Unit]
    Description=Encrypted off-site backup
    OnFailure=backup-alert.service
    
    [Service]
    Type=oneshot
    ExecStart=/usr/local/sbin/backup-run
    Nice=10
    IOSchedulingClass=idle
    # /etc/systemd/system/backup.timer
    [Unit]
    Description=Nightly encrypted backup
    
    [Timer]
    OnCalendar=daily
    RandomizedDelaySec=45m
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    systemctl daemon-reload && systemctl enable --now backup.timer
    systemctl list-timers backup.timer
    journalctl -u backup.service -n 50

    Nice 和 IOSchedulingClass=idle 能让每晚的备份在运行时不拖慢网站。RandomizedDelaySec 比看上去更重要:没有它,你名下的每台机器都会在同一秒开始备份,这对目标机不友好,对你自己也不友好。

  7. 收回生产服务器的删除权限

    现在来补上大多数方案都没补上的那个缺口。在备份目标机上编辑 /srv/backup/.ssh/authorized_keys,把生产服务器的密钥锁定到一条删不了任何东西的命令上:

    command="borg serve --append-only --restrict-to-path /srv/backup/prod",restrict ssh-ed25519 AAAA...

    这里有两个机制在协同工作。--append-only 让服务器拒绝一切删除请求,不管客户端怎么要求;--restrict-to-path 则把它限制在自己的仓库里,这样一台被攻破的主机就碰不到其他主机的仓库。restrict 选项会关闭端口转发、agent 转发以及其他所有功能,所以这把密钥除了这件事,什么都打不开。

    restic 的等价做法,是在目标机上运行它的 REST 服务器,由它来强制执行同样的规则:

    rest-server --path /srv/backup --append-only --private-repos

    在生产服务器上认真测试一遍,方法就是故意去做那件被禁止的事:

    borg delete ::prod-...     # must fail
    borg create ::test-{now} /etc   # must succeed

    如果删除成功了,说明 command= 那一行没有生效 — 通常是文件里还留着第二步时加的另一把不受限制的密钥。从现在起,清理在目标机上进行,仓库在那里是本地的:在那边设一个每周运行 borg prune 和 borg compact 的定时器,生产服务器对此完全没有发言权。

  8. 动手恢复——然后把下一次演练写进日历

    到这里为止的一切都只是准备。这一步,才让它真正成为一份备份。

    先做完整性检查,它检验的是仓库自身的一致性。要抽样读取实际数据,而不只是元数据,因为只查元数据的检查,在无法恢复的仓库上照样能通过:

    borg check --verify-data ::
    restic check --read-data-subset=10%

    然后做一次真正的恢复,恢复到一个临时目录,或者更好的是,恢复到一台新部署、用完就销毁的服务器上。挂载是浏览内容最快的方式;而解包,才是你凌晨三点真正要做的事:

    mkdir /mnt/r && borg mount ::prod-... /mnt/r      # browse
    borg extract ::prod-... etc/nginx                  # restore one path
    
    restic mount /mnt/r
    restic restore latest --target /tmp/restore --include /etc/nginx

    现在来做与文件无关的那部分。把数据库转储导入一个正在运行的数据库引擎,再让应用连上它打开看看。能解压的转储,不等于能导入的转储,而这两者之间的差距,已经葬送过不止一家公司:

    zstd -dc /tmp/restore/var/backups/db/all-2026-09-03.sql.zst | mariadb

    记下整个过程花了多长时间,以及中途有哪些东西需要现查。这个数字才是你真实的恢复时间,而不是你想当然的那个;这些笔记则能让第二次演练快上很多。然后安排好下一次 — 每季度一次是合理的间隔,而一条日历提醒,比“打算要做”靠谱得多。

保留策略:为一个你还没察觉的问题留足历史

保留策略,是人们悄无声息地弄丢本来还在的数据的地方。保留七个每日快照,感觉挺负责,实际上只能防住你一周之内就发现的问题。静默损坏、被某个 bug 写成截断状态的数据库、四月删掉而六月才需要的文件、一个在做出任何可见动作之前先潜伏了很久的入侵者 — 这些都是在那七天早已轮换完之后很久才被发现的,到那时,每一个快照都忠实地保存着损坏后的状态。

标准做法是分层保留,而这几乎不花什么成本,因为有了去重,变化缓慢的数据做一个月度快照,占用的空间小到可以忽略不计:

restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 2 --prune
borg prune --keep-daily=7 --keep-weekly=5 --keep-monthly=12 --keep-yearly=2 ::
borg compact

有两个机制上的细节值得了解,因为它们都让人吃过一惊。在 restic 里,forget 只是把快照从索引中移除,并不回收任何空间;真正删除数据的是 prune 这一步,所以命令里才带着这个参数。在 Borg 里,prune 会删除归档,但仓库并不会变小,直到 borg compact 运行为止 — 它被做成一条单独的命令,正是为了能单独安排运行时间。

这和上一节确实存在矛盾:清理需要删除权限,而你刚刚把删除权限从客户端手里收走了。这是正确的取舍,而不是设计缺陷。解决办法是在备份主机上做清理,在那里仓库是本地的,客户端没有发言权 — 在目标端设一个每周运行的定时器,全程不需要生产服务器参与。如果你嫌这套机制太重,替代方案是准备第二把不带 --append-only 的 SSH 密钥,不放在生产服务器上,每隔几个月手动用一次。你不该做的,是为了让一个 cron 任务简单点,又把不受限制的密钥放回服务器上。

监控,因为备份失败时悄无声息

一个停止运行的备份任务,和一个无事可做的备份任务,输出完全一样:什么都没有。这是备份在运维上的决定性特征,也是大多数坏掉的备份会一直坏上好几个月的原因。有三样东西值得接上,按价值从低到高排列:

  • 失败告警。systemd 单元可以通过 OnFailure= 在失败时触发另一个单元,由那个单元给你发一条消息。必要,但却是三者中最弱的,因为它只有在某个东西真的运行了、并且报了错时才会触发。
  • 时效告警。询问仓库最后一次收到数据是什么时候,如果答案超过一两天,就发出告警。这能抓住被屏蔽(mask)的定时器、写满的磁盘、解析不了的主机名 — 所有那些因为根本没跑、所以从来不会报错的故障。
  • 死人开关。把逻辑反过来:任务成功时去 ping 一个 URL,一旦 ping 停了,就由外部的某个服务向你告警。这是唯一一种在整台服务器都没了的情况下依然有效的办法,而这种故障,你最不希望是从客户那里得知的。

最划算的是时效检查,只要三行:

#!/bin/sh
# alert if the newest snapshot is older than 36 hours
last=$(restic snapshots --json --latest 1 | grep -o '"time":"[^"]*"' | head -1 | cut -d'"' -f4)
[ $(( $(date +%s) - $(date -d "$last" +%s) )) -lt 129600 ] || echo "BACKUP STALE: $last"

目标端的磁盘也要盯着。备份卷一旦写满,此后的每个任务都会失败,而且是在最后才失败 — 在传输完成之后,这是发现问题代价最高的方式。在用量达到百分之八十时告警,能给你留出几周的提前量;不设告警,就一点提前量都没有。

真正让人丢掉数据的那些失败方式

大致按它们把备份变成纪念品的频率排序:

  • 数据库在运行时被直接拷贝。在写入中途被抓取的 InnoDB 数据目录,恢复出来是一个启动不了的数据库,而且这一切悄无声息 — 备份顺利完成,文件一个不少,大小看起来也对。先转储。它被排在第三步,是有原因的。
  • 口令只存在服务器上。一个完美无瑕的加密仓库,如果它的口令放在那台已经烧毁的机器的 /root 里,那它就只是一堆高质量的噪声。口令存在密码管理器里、而这个密码管理器唯一的副本又是通过同一台服务器同步的,也是一样。
  • 从来没人做过恢复。排除列表过于激进,数据库转储连续三个月都是空的,路径写错了。这些问题在恢复之前全都看不见,而一旦做了恢复,十分钟之内就一目了然。
  • 备份放在同一个账户里。同一家服务商、同一个登录、同一段付费关系。应对磁盘故障没问题,除此之外一概无用。
  • 客户端能执行删除。勒索软件会在碰数据之前先删掉备份,因为它的整个商业模式就建立在这上面。要么启用仅追加,要么接受这种情况你防不住。
  • 目标端悄悄被写满了。保留策略从来没执行过清理,或者包含列表里新加了一个大目录。此后的每个任务都会失败,而如果你没做时效监控,没有任何东西会告诉你。
  • 区块链数据进了备份。备份 Bitcoin 或 Monero 的数据目录,等于花钱存储一本你随时可以免费重新下载的公共账本。备份钱包和配置文件;把链数据排除掉。
  • 排除列表误吞了东西。一条本来针对 /var/cache 的规则,同时匹配到了某个应用的缓存目录,而那里面存着应用永远不会自己重建的状态。把第一次备份的文件列表认认真真读一遍。
  • 恢复需要的密钥就存在仓库里。Borg 的 repokey 模式把密钥保存在仓库内部,这很方便,直到你丢的恰好就是仓库本身。一开始就用 borg key export 把它导出到别处,做一次就够。

备份做不到的事

老老实实说清四个局限,因为一份把备份吹过头的指南,和一份没测试过的备份,干的是同一件事。

备份不是高可用。它能缩短停机时间,却不能避免停机。恢复一台服务器,意味着开通机器、拉取数据、解包、重新配置 — 规模小的也得足足一个小时,要是你上一次尝试恢复是“从来没有”,花的时间还要多得多。如果一个小时的停机你无法接受,答案是一台备用机,而备份依然必不可少,因为备用机会忠实地复制一切,包括你犯的错。

加密并不会让副本隐形。它让副本无法被读取,这是两回事。托管仓库的一方依然能看到它的存在、它有多大、它最后一次变动是什么时候 — 而为它付费的那个账户,和其他任何信息一样,都是能识别身份的细节。如果当初让你以免实名方式租下原服务器的理由依然成立,那它对第二台机器同样成立;同一套结账流程两台都适用。

静态加密并不等于在任何意义上都是端到端加密。restic 和 Borg 保护的是仓库。它们保护不了正在运行的服务器,那里的数据按定义就是明文;如果口令连同保存它的机器一起失守,它们也无能为力。源服务器上的全盘加密,和一份在两台机器之外另行保管的口令,是互补的控制措施,而不是重复的。

还有,放在另一个国家的副本,带来的是阻力,而不是豁免。司法辖区改变的是谁需要向谁提出请求,以及要花多长时间 — 离岸主机托管词条对这一点有详细论述。这里的两台机器都遵守同一份明文写出的可接受使用政策,底线也完全相同。这种安排真正换来的,是任何单一故障 — 一块坏掉的磁盘、一条敲错的命令、一个入侵者、一个被冻结的账户 — 都不会同时带走原件和副本。这正是备份存在的意义,而这就够了。

常见问题

常见问题

服务商的快照够用吗?

不够,而这恰恰是最常见的缺口。快照和原件在同一套基础设施上、同一个账户里,由同一家服务商拍摄 — 所以它能应对文件系统损坏或一次失败的升级,其他情况一概应付不了:拿到你凭据的攻击者、被封停或丢失的账户、司法辖区方面的问题、一个不再回应的服务商。快照是一个快捷的撤销按钮,而且确实有用。备份则是一份在整个账户都丢失后依然存在的副本,这也是为什么我们的文档明确写着备份由你自己负责,而不是含糊其辞地暗示别的。

restic 还是 Borg——我该用哪个?

如果另一端归你控制,选 Borg,因为 borg serve --append-only 是由服务器强制执行的保证:被攻破的客户端删不掉你的历史,而这是一套备份系统最有价值的特性。如果另一端是存储桶,或者是一台你没法装软件的机器,选 restic,因为它是一个静态编译的单文件程序,能对接 S3、SFTP、它自己的 REST 服务器,或者任何 rclone 能访问到的地方。两者都在客户端加密和认证,都做去重,都做压缩。按目标端来选,然后就别再比较了,去测试一次恢复吧。

做备份目标的 VPS 需要多大配置?

Cub(1 vCPU / 2 GB / 40 GB NVMe,$5.00/月)是默认选择,足以容纳一台普通 Web 或应用服务器一年的每日快照,因为有了去重和压缩,保存历史的成本远低于保存完整拷贝。Scout(2 vCPU / 4 GB / 70 GB,$9.00/月)适合备份好几台服务器,或者一套存着照片的 Nextcloud;再往上是 100 GB 和 140 GB,分别为 $14.00 和 $19.00。磁盘按单次完整备份的两到三倍来规划,内存宁选 2 GB 而不选 1 GB — 清理和完整性检查这两项操作需要它。

我能直接备份到网盘或 S3 存储桶吗?

技术上可以 — restic 能直接写入 S3 兼容存储,借助 rclone 几乎能写入其他任何地方,而且因为加密发生在上传之前,服务商存的只是它读不懂的密文。不过有两件事变了。那个账户意味着一张银行卡、一个名字和一个司法辖区,和你服务器的一份完整副本绑在了一起;如果原服务器是用只凭一个令牌开通的账户租下、用加密货币支付的,这一点就值得留意。而且不可篡改性现在取决于那家服务商的对象锁定实现,而不是 authorized_keys 里一行由你掌控的配置。一台你自己拥有的小服务器,通常更便宜,也永远更可预期。

正在被写入的数据库该怎么备份?

绝不要直接拷贝它的数据目录。先转储,再备份转储文件:MariaDB 或 MySQL 用 mariadb-dump --single-transaction,它能在不把应用锁在门外的情况下,给出 InnoDB 的一致视图;PostgreSQL 以 postgres 用户身份运行 pg_dumpall,它还会把角色一并导出;SQLite 用 sqlite3 db ".backup out.db",因为在预写日志仍处于活动状态时拷贝文件,可能抓到一个撕裂的中间状态。对一个正在运行的数据库引擎做文件级拷贝,恢复出来是一个启动不了的数据库,而且失败得悄无声息 — 备份顺利完成,看上去一切正常。

如果我弄丢了仓库口令,会怎样?

数据就没了。这不是一条可以申诉的政策 — 加密是在你的机器上、用你的密钥完成的,无论是我们还是工具的作者,都没有任何办法进去。这是正确的设计,也正因如此,口令必须在第一次备份运行之前,就存进密码管理器并且抄在纸上,绝不能只存在它所保护的那台服务器上。用 Borg 的话要注意,repokey 模式把密钥存放在仓库内部,所以一开始就要用 borg key export 把它导出到别处 — 否则一旦丢了仓库,你手上可能还留着的任何仓库副本,也都没法读取了。

多久应该测试一次恢复?

完整演练每季度一次;此外,只要备份内容发生了重大变化,就立刻再做一次。完整演练指的是:真正恢复到一台临时服务器上,把数据库转储导入一个正在运行的引擎,再让应用连上它打开 — 而不是列一下归档内容,那只能证明归档存在。给它计时。这个数字就是你真实的恢复时间,而它几乎总是比人们想当然的更长。两次演练之间,用带 --verify-data 或 --read-data-subset 的自动完整性检查,加上快照时效告警,覆盖其余的部分。

约一分钟部署一台离岸 VPS

无 KYC,加密货币支付,全 NVMe。选择套餐,用 Monero 或任意主流币付款,约 60 秒即可获得 root。

Fenrir 守卫中