Skip to content

案例:定时任务不执行 ​

现象 ​

数据库备份脚本手工执行正常,但 crontab 里配了半个月,从来没生成过备份文件。

bash
crontab -l
text
0 3 * * * /opt/backup/backup.sh

定位过程 ​

第一步:cron 到底有没有触发 ​

bash
grep backup /var/log/cron | tail -5
text
Oct 28 03:00:01 db-1 CROND[41200]: (root) CMD (/opt/backup/backup.sh)
Oct 27 03:00:01 db-1 CROND[39811]: (root) CMD (/opt/backup/backup.sh)

触发了,但没有 CMDOUT —— 说明脚本没往 stdout 写任何东西,或者很早就失败了。

第二步:看脚本自己的日志 ​

bash
tail -20 /var/log/backup.log
text
(空)

脚本里写了重定向,但文件是空的。说明脚本在执行第一行有效命令之前就退出了。

第三步:手动执行 ​

bash
/opt/backup/backup.sh
echo $?
text
0

手动成功。差别在于环境。

第四步:对比环境 ​

在 crontab 里临时加一条打印环境:

text
* * * * * env > /tmp/cron-env.txt 2>&1
bash
diff <(env | sort) <(sort /tmp/cron-env.txt)
diff
< JAVA_HOME=/usr/local/jdk-17
< PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/jdk-17/bin
---
> HOME=/root
> PATH=/usr/bin:/bin
> SHELL=/bin/sh

cron 的 PATH 只有 /usr/bin:/bin,没有 mysqldump 所在的 /usr/local/mysql/bin。

第五步:确认脚本里的第一处失败 ​

bash
head -10 /opt/backup/backup.sh
bash
#!/bin/bash
set -euo pipefail

DATE=$(date +%F)
mysqldump -uroot -p"$DB_PASS" --all-databases | gzip > /data/backup/all-$DATE.sql.gz

mysqldump: command not found → set -e 立即退出 → gzip 收到空输入 → 生成一个很小的文件(但这里连文件都没生成,因为管道失败被 pipefail 捕获)。

等等——/data/backup 里应该有文件?查一下:

bash
ls -lh /data/backup/ | tail -3
text
-rw-r--r-- 1 root root  20 Oct 28 03:00 all-2026-10-28.sql.gz

有文件,但只有 20 字节(gzip 空流的头尾)。这正是最危险的情况:看起来备份成功,实际是空的。

处置 ​

立即 ​

bash
# 1. 找出所有 20 字节的"假备份"
find /data/backup -name 'all-*.sql.gz' -size -1k -print

# 2. 删除并补一次真实备份
/opt/backup/backup.sh
ls -lh /data/backup/all-$(date +%F).sql.gz

根治 ​

  1. crontab 里显式设置环境:
text
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/usr/local/mysql/bin
JAVA_HOME=/usr/local/jdk-17
LANG=en_US.UTF-8
SHELL=/bin/bash

0 3 * * * /opt/backup/backup.sh >> /var/log/backup.log 2>&1
  1. 脚本里用绝对路径 + 全量日志:
bash
#!/usr/bin/env bash
set -euo pipefail
exec >> /var/log/backup.log 2>&1

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

MYSQLDUMP=/usr/local/mysql/bin/mysqldump
DST=/data/backup
FILE="$DST/all-$(date +%F).sql.gz"

"$MYSQLDUMP" -uroot -p"${DB_PASS:?DB_PASS not set}" \
  --single-transaction --routines --triggers --events \
  --all-databases | gzip > "$FILE"

# 关键:校验产物
SIZE=$(stat -c %s "$FILE")
if [[ "$SIZE" -lt 1048576 ]]; then
  echo "[$(date '+%F %T')] ERROR: backup too small: ${SIZE} bytes"
  exit 1
fi

echo "[$(date '+%F %T')] backup ok: $FILE ($(du -h "$FILE" | cut -f1))"
  1. 加失败告警:
bash
0 3 * * * /opt/backup/backup.sh || echo "backup failed" | mail -s ALERT ops@example.com
  1. % 的坑(如果脚本里直接写 cron):
text
# 错:%F 被当换行
0 3 * * * /opt/backup/backup.sh all-$(date +%F).sql.gz

# 对
0 3 * * * /opt/backup/backup.sh all-$(date +\%F).sql.gz
  1. 防重叠:
text
0 3 * * * flock -n /var/lock/backup.lock /opt/backup/backup.sh >> /var/log/backup.log 2>&1

复盘 ​

问题根因改进
任务"跑了"但脚本失败cron 的 PATH 没有 mysql 目录crontab 里设 PATH + 脚本用绝对路径
生成了 20 字节的空备份管道 + set -e 静默失败校验文件大小
半个月没发现没有成功/失败告警加产物校验 + 告警
只看 crontab 以为没问题不知道 cron 日志在哪监控 /var/log/cron

空备份比没有备份更危险

没有备份,你会小心。有一个"看起来正常"的空备份,你会放心,直到真正需要恢复的那天。

所有备份脚本必须校验:

  • 文件大小 > 阈值。
  • 能成功解压(gzip -t)。
  • 抽样能读出数据(zcat file | head)。
bash
# 每天检查所有备份产物
find /data/backup -name '*.gz' -mtime -1 -size -1M -print
gzip -t /data/backup/all-$(date +%F).sql.gz && echo "gzip ok"