备份脚本昨天还成功,今天为什么没跑:用 Healthchecks 监控定时任务

为定时备份建立开始、成功和失败信号,保留真实退出状态,处理漏跑、管道错误、通知失败与迁移后的自动执行验证。

·10 min监控
土耳其 VDS,完整 root 权限|BRNCHOST · 自管服务器
建站开服,云上轻松起步|雨云 RCS · 宝塔 / 1Panel 预装
低价年付,搭起你的应用|RackNerd · KVM VPS · SSD 存储
香港轻量,按配置选型|晚安云 · 云服务器
NVMe 机型,关注磁盘 I/O|野草云 · 香港 VPS
大陆优化,连接海外应用|搬瓦工 · CN2 GIA / CTGNet 套餐
读文件、写文档、跑任务|WorkBuddy · AI 工作台
CVM 云主机,配置按需选|腾讯云 · 云服务器
中国方向优化,认准系列|DMIT · Premium / CN2 GIA
双 ISP 住宅 VPS|丽萨主机 · 原生 IP · 多地区产品
每周自动异地备份|Evoxt · 高频 CPU · 云服务器
京东云轻量云主机:2核2G,129元/年,新人限购1台

服务器在线、网站返回 200,并不能说明凌晨的备份正常执行。cron 配置被改了,磁盘空间不足,脚本只执行了前半段,或者迁移服务器时漏掉了定时任务,网站仍可能照常打开。等到需要恢复数据时,才发现最新备份停在几周前。

这类问题需要监控任务的执行约定。约定凌晨两点备份,就在相应时间确认收到成功信号;迟迟收不到信号,也应报警。Healthchecks 适合做这件事:脚本主动汇报,监控端根据周期或 cron 时间判断任务有没有按时完成。

接入时最容易出错的地方是把“发送了一次请求”误当成“任务完成”。好的监控必须让信号跟任务结果一致,还要区分备份失败和通知网络暂时失败。

一个检查项对应一个明确任务

先为任务写一句可以判断对错的话,例如:“每天凌晨两点生成 PostgreSQL 备份,上传到远端,上传后的文件大小与本地一致。”这比“数据库正常”更适合建立检查项。

数据库导出、媒体归档和日志清理如果有不同执行周期,应分别监控。几台服务器共享一个检查地址,也会让其中一台的成功掩盖另一台长期没运行的问题。任务更名或迁移时,记录新旧机器和检查项的对应关系,再停掉旧任务。

可以从托管服务开始,也可以按照 官方自托管说明部署。监控与任务全部放在同一台机器上,机器停机时两边一起消失,监控端也无法发送通知。自托管时应放到独立故障范围,并为监控自身设置外部检查。

检查地址里的 UUID 相当于发信凭据。知道地址的人可以伪造正常信号,导致任务故障被遮住。它应保存在服务器受限配置里,不能写进公开仓库。下面的 your-uuid 是占位符。

周期与 cron 时间不要混用

“每六小时执行一次”和“每天凌晨两点执行”看似都能计算间隔,但故障判断不同。周期型任务适合用 Period 和 Grace Time;固定日历时间的任务适合 cron 调度,并明确时区。

服务器使用 UTC,监控选择 Asia/Shanghai,任务又在容器里按第三个时区运行,就可能每天固定出现一次误报。配置后将下一次预期运行时间与服务器上的实际计划对照,而不是只看 cron 表达式字符一致。夏令时地区还需要考虑本地时间跳变。

宽限时间要包含正常波动,又不能长到故障失去意义。平常任务十分钟完成,偶尔二十分钟,不宜设成一天;但设成十一分钟又可能频繁报警。先观察正常时长和数据量增长,再留出可以解释的余量。使用开始信号时,宽限时间还用于判断任务从开始到结束是否耗时过长。具体调度规则见 官方检查配置说明。

开始、成功和失败分别发送

一次任务可以先请求 /start,结束成功请求基础地址,失败请求 /fail。这样可以区分“完全没有启动”和“已经启动但一直没完成”。运行时长文档与失败信号文档解释了这些路径。

下面是一份 Bash 包装脚本。它假定真正的备份程序位于 /usr/local/sbin/backup-database,该程序完成导出、验证和上传后才返回 0。先替换任务路径和检查地址,再放进定时任务:

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

HC_URL="${HC_URL:?set HC_URL before running}"

ping_check() {
  curl --fail --silent --show-error \
    --max-time 10 --retry 2 \
    "$HC_URL$1" >/dev/null
}

report_exit() {
  status=$?
  trap - EXIT
  if [ "$status" -eq 0 ]; then
    suffix=""
  else
    suffix="/fail"
  fi
  if ! ping_check "$suffix"; then
    printf 'healthchecks notification failed; job status=%s\n' "$status" >&2
  fi
  exit "$status"
}
trap report_exit EXIT

if ! ping_check "/start"; then
  printf 'healthchecks start notification failed\n' >&2
fi

/usr/local/sbin/backup-database
job_status=$?
exit "$job_status"

退出处理一开始就保存 $?,避免后续日志或 curl 覆盖任务结果。开始信号失败不会阻止备份;结束信号失败会写日志,但也不会将已经成功的备份伪装成失败。反过来,任务返回非零时,即使失败信号发送成功,包装脚本仍返回任务的原始状态。

这个示例没有依赖 set -e 自动退出,真正任务的状态被明确保存。复杂脚本使用 set -e 时,要理解条件表达式、管道和子 shell 的行为,不能只加一行选项就认为所有失败都能捕获。

信号发送设置了单次超时和有限重试,总时间仍可能超过十秒。对严格时限的任务,应把通知开销计入调度与宽限时间。不要为了确保信号送达,给失败通知设置无限重试。

管道最后成功,不代表备份成功

常见导出方式是:

pg_dump appdb | gzip > appdb.sql.gz

若 pg_dump 失败而 gzip 正常结束,默认管道状态可能仍是成功。备份文件存在,却没有完整数据。任务脚本应使用 Bash 的 pipefail,并在结束前检查预期产物:

#!/usr/bin/env bash
set -euo pipefail
umask 077

backup_dir="/srv/backups/database"
mkdir -p "$backup_dir"
backup_file="$backup_dir/appdb-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
tmp_file="$backup_file.partial"
trap 'rm -f "$tmp_file"' EXIT

pg_dump appdb | gzip > "$tmp_file"
test -s "$tmp_file"
gzip -t "$tmp_file"
mv "$tmp_file" "$backup_file"

这里的数据库连接由运行环境提供。非空与 gzip 校验只能检查文件层面,不能证明 SQL 可恢复,也没有包含远端上传。把这个片段用作备份程序时,还要加入自己的上传步骤,验证上传结果,然后才退出成功。数据库密码不要放进命令行示例或 cron 日志。

使用 .partial 后缀能减少半成品被同步或清理脚本误认成完整备份的机会。完成后再重命名;如果有跨文件系统移动,应重新理解移动过程,不能套用同一文件系统上的原子重命名假设。

真正的备份验收仍是隔离恢复:导入到独立数据库,确认主要表和应用能读取。Healthchecks 收到信号只说明脚本走到了你定义的成功位置。成功定义太弱,监控就会忠实记录一个不可靠的结果。

定时任务里的环境比终端少

手工执行成功、cron 执行失败,常见原因是工作目录、PATH、账户和权限不同。脚本中使用必要的绝对路径;配置文件由执行账户可读;需要的环境显式加载;日志写到可写目录。不要依赖交互式 shell 自动加载的工具路径或当前目录。

例如由受限的根账户配置文件提供检查地址时,可用包装脚本加载配置:

#!/usr/bin/env bash
set -euo pipefail
source /etc/backup-healthchecks.env
export HC_URL
exec /usr/local/sbin/backup-with-healthchecks

这个文件是可执行的 shell 配置,只能由可信维护者写入,不能拿用户提交的内容直接 source。权限、运行用户与备份访问范围应一起安排。

如果一次任务可能超过下一次调度间隔,应先解决并发运行:采用适合系统的锁机制,或者调整计划。不要让两个备份进程相互覆盖输出,然后只在监控端猜哪一次结束。需要关联开始与结束时,官方支持运行标识 rid,但任务与主机的清晰拆分仍然有价值。

告警要测试到收件人那里

配置一个通知渠道后,先确认渠道测试能够抵达,再测试真实检查项。可以为演练建立单独的短周期任务,依次验证正常完成、显式失败、开始后不结束、完全不发送信号。每一种都应该有可以理解的通知,不要在生产备份上用删文件来制造失败。

还有一种需要刻意测试的情况:备份成功,但监控地址暂时不可达。脚本应保留任务成功状态,同时留下通知失败日志;监控端可能随后因缺失信号报警。收到这类报警先查真实任务和产物,不要自动重新执行具有副作用的任务。

报警信息写清服务器、任务用途和日志位置,比单纯“Down”更能缩短处理时间。恢复通知也要确认正常,避免维护者收到失败后一直不知道是否恢复。

一次漏跑告警,可以按这条路径排查

先看监控端最后一次成功、开始信号与报警时间。如果有开始信号却没有结束,去任务主机查进程和日志,区分还在执行、卡住和已经异常退出。完全没有开始信号,则先查调度器是否运行、计划是否启用、服务器时钟是否正确,以及该账户是否有执行权限。

随后检查真实产物。导出文件存在时,确认时间、大小与校验结果,再看上传日志和远端对象。文件完整而信号缺失,可能是通知地址或网络路径的问题;没有文件,则不能凭一条 curl 请求成功判断任务没事。

若日志显示成功,仍要查这个“成功”是谁打印的。脚本可能先输出完成消息,再执行最后的上传,也可能把多个步骤放进后台后立刻退出。监控包装程序只认识退出状态,不能自动识别这种语义错误。把完成日志放在最后,并等待所有属于任务成功条件的步骤结束。

排查期间不要连续手工触发相同任务。备份还在运行时再开一个进程,可能争抢磁盘和网络;清理、扣费或发送邮件类任务则可能重复执行实际操作。先确认旧进程状态,再决定补跑或等待。

处理结束后,在内部记录里写清失败原因和修复动作。若只是手动补跑成功,却没有修复下一次计划,第二天仍会漏跑。应保留到下一次自动执行完成的跟进,而不只在报警消失时结束排查。

迁移和维护时把监控一起带走

迁移服务器时,先在新机器上手动执行一次,检查产物与信号,再验证新调度会自动运行。旧机器的任务停掉后,监控端仍应只接受预期来源的工作。不能用一次手动成功替代次日自动执行的验证。

长期暂停的任务应在监控端同步暂停或调整,避免大量已知告警淹没真正故障。升级脚本、变更数据库账户、调整备份存储时,都应重新确认成功条件还成立。

维护一段时间后,值得定期翻看任务时长:持续增长可能来自数据量、网络传输或锁等待。它不是完整性能诊断,但能提示你在宽限时间被耗尽前检查原因。一个可解释的检查项,会让“昨天还在运行”变成可追溯的执行记录,也让漏跑不必等到恢复当天才被发现。