---
title: "备份失败告警：别等到要恢复时才发现备份坏了"
description: "备份可能悄悄失败好几周。捕获 restic 和 borg 的错误、过期快照与漏跑的任务，备份出问题时直接打电话告诉你。附可复制脚本。"
date: 2026-10-09
author: Nooc
authorAvatarLink: /images/avatars/nooc.webp
authorLink: https://nooc.me
tags:
  - 备份监控
  - restic
  - borg
  - 故障告警
  - 电话告警
  - Webhook
---

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

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

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

## 备份失败的三种方式

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

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}}
```

模板语法见[模板参考](/zh/docs/template)。

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

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

```bash
#!/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` 只发一条安静的通知：

```bash
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](https://healthchecks.io)（托管或自建均可）正是做这件事的：

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

ping 放在包装脚本的最后：

```bash
# 放在 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 任务失败告警](/zh/blog/cron-job-failure-alerts)。

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

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

**快照年龄。** 如果最新快照比预期的旧，就告警：

```bash
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 的[条件](/zh/docs/conditions)在同一个频道里区分级别：

```
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](https://apps.apple.com/app/apple-store/id6743597198?pt=128151925&ct=blog-backup-failure-alerts-zh&mt=8) 或[在 Google Play 获取](https://play.google.com/store/apps/details?id=one.echobell.echobellandroid)，创建 `backups` 频道，然后故意用错误的密码跑一次包装脚本，亲耳听听它响铃。

## 相关内容

- [cron 任务失败告警](/zh/blog/cron-job-failure-alerts)
- [开发者的告警疲劳修复指南](/zh/blog/fix-alert-fatigue-developer-guide)
- [频道条件参考](/zh/docs/conditions)
- [Webhook 集成指南](/zh/docs/webhook)
- [SSL 证书到期告警](/zh/blog/ssl-certificate-expiry-alerts)
