香港服务器Crontab完全指南:定时任务设计 + 执行监控 + 常见故障排查实战
Crontab 是服务器上最常用但也最容易出问题的功能。很多运维同学的惨痛经历:备份脚本写进了 Crontab,以为每天都在执行,结果真正需要恢复数据时发现任务从未成功运行过。本文系统梳理 Crontab 的完整使用方法,重点解决「任务写了但没执行」的各种根因。
一、Cron 时间表达式语法
<code"> # Crontab 格式(五个时间字段 + 命令) # ┌──────── 分钟 (0-59) # │ ┌────── 小时 (0-23) # │ │ ┌──── 日期 (1-31) # │ │ │ ┌── 月份 (1-12) # │ │ │ │ ┌ 星期 (0-7, 0和7都是周日) # │ │ │ │ │ # * * * * * 命令 # 常用示例: 0 3 * * * # 每天凌晨 3:00 0 */4 * * * # 每4小时整点(0/4/8/12/16/20时) */5 * * * * # 每5分钟 0 3 * * 0 # 每周日凌晨 3:00 0 3 1 * * # 每月1日凌晨 3:00 0 3 1 1 * # 每年1月1日凌晨 3:00 0 9-18 * * 1-5 # 工作日9点到18点每整点 # 特殊字符: # * 任意值 # , 枚举(0,30 = 0分和30分) # - 范围(1-5 = 周一到周五) # / 步进(*/15 = 每15个单位) # @reboot 系统重启后执行一次 # @daily = 0 0 * * * # @weekly = 0 0 * * 0 # @monthly = 0 0 1 * *
二、Crontab 的运行环境陷阱(最常见的踩坑点)
Cron 的运行环境与交互式 Shell 不同,这是大量脚本「手动能跑、Cron 不执行」的根本原因:
<code"># ── 陷阱1:PATH 不同 ── # Cron 的 PATH 通常只有:/usr/bin:/bin # 你的 Shell 的 PATH 还包含:/usr/local/bin:/usr/sbin 等 # 错误写法(可能找不到命令): 0 3 * * * mysqldump -uroot -p... > /backup/db.sql # 正确写法(使用绝对路径): 0 3 * * * /usr/bin/mysqldump -uroot -p... > /backup/db.sql # 或者在 Crontab 顶部定义 PATH: PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 0 3 * * * mysqldump ... # 现在可以找到 mysqldump
<code"># ── 陷阱2:环境变量不继承 ── # ~/.bashrc 和 ~/.bash_profile 中定义的变量在 Cron 中不可用 # .env 文件也不会自动加载 # 错误写法($DB_PASSWORD 为空): 0 3 * * * mysqldump -uroot -p$DB_PASSWORD ... # 正确写法(在脚本中显式加载环境变量): #!/bin/bash source /etc/app/env # 或 export $(cat /etc/app/.env | xargs) mysqldump -uroot -p"$DB_PASSWORD" ...
<code"># ── 陷阱3:工作目录不是预期目录 ── # Cron 的工作目录通常是用户的 HOME 目录,不是脚本所在目录 # 正确写法(脚本开头切换到正确目录): #!/bin/bash cd /opt/myapp || exit 1 # 切换失败则退出,避免在错误目录执行 python manage.py cleanup
<code"># ── 陷阱4:% 号是换行符 ── # 在 Crontab 中,% 号会被解释为换行符,必须转义 # 错误写法(date +%Y%m%d 中的 % 会被解释错误): 0 3 * * * /usr/bin/mysqldump ... > /backup/db-$(date +%Y%m%d).sql # 正确写法(转义 % 号或使用脚本): 0 3 * * * /usr/bin/mysqldump ... > /backup/db-$(date +\%Y\%m\%d).sql # 或者将命令放进脚本文件,规避 % 问题(最推荐)
三、正确的 Crontab 配置规范
<code"># 使用 /etc/cron.d/ 目录管理任务(推荐,比 crontab -e 更易管理) # /etc/cron.d/myapp-tasks # 格式:时间表达式 用户名 命令 PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin SHELL=/bin/bash MAILTO="" # 禁止发送执行结果邮件到本地(避免邮件队列堆积) # 数据库备份(每天 3:00,root 用户) 0 3 * * * root /opt/backup/restic-backup.sh >> /var/log/backup.log 2>&1 # 日志清理(每周日 4:00) 0 4 * * 0 root /opt/scripts/cleanup-logs.sh >> /var/log/cleanup.log 2>&1 # SSL 证书续期检查(每周一 5:00) 0 5 * * 1 root certbot renew --quiet --post-hook "systemctl reload nginx" # 应用健康检查(每5分钟) */5 * * * * appuser /opt/scripts/health-check.sh # 注意:文件末尾必须有空行,否则最后一个任务可能不被执行
四、Healthchecks.io 监控定时任务是否执行
Crontab 失败时默认无任何通知——任务没跑,你毫不知情。Healthchecks.io 提供「死亡开关」监控:任务执行后主动 ping 一个 URL,如果超过指定时间没有收到 ping,立即发送告警。
<code">
配置流程(以数据库备份任务为例):
1. 注册 Healthchecks.io(免费计划支持 20 个监控)
2. 创建监控:Projects → Add Check
- Name: 数据库日备份
- Schedule: 0 3 * * *(与 Cron 相同)
- Grace time: 30 minutes(超过30分钟未ping视为失败)
3. 复制 Ping URL(形如:https://hc-ping.com/uuid-xxxxx)
4. 在备份脚本末尾添加 ping:
# /opt/backup/restic-backup.sh 末尾追加:
# 备份成功后 ping Healthchecks
if [ $? -eq 0 ]; then
curl -fsS "https://hc-ping.com/uuid-xxxxx" > /dev/null
else
curl -fsS "https://hc-ping.com/uuid-xxxxx/fail" > /dev/null
fi
# 5. 配置告警渠道(Email/Slack/飞书)
# Settings → Integrations → 添加飞书 Webhook
<code"># 也可以直接在 Crontab 中包裹命令(更简洁) # /etc/cron.d/myapp-tasks # 包裹模式:命令成功则 ping,失败则 ping/fail 0 3 * * * root curl -fsS "https://hc-ping.com/uuid-xxxxx/start" && \ /opt/backup/restic-backup.sh && \ curl -fsS "https://hc-ping.com/uuid-xxxxx" || \ curl -fsS "https://hc-ping.com/uuid-xxxxx/fail"
五、查看 Cron 执行日志
<code"># Ubuntu/Debian 查看 Cron 日志 grep CRON /var/log/syslog | tail -50 # 或者通过 journalctl journalctl -u cron -f # 实时查看 journalctl -u cron --since "1 hour ago" # 查看特定任务的执行记录 grep "backup.sh" /var/log/syslog # 输出示例: # Jul 28 03:00:01 hk-web CRON[12345]: (root) CMD (/opt/backup/restic-backup.sh >> /var/log/backup.log 2>&1) # 这说明任务被启动了,如果后续没有执行完成记录,去看 backup.log
六、systemd Timer(现代替代方案)
<code"># systemd Timer 比 Crontab 的优势: # 1. 任务失败时 systemd 自动重启 # 2. 日志通过 journalctl 统一管理 # 3. 精确到秒的调度 # 4. 可以设置依赖(在某个服务启动后才运行) # 示例:将数据库备份转换为 systemd Timer # 步骤1:创建 Service 文件 cat > /etc/systemd/system/db-backup.service << 'EOF' [Unit] Description=数据库每日备份 After=network.target mysql.service [Service] Type=oneshot ExecStart=/opt/backup/restic-backup.sh StandardOutput=journal StandardError=journal User=root EOF # 步骤2:创建 Timer 文件 cat > /etc/systemd/system/db-backup.timer << 'EOF' [Unit] Description=每天凌晨3点执行数据库备份 Requires=db-backup.service [Timer] OnCalendar=*-*-* 03:00:00 # 每天 3:00 AccuracySec=1min # 精度1分钟 Persistent=true # 服务器重启后补跑错过的任务 [Install] WantedBy=timers.target EOF # 步骤3:启用 Timer systemctl daemon-reload systemctl enable --now db-backup.timer # 查看所有 Timer 状态 systemctl list-timers --all # 手动触发(测试) systemctl start db-backup.service journalctl -u db-backup.service -f
七、常见故障排查速查表
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 任务从未执行 | Cron 服务未运行 | systemctl status cron |
| 命令找不到 | PATH 问题 | 脚本中 which 命令名 |
| 权限错误 | 文件权限或用户问题 | ls -la /path/to/script |
| 环境变量为空 | 未 source .env | 脚本中 echo $变量名 输出到日志 |
| 任务执行但无效果 | 工作目录错误 | 脚本中 echo "PWD: $(pwd)" |
| 邮件堆积 | 未设置 MAILTO=”” | ls -lh /var/mail/root |
| % 符号导致报错 | date 格式中的 % | 将 % 改为 \% |
| 最后一行任务不执行 | 文件末尾没有空行 | 在文件末尾加一个空行 |
八、Crontab 调试技巧
<code"># 技巧1:先用 sleep 测试 Cron 是否能运行 * * * * * root sleep 5 && echo "Cron works $(date)" >> /tmp/cron-test.log # 技巧2:将脚本输出完整记录到日志(含 stdout 和 stderr) 0 3 * * * root /opt/backup/backup.sh >> /var/log/backup.log 2>&1 # ^^^^ # 2>&1 将 stderr 也重定向到日志 # 技巧3:使用 flock 防止任务重复运行(上一次未完成时跳过) 0 * * * * root flock -n /tmp/hourly-task.lock /opt/scripts/hourly-task.sh # 技巧4:超时控制(最多运行 10 分钟) */5 * * * * root timeout 10m /opt/scripts/quick-task.sh
九、总结
Crontab 看似简单,实则暗坑密布。本文核心要点:用绝对路径 → 脚本中 source 环境变量 → 重定向输出到日志 → Healthchecks.io 监控是否执行 → 设置 MAILTO=”” 避免邮件堆积。将重要定时任务(备份、证书续期、数据清理)都接入 Healthchecks 监控,是发现问题的最有效手段,远比事后查日志省力。