结论: 服务器上设置定时任务首选 crontab:用 crontab -e 编辑当前用户的任务表,按"分 时 日 月 周"五个字段写调度表达式,命令一律用绝对路径并显式重定向输出。需要开机执行、依赖网络或要求可观测性时,改用 systemd timer 更可靠。两者都要用 flock 防止任务重叠执行。
引言:定时任务是服务器自动化的地基
定时任务的典型场景包括:数据库每日备份、日志按天切割、证书自动续期、缓存预热、监控脚本轮询、业务报表生成。它看起来简单,但线上出问题时往往最隐蔽——脚本手动执行好好的,放进 crontab 就失败;备份任务跑了两次互相覆盖;服务器重启后任务再也没执行过。
本文把 crontab 的语法、环境陷阱、日志排查讲透,并给出 systemd timer 的对照方案。示例基于 Ubuntu 24.04 LTS(cron 3.0pl1)与 systemd 255(截至 2026 年主流版本)。
crontab 五个字段的含义
结论:crontab 的调度表达式由 5 个时间字段加 1 条命令组成,理解每个字段的取值范围是写对表达式的前提。
# ┌───────── 分钟 (0 - 59)
# │ ┌─────── 小时 (0 - 23)
# │ │ ┌───── 日 (1 - 31)
# │ │ │ ┌─── 月 (1 - 12 或 jan-dec)
# │ │ │ │ ┌─ 星期 (0 - 7,0 和 7 都代表周日,或用 sun-sat)
# │ │ │ │ │
# * * * * * 要执行的命令| 字段 | 含义 | 取值范围 | 常用写法示例 |
|---|---|---|---|
| 第 1 位 | 分钟 | 0~59 | */5 每 5 分钟;0,30 每半点 |
| 第 2 位 | 小时 | 0~23 | 0 整点;9-18 9 点到 18 点 |
| 第 3 位 | 日 | 1~31 | 1 每月 1 号;*/2 每隔两天 |
| 第 4 位 | 月 | 1~12 或 jan~dec | * 每月;1,4,7,10 每季度首月 |
| 第 5 位 | 星期 | 0~7(0 与 7 均为周日) | 1-5 工作日;0,6 周末 |
特殊字符与快捷语法:
| 写法 | 含义 | 示例 | 说明 |
|---|---|---|---|
* | 任意值 | * | 每分钟执行一次 |
, | 值列表 | 0,15,30,45 | 每 15 分钟 |
- | 范围 | 0 9-18 1-5 | 工作日 9~18 点整点 |
/ | 步进 | /10 * | 每 10 分钟 |
@reboot | 开机运行一次 | @reboot /opt/init.sh | 系统启动时执行 |
@hourly | 每小时 | @hourly /opt/job.sh | 等价于 0 |
@daily | 每天 00:00 | @daily /opt/backup.sh | 等价于 0 0 * |
@weekly | 每周日 00:00 | @weekly /opt/clean.sh | 等价于 0 0 0 |
@monthly | 每月 1 号 00:00 | @monthly /opt/report.sh | 等价于 0 0 1 |
第一步:crontab 基本操作
结论:crontab 分"用户级"与"系统级"两类。日常脚本放用户级(尤其是专用服务账号),需要指定执行用户的放 /etc/cron.d/。
crontab -e # 编辑当前用户的任务表(首次会让你选编辑器)
crontab -l # 列出当前任务
crontab -r # 删除全部任务(无二次确认,慎用!)
crontab -i -r # 带确认地删除
crontab -u www-data -e # 编辑指定用户的任务(需 root)
crontab -u root -l
sudo systemctl status cron # Debian/Ubuntu 服务名是 cron
sudo systemctl status crond # RHEL 系服务名是 crond一份典型的用户级 crontab:
# 显式声明环境,避免 PATH 陷阱(见第四步)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
# 每 5 分钟检查一次服务存活
*/5 * * * * /usr/local/bin/check-alive.sh >> /var/log/check-alive.log 2>&1
# 每天 03:30 做数据库备份
30 3 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1
# 每周一 04:00 清理 7 天前的日志
0 4 * * 1 /usr/bin/find /var/log/app -name '*.log' -mtime +7 -delete
# 开机后拉起业务脚本
@reboot sleep 30 && /opt/app/start.sh >> /var/log/app-start.log 2>&1第二步:系统级 cron 与目录型任务
结论:需要为不同用户分派任务、或希望用文件管理(便于纳入 Git 与配置下发)时,用 /etc/cron.d/。注意这里的格式多一个用户字段。
sudo tee /etc/cron.d/app-jobs <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 分 时 日 月 周 用户 命令
*/5 * * * * www-data /usr/bin/php /var/www/nextcloud/cron.php > /dev/null 2>&1
0 2 * * * root /usr/local/bin/certbot renew --quiet
EOF
sudo chmod 0644 /etc/cron.d/app-jobs # 权限必须是 644,否则不生效
sudo chown root:root /etc/cron.d/app-jobs另外还有四个按周期执行的目录,把可执行脚本丢进去即可(由 run-parts 调用):
/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/
目录型任务的优点是零配置,缺点是无法精确到具体分钟,且脚本必须去掉扩展名(不能有 .sh)、具备可执行权限、且不以 . 开头。
第三步:环境变量陷阱(cron 最经典的坑)
结论:cron 执行环境的 PATH 只有 /usr/bin:/bin,没有登录 Shell 的环境变量、没有 ~/.bashrc、当前目录是用户的 home。脚本手动能跑、cron 里失败,九成是这个原因。
常见表现与处理:
command not found—— 命令不在精简 PATH 中。解决:写绝对路径(/usr/local/bin/node),或在 crontab 顶部声明PATH=...。python: command not found但手动能跑 —— 你用的是 pyenv/conda/venv 里的解释器。解决:脚本里写完整路径,如/opt/venv/bin/python。- 中文乱码或
locale报错 —— 加LANG=zh_CN.UTF-8或LC_ALL=C。 - 脚本依赖当前目录 —— cron 的工作目录是 HOME,脚本里用
cd /path && ...显式切换。 docker: command not found或权限不足 —— 用/usr/bin/docker绝对路径,并确认用户在 docker 组。
调试技巧:先把当前环境导出对比,再在 crontab 中补齐:
# 查看 cron 实际拿到的环境(临时任务)
* * * * * env > /tmp/cron-env.txt 2>&1
# 对比你的登录环境
env | sort > /tmp/shell-env.txt
diff /tmp/shell-env.txt /tmp/cron-env.txt另外两个语法细节:
- 命令中的
%是换行符,必须转义为\%,否则后面的内容会被当作标准输入。写日期时尤其容易踩:date +\%Y\%m\%d。 - 输出默认会通过邮件发给任务所属用户,未配置 MTA 时会堆在
/var/spool/mail/。生产任务一律显式重定向到日志文件,不关心的加> /dev/null 2>&1。
第四步:日志与排错
结论:cron 本身只记录"有没有执行",不记录脚本的输出。要定位问题,先看 cron 日志确认触发,再看脚本自己的日志看报错。
# Debian / Ubuntu:cron 日志混在 syslog 里
sudo grep CRON /var/log/syslog | tail -30
# RHEL 系
sudo tail -f /var/log/cron
# 用 journalctl 查看(systemd 系统通用)
sudo journalctl -u cron --since "2 hours ago"
sudo journalctl -u cron -f
# 确认服务在跑、配置被加载
sudo systemctl status cron
sudo ls -l /var/spool/cron/crontabs/排查清单:
- cron 服务是否在运行(
systemctl status cron)。 - 任务是否出现在
crontab -l输出里(别改错用户)。 - cron 日志里有没有
CMD (...)记录 —— 有说明已触发,问题在脚本内部。 - 脚本是否有可执行权限、shebang 是否正确(
#!/bin/bash)。 - 脚本日志里是否有
command not found/Permission denied。 /etc/cron.allow//etc/cron.deny是否限制了用户。
第五步:systemd timer —— 更现代的替代方案
结论:cron 简单但"哑"——没有依赖管理、错过执行不会补跑、日志分散。systemd timer 支持依赖网络就绪、错过时间点后补执行、随机延迟打散,日志统一进 journald,是生产关键任务的更好选择。
创建服务单元 /etc/systemd/system/db-backup.service:
[Unit]
Description=Daily database backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
ExecStart=/usr/local/bin/db-backup.sh
StandardOutput=journal
StandardError=journal创建定时器单元 /etc/systemd/system/db-backup.timer:
[Unit]
Description=Run database backup every day at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=300
Persistent=true
AccuracySec=1min
Unit=db-backup.service
[Install]
WantedBy=timers.target启用与验证:
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
systemctl list-timers --all # 查看所有定时器及下次触发时间
systemctl status db-backup.timer
journalctl -u db-backup.service -f # 看任务输出
# 校验时间表达式是否符合预期(非常实用)
systemd-analyze calendar '*-*-* 03:30:00'
systemd-analyze calendar 'Mon *-*-* 04:00:00'cron 与 systemd timer 的对照:
| 对比项 | cron | systemd timer |
|---|---|---|
| 语法难度 | 简单 | 需要写两个单元文件 |
| 最小粒度 | 分钟 | 秒(AccuracySec 控制) |
| 错过补跑 | 不支持 | Persistent=true 支持 |
| 依赖管理 | 无 | 支持 After=network-online.target |
| 日志 | 需自行重定向 | 自动进 journald |
| 随机打散 | 需自己写 sleep | RandomizedDelaySec 内置 |
| 适用场景 | 简单周期脚本 | 关键业务、备份、需依赖的任务 |
企业QQ咨询




