定时任务悄悄失败最危险:cron 与 systemd timer 的失败感知设计
定时任务最怕的不是报错,而是根本不知道它失败了。备份没跑、报表没出、对账没做,往往要等业务方来问才发现。本文用一个可落地的思路讲清楚:怎样让 cron 和 systemd timer 的失败“看得见、可追踪、能告警”,而不是只看一句“任务已配置”。
一、为什么定时任务会悄悄失败
常见原因可以归成四类,先对号入座,再谈工具:
- 输出被丢弃:cron 默认把输出发邮件,本地没有邮件服务时等于石沉大海;命令加了
>/dev/null 2>&1,连最后的线索也没了。 - 环境不一致:手动能跑、定时跑不起来,多半是 PATH、当前目录、工作目录、环境变量、Shell 解释器不同。cron 的环境比交互 Shell 精简得多。
- 退出码没人看:脚本中间某一步失败,后续步骤照常跑,最后退出码还是 0;或者管道的退出码只反映最后一个命令。
- 重叠与静默跳过:上一次任务还没跑完,下一次又启动;有人加了锁却没记录“本次被跳过”,事后根本无法区分“没跑”和“跑了但没产出”。
判断标准很简单:一次定时任务结束时,你至少要能回答三个问题——它跑了吗?成功还是失败?失败时留下了什么证据?
二、给 cron 加三层保险
1. 先让输出和退出码落盘
不要一上来就重定向到 /dev/null。推荐的 crontab 写法是把标准输出和标准错误分开或合并到带日期的日志里,同时在脚本内部打时间戳:
# 每天 02:30 跑备份脚本,日志按天归档
30 2 * * * /usr/local/bin/backup.sh >>/var/log/backup.log 2>&1
脚本开头建议写死关键环境,并用严格模式拦截中途失败:
#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /srv/app || exit 1
echo "[$(date '+%F %T')] backup start"
# 这里放真正的备份步骤,任何一步失败都会中断并返回非零退出码
echo "[$(date '+%F %T')] backup done"
set -euo pipefail 的含义是:命令失败即退出、未定义变量即报错、管道中任一命令失败都算失败。它不是万能药,但能把“假成功”挡掉一大半。
2. 用锁防止重叠,用日志记录跳过
长时间任务一定要加锁,否则并发跑起来可能互相覆盖输出、重复扣账或重复发通知。flock 是最省事的做法:
# 同一时刻只允许一个实例;拿不到锁就记录并退出,而不是悄悄丢弃
30 2 * * * /usr/bin/flock -n /var/lock/backup.lock /usr/local/bin/backup.sh >>/var/log/backup.log 2>&1 || echo "[$(date '+%F %T')] backup skipped: locked" >>/var/log/backup.log
注意:锁文件本身也要纳入巡检,异常退出后残留的锁、权限不对的锁目录,都会让后续任务长期被跳过。
3. 失败才告警,成功留心跳
只看日志还不够,关键任务需要两条线:
- 失败告警:脚本返回非零时触发通知,通知里至少带任务名、时间、退出码和日志尾部 20 行,不要只发一句“任务失败了”。
- 成功心跳:每天应产出的文件、行数、记录数要做断言。例如备份文件不存在、大小为 0、报表行数为 0,都应视为失败,哪怕脚本退出码是 0。
一个最小可用的包装思路是:
#!/bin/bash
set -euo pipefail
/usr/local/bin/backup.sh
test -s /data/backup/latest.tar.gz || { echo "backup file missing or empty" >&2; exit 1; }
把“产出检查”写进任务本身,比事后翻日志可靠得多。
三、systemd timer:把状态交给系统管理
如果机器已经用 systemd,timer 比 cron 更适合需要“可观测、可重试、可补跑”的任务。核心是两个文件:一个 service 描述做什么,一个 timer 描述何时做。
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup job
OnFailure=backup-alert.service
[Service]
Type=oneshot
WorkingDirectory=/srv/app
ExecStart=/usr/local/bin/backup.sh
Restart=no
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=daily
Persistent=true
AccuracySec=1min
Unit=backup.service
[Install]
WantedBy=timers.target
这几个字段值得单独说明:
Persistent=true:机器关机错过了执行时间,开机后会补跑一次,避免“停机一天就少一次备份”。OnFailure:服务失败时自动触发另一个告警服务,不用在脚本里到处写通知逻辑。StandardOutput=journal:输出进 journal,可以按服务名和时间过滤,比散落在各处的日志文件好查。
启用与巡检命令:
systemctl enable --now backup.timer
systemctl list-timers --all | grep backup
journalctl -u backup.service --since "today" -p err
systemctl show backup.service -p Result,ExecMainStatus
list-timers 能直接看到下次执行时间和上次执行结果;journalctl -p err 只看错误级别,避免在海量日志里大海捞针。
四、cron 与 systemd timer 怎么选
| 维度 | cron | systemd timer |
|---|---|---|
| 上手成本 | 低,一行搞定 | 稍高,需要两个单元文件 |
| 错过补跑 | 默认不补跑 | Persistent=true 可补跑 |
| 失败感知 | 靠脚本和日志自己搭 | journal、OnFailure、状态查询更完整 |
| 重试控制 | 需脚本或包装器实现 | Restart、RestartSec 等字段原生支持 |
| 适用场景 | 简单、短时、低频任务 | 关键任务、长任务、需要审计的任务 |
选型建议:临时、一次性的轻任务用 cron;备份、对账、报表、同步这类“失败代价高”的任务,优先用 timer,并把失败告警当作任务的一部分交付,而不是上线后再补。
五、上线前自查清单
发布定时任务前,逐项打勾,缺一项都不要算完成:
- 手动跑一次,定时跑一次,两次结果一致。
- 故意让它失败一次,确认告警能收到、日志里有退出码和关键上下文。
- 故意让它超时或重叠一次,确认锁和跳过逻辑符合预期。
- 确认日志路径、轮转策略和磁盘空间,不让日志本身成为新的故障点。
- 在文档里写清任务用途、负责人、产出位置和恢复步骤,新人接手时不用重新猜。
结语
定时任务的可靠性,不取决于你写了多少行脚本,而取决于失败时系统能不能第一时间告诉你。先做到“跑没跑看得见、失败有证据、关键产出有断言”,再谈调度和优化,这才是对线上任务最划算的投入。
- 点赞
- 收藏
- 关注作者
评论(0)