外观
案例:定时任务不执行
现象
数据库备份脚本手工执行正常,但 crontab 里配了半个月,从来没生成过备份文件。
bash
crontab -ltext
0 3 * * * /opt/backup/backup.sh定位过程
第一步:cron 到底有没有触发
bash
grep backup /var/log/cron | tail -5text
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.logtext
(空)脚本里写了重定向,但文件是空的。说明脚本在执行第一行有效命令之前就退出了。
第三步:手动执行
bash
/opt/backup/backup.sh
echo $?text
0手动成功。差别在于环境。
第四步:对比环境
在 crontab 里临时加一条打印环境:
text
* * * * * env > /tmp/cron-env.txt 2>&1bash
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/shcron 的 PATH 只有 /usr/bin:/bin,没有 mysqldump 所在的 /usr/local/mysql/bin。
第五步:确认脚本里的第一处失败
bash
head -10 /opt/backup/backup.shbash
#!/bin/bash
set -euo pipefail
DATE=$(date +%F)
mysqldump -uroot -p"$DB_PASS" --all-databases | gzip > /data/backup/all-$DATE.sql.gzmysqldump: command not found → set -e 立即退出 → gzip 收到空输入 → 生成一个很小的文件(但这里连文件都没生成,因为管道失败被 pipefail 捕获)。
等等——/data/backup 里应该有文件?查一下:
bash
ls -lh /data/backup/ | tail -3text
-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根治
- 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- 脚本里用绝对路径 + 全量日志:
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))"- 加失败告警:
bash
0 3 * * * /opt/backup/backup.sh || echo "backup failed" | mail -s ALERT ops@example.com%的坑(如果脚本里直接写 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- 防重叠:
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"