定时任务的错误处理:静默失败最可怕

凌晨 3:00 的 cron 已执行,但早上 9:00 目标文件仍未生成,这类“静默失败”比直接报错更难处理。

定时任务和手动执行不是同一个环境:路径可能不同,权限可能不同,环境变量可能缺失,网络也可能超时。终端里能跑通,不代表定时执行也能跑通。

真正要防的,不是报错,是“看起来没问题”

常见的静默失败包括:

  • 命令失败了,但脚本继续执行
  • 输出被重定向后无人查看
  • 请求超时了,但脚本仍返回 0
  • 新结果没写成功,却保留了旧文件

定时任务至少要做到三件事:

  1. 有退出码
  2. 有日志
  3. 有通知或可检查状态

第一层:脚本先学会正确失败

Shell 脚本通常先补这几行:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env bash
set -Eeuo pipefail

LOG_FILE="/var/log/job-example.log"

exec >>"$LOG_FILE" 2>&1

echo "[$(date '+%F %T')] job start"

python3 /path/to/task.py

echo "[$(date '+%F %T')] job done"

作用分别是:

  • set -e:命令失败就退出
  • set -u:使用未定义变量时退出
  • pipefail:管道中前序命令失败时整体失败
  • 2>&1:把错误输出也写入日志

重点不是“记录过程”,而是失败时不要继续往下执行。

如果是 Python,也要保证异常会导致非 0 退出码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import logging
import sys

logging.basicConfig(
filename="/var/log/job-example.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)

def main():
logging.info("job start")
# do work
logging.info("job done")

if __name__ == "__main__":
try:
main()
except Exception:
logging.exception("job failed")
sys.exit(1)

调度器判断任务是否成功,首先看的就是退出码。

第二层:日志别只记“开始了”

只有一句日志时,排查价值很低:

1
task started

更有用的日志应至少包含:

  • 任务名
  • 开始时间、结束时间
  • 处理数量
  • 关键输入
  • 失败步骤
  • 退出码

例如:

1
2
3
echo "[$(date '+%F %T')] sync start source=/data/input.csv"
echo "[$(date '+%F %T')] sync processed rows=128"
echo "[$(date '+%F %T')] sync failed step=upload exit_code=1"

如果 rows=128 这类数字来自示例,实际效果仍需按业务场景验证。

日志不要只输出到终端。定时任务通常无人值守,日志应落文件或进入系统日志。Linux 下可以直接用:

1
logger -t myjob "sync start"

第三层:给失败留一个最小通知

只有日志还不够,因为没人保证会主动查看。

最低成本的方式,是失败时发送通知。通知渠道可以是邮件、Webhook 或消息机器人,重点是失败后能被看到。

示例:

1
2
3
4
5
6
#!/usr/bin/env bash
set -Eeuo pipefail

trap 'curl -fsS -X POST https://example.invalid/webhook -d "job=backup&status=failed&time=$(date +%F_%T)"' ERR

/path/to/backup.sh

这里的 trap ERR 表示:脚本中任一步骤出错,就触发通知。

如果不想在每次失败时立刻发消息,也可以做“心跳”:任务成功后更新时间戳,由监控检查是否超时未更新。这种方式对“脚本卡住但未退出”的情况更有用。

第四层:别直接覆盖结果

静默失败还常见于输出文件被部分写入,结果已损坏,但文件名看起来正常。

更稳妥的做法是先写临时文件,验证通过后再替换:

1
2
3
4
5
6
tmp_file="/data/output.tmp"
final_file="/data/output.json"

generate_data > "$tmp_file"
test -s "$tmp_file"
mv "$tmp_file" "$final_file"

这样即使中途失败,也不会直接破坏旧结果。

第五层:把运行环境写死一点

定时任务的高频问题之一,是环境不一致。

常见处理方式:

  • 命令使用绝对路径
  • 脚本开头显式 cd 到工作目录
  • 必要环境变量在脚本中写明
  • 固定依赖版本,不依赖系统里“刚好有”

例如:

1
2
cd /srv/myjob
/usr/bin/python3 /srv/myjob/task.py

调度器只负责按时执行,不会补全你平时手动运行时的上下文。

一个够用的检查清单

改造定时任务时,先检查这 6 项:

  • 失败时是否返回非 0
  • 标准输出和错误输出是否落日志
  • 日志里是否有关键参数和处理结果
  • 失败后是否有通知
  • 输出是否先写临时文件再替换
  • 路径、目录、环境变量是否显式指定

如果你准备补一项,优先补“失败告警入口”。至少要做到任务出错后,能明确知道是谁、通过什么渠道、在什么时候收到通知。


定时任务的错误处理:静默失败最可怕
https://ghost.kasumi.live/2026/03/20/定时任务的错误处理:静默失败最可怕/
作者
Amadeus
发布于
2026年3月20日
许可协议