定时任务悄悄失败最危险:cron 与 systemd timer 的失败感知设计

举报
yd_299492234 发表于 2026/10/29 08:54:16 2026/10/29
【摘要】 定时任务失败最危险的不是报错,而是无人知晓。本文从输出丢弃、环境差异、退出码失真和任务重叠四类静默失败讲起,给出 cron 的日志、锁与产出断言做法,以及 systemd timer 的补跑、失败告警与巡检方法,并附上线前自查清单。

定时任务最怕的不是报错,而是根本不知道它失败了。备份没跑、报表没出、对账没做,往往要等业务方来问才发现。本文用一个可落地的思路讲清楚:怎样让 cron 和 systemd timer 的失败“看得见、可追踪、能告警”,而不是只看一句“任务已配置”。

一、为什么定时任务会悄悄失败

常见原因可以归成四类,先对号入座,再谈工具:

  1. 输出被丢弃:cron 默认把输出发邮件,本地没有邮件服务时等于石沉大海;命令加了 >/dev/null 2>&1,连最后的线索也没了。
  2. 环境不一致:手动能跑、定时跑不起来,多半是 PATH、当前目录、工作目录、环境变量、Shell 解释器不同。cron 的环境比交互 Shell 精简得多。
  3. 退出码没人看:脚本中间某一步失败,后续步骤照常跑,最后退出码还是 0;或者管道的退出码只反映最后一个命令。
  4. 重叠与静默跳过:上一次任务还没跑完,下一次又启动;有人加了锁却没记录“本次被跳过”,事后根本无法区分“没跑”和“跑了但没产出”。

判断标准很简单:一次定时任务结束时,你至少要能回答三个问题——它跑了吗?成功还是失败?失败时留下了什么证据?

二、给 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,并把失败告警当作任务的一部分交付,而不是上线后再补。

五、上线前自查清单

发布定时任务前,逐项打勾,缺一项都不要算完成:

  1. 手动跑一次,定时跑一次,两次结果一致。
  2. 故意让它失败一次,确认告警能收到、日志里有退出码和关键上下文。
  3. 故意让它超时或重叠一次,确认锁和跳过逻辑符合预期。
  4. 确认日志路径、轮转策略和磁盘空间,不让日志本身成为新的故障点。
  5. 在文档里写清任务用途、负责人、产出位置和恢复步骤,新人接手时不用重新猜。

结语

定时任务的可靠性,不取决于你写了多少行脚本,而取决于失败时系统能不能第一时间告诉你。先做到“跑没跑看得见、失败有证据、关键产出有断言”,再谈调度和优化,这才是对线上任务最划算的投入。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。