香港服务器Crontab完全指南:定时任务设计 + 执行监控 + 常见故障排查实战

香港服务器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 监控,是发现问题的最有效手段,远比事后查日志省力。

Telegram