备份失败告警:别等到要恢复时才发现备份坏了

备份可能悄悄失败好几周。捕获 restic 和 borg 的错误、过期快照与漏跑的任务,备份出问题时直接打电话告诉你。附可复制脚本。

更新于

目录

大多数人是在需要恢复数据的那天,才发现备份早就坏了。任务三周前就报错退出,没人看日志,最新的快照比你想撤销的那次误操作还要旧。

解决办法不是换一个更好的备份工具,而是确保备份出问题的那一小时,就有人知道。本文以 restic 和 borg(以及任何在 shell 里运行的备份命令)为例,覆盖备份真正会失败的三种方式,并提供可以直接复制的脚本。最后一步通过 Echobell 把告警发到你的手机上,让坏掉的备份真的响铃,而不是躺在邮箱里。

备份失败的三种方式

只监控第一种,是最常见的错误。

  1. 跑了,但失败了。 仓库被锁、磁盘写满、密码错误、网络中断。工具以非零状态码退出。
  2. 跑了,但只成功了一半。 restic 在快照已创建、但部分文件无法读取时会以退出码 3 结束;borg 遇到警告时退出码为 1。这两种都很容易被一笔带过,但都可能掩盖一个权限问题,悄悄跳过你最重要的目录。
  3. 根本没跑。 服务器关机了、定时器被禁用了、crontab 被覆盖了。没有进程,自然没有报错。这才是真正要命的失败。

每一种需要不同的检测手段,所以我们把三种都做出来。

第 1 步:创建一个能吸引你注意的频道

在 Echobell 中创建一个名为 backups 的频道,对重要的任务把通知类型设为 来电(Calling)。复制它的 Webhook URL:

https://hook.echobell.one/t/<channel-token>

用我们下面会发送的字段,写一套半梦半醒也能看懂的模板:

标题:{{host}} 备份{{status}}
正文:{{job}}:{{detail}}

模板语法见模板参考。

第 2 步:用包装脚本捕获失败

一个小小的包装脚本就能把退出码变成告警。保存为 /usr/local/bin/backup-run:

#!/usr/bin/env bash
set -uo pipefail

HOOK="https://hook.echobell.one/t/<channel-token>"
JOB="nightly-restic"

alert() {
  curl -fsS -m 10 -X POST "$HOOK" \
    -H "Content-Type: application/json" \
    -d "{\"job\":\"$JOB\",\"host\":\"$(hostname)\",\"status\":\"$1\",\"detail\":\"$2\"}" || true
}

restic backup /srv /etc --exclude-caches
code=$?

case $code in
  0)  ;;                                   # 成功:保持安静
  3)  alert "incomplete" "Snapshot saved, but some files were unreadable" ;;
  11) alert "locked" "Repository is locked by another process" ;;
  12) alert "auth-failed" "Wrong repository password" ;;
  *)  alert "failed" "restic exited with code $code" ;;
esac

exit $code

这些细节为什么重要:

  • 刻意不用 set -e,这样 restic 失败之后脚本仍能执行到告警那一步。
  • curl 后面的 || true 保证 Webhook 不可达时,也不会掩盖真正的退出码。
  • -m 10 避免网络请求挂住,进而卡住下一次定时运行。

上面用到的 restic 退出码在 restic 手册中有说明:0 成功,1 致命错误,3 快照已创建但部分源文件不可读,10 仓库不存在,11 加锁失败,12 密码错误。退出码是逐版本新增的,请在你自己的机器上用 restic version 核对。

borg 和 borgmatic

borg 成功返回 0,警告返回 1,错误返回 2 或更大。备份过程中有文件被修改会产生 1,日志目录里这很常见,所以常见做法是只在 2 及以上时打电话,1 只发一条安静的通知:

borg create --stats ::'{hostname}-{now}' /srv /etc
code=$?
if [ "$code" -ge 2 ]; then
  alert "failed" "borg exited with code $code"
elif [ "$code" -eq 1 ]; then
  alert "warning" "borg finished with warnings"
fi

如果你用 borgmatic,它的 on_error 钩子会在 borgmatic 自身出错时执行一条命令,正好可以放同样的 curl。注意 borgmatic 文档自己指出的局限:如果 borgmatic 根本没有运行,on_error 就不会触发。这正是第三种失败,需要第 3 步来解决。

第 3 步:捕获从未运行的备份

包装脚本只能报告「发生过的运行」。要捕获「沉默」,就把逻辑倒过来:备份成功时向监控服务发一次 ping,监控服务在 ping 迟到时通知你。这叫做死人开关(dead man's switch)。

Healthchecks.io(托管或自建均可)正是做这件事的:

  1. 创建一个检查项,调度设为 0 3 * * *,宽限时间设为一两个小时。
  2. 把它的 ping 加到任务末尾,保证只有成功时才会触发。
  3. 在该检查项的集成里添加一个 Webhook,指向你的 Echobell 频道 URL。

ping 放在包装脚本的最后:

# 放在 backup-run 最后的 `exit $code` 之前,只在成功时 ping
if [ $code -eq 0 ]; then
  curl -fsS -m 10 --retry 3 -o /dev/null https://hc-ping.com/<your-uuid>
fi

Healthchecks 还支持 /start 和 /fail 两种 ping,也可以把退出码放进 URL,所以同一个检查项还能抓到卡死的任务。结果就是:服务器宕机、定时器被禁用、crontab 那一行被删,最终都是同一个结局,手机响铃。通用思路和其他监控工具,见 cron 任务失败告警。

第 4 步:确认备份够新、能恢复

备份可以按时运行、退出码为 0,却依然毫无用处,比如挂载失败后你备份的是一个空目录。再加一个独立的每日任务,做两项低成本的检查:

快照年龄。 如果最新快照比预期的旧,就告警:

latest=$(restic snapshots --latest 1 --json | jq -r '.[0].time')
age_h=$(( ( $(date +%s) - $(date -d "$latest" +%s) ) / 3600 ))
if [ "$age_h" -gt 30 ]; then
  alert "stale" "Newest snapshot is ${age_h}h old"
fi

仓库完整性。 restic check 会校验仓库结构。每周跑一次;仓库很大时,restic check --read-data-subset=5% 可以抽样检查数据而不必全部读完。退出码非零就告警。

这两项都代替不了真正的恢复测试。每个季度,把一个你能验证的文件恢复到临时目录一次。这是唯一能证明备份可用的测试。

第 5 步:只在重要时才响铃

如果每条警告都打电话,你迟早会把频道静音,然后真正要紧的那次它也不会响。用 Echobell 的条件在同一个频道里区分级别:

status == "failed" || status == "stale" || status == "auth-failed"

把 incomplete 和 warning 发到第二个 时效性(Time Sensitive) 或普通频道,它们会出现在锁屏上但不会响铃。如果想让白天的失败安静一点,可以加一个时间窗口,使用条件参考中介绍的 UTC 变量。

常见问题

备份失败时怎么收到通知?

包装备份命令,读取退出码,任何非零码都发一个 Webhook。再加一个 Healthchecks.io 之类的死人开关,这样从未启动的运行你也能知道。

应该对哪些 restic 退出码告警?

对 1、10、11、12 这类真正的失败要大声告警。3(快照已创建但部分文件不可读)当作需要排查的警告,因为它可能掩盖权限问题。

怎么知道备份是不是悄悄停了?

用心跳:任务成功时向监控服务发 ping,ping 迟到时监控服务告警。错误钩子抓不到这种情况,因为根本没有进程在运行、也就没有人去抛出错误。

退出码成功就能信任备份吗?

不能。它只说明工具跑完了,不说明数据完整或可恢复。要检查快照年龄、定期做完整性检查,并经常测试恢复。

能用电话代替邮件吗?

可以。邮件太容易被漏掉。Echobell 的来电频道表现得像一通打进来的电话,能穿透 iOS 专注模式,所以凌晨 03:00 备份失败你一定会注意到。

小结

三种检测手段覆盖三种失败模式:用退出码包装脚本抓「运行失败」,用新鲜度检查抓「成功却没用」,用心跳抓「根本没跑」。把它们接到来电频道,警告留给更安静的频道,坏掉的备份就会变成一次五分钟的修复,而不是一个报废的周末。

在 iPhone 上下载 Echobell 或在 Google Play 获取,创建 backups 频道,然后故意用错误的密码跑一次包装脚本,亲耳听听它响铃。

相关内容