外观
定时任务:cron 与 systemd timer
crontab 写起来简单,但环境变量、% 转义、任务重叠这三个坑几乎人人都踩过。复杂的定时任务用 systemd timer 更可控。
一、cron 基本用法
bash
crontab -l
crontab -e
crontab -r # 删除当前用户所有任务(危险,没有确认)
crontab -u app -l # root 查看别人的text
# 每分钟
* * * * * /opt/app/check.sh
# 每天 3:20
20 3 * * * /opt/app/backup.sh五个字段:
text
* * * * * command
│ │ │ │ └─ 星期 (0-7, 0 和 7 都是周日)
│ │ │ └──── 月 (1-12)
│ │ └─────── 日 (1-31)
│ └────────── 时 (0-23)
└───────────── 分 (0-59)常用写法:
| 表达式 | 含义 |
|---|---|
*/5 * * * * | 每 5 分钟 |
0 * * * * | 每小时整点 |
0 2 * * * | 每天 2:00 |
0 2 * * 1 | 每周一 2:00 |
0 2 1 * * | 每月 1 日 2:00 |
0 9-18 * * 1-5 | 工作日 9~18 点每小时 |
0 0,12 * * * | 每天 0 点和 12 点 |
0 3 1,15 * * | 每月 1 号和 15 号 |
快捷别名:
text
@reboot /opt/app/start.sh
@hourly /opt/app/hourly.sh
@daily /opt/app/daily.sh
@weekly /opt/app/weekly.sh
@monthly /opt/app/monthly.sh@reboot 在 crond 启动时跑,不等于系统完全就绪,依赖网络/数据库的任务要自己加重试。
二、三个必踩的坑
1. PATH 只有系统默认值
cron 的环境变量极简:PATH=/usr/bin:/bin,HOME 是用户目录,没有 .bashrc、没有 nvm、没有自定义 PATH。
bash
# 在 crontab 顶部显式设置
PATH=/usr/local/bin:/usr/bin:/bin:/opt/app/bin
JAVA_HOME=/usr/local/jdk-17
LANG=zh_CN.UTF-8交互式能跑、cron 跑不了
九成是这个原因。排查方法:在任务里先打印环境。
text
* * * * * env > /tmp/cron-env.txt 2>&1再对比 env 在终端里的输出。
2. % 要转义
cron 把未转义的 % 当作换行符。
text
# 错:date +%F 会被截断
0 3 * * * /opt/app/backup.sh $(date +%F)
# 对:加反斜杠
0 3 * * * /opt/app/backup.sh $(date +\%F)更省心的做法是把逻辑写进脚本,crontab 里只调用脚本,脚本内部怎么用 % 都行。
3. 输出不重定向会塞满邮箱
默认 cron 会把 stdout/stderr 通过邮件发给用户。没配 MTA 时可能堆积在 /var/spool/mail。
text
MAILTO=""或者显式重定向:
text
0 3 * * * /opt/app/backup.sh >> /var/log/backup.log 2>&1text
# 只保留错误输出(成功就静默)
0 3 * * * /opt/app/backup.sh >> /var/log/backup.log 2>&1三、日志在哪
bash
# CentOS / Rocky
tail -f /var/log/cron
# Ubuntu / Debian
journalctl -u cron -f
grep CRON /var/log/syslog | tailtext
Oct 27 21:50:01 kafka-1 CROND[41120]: (app) CMD (/opt/app/backup.sh)
Oct 27 21:50:01 kafka-1 CROND[41119]: (app) CMDOUT (backup start)CMD 是执行了什么,CMDOUT 是输出(没重定向时)。
四、任务重叠
备份跑 40 分钟,但 cron 每 30 分钟起一次 → 同时跑两份,IO 打满。
bash
flock -n /var/lock/backup.lock -c '/opt/app/backup.sh'-n 表示拿不到锁就立即退出(不排队)。配合日志:
text
*/30 * * * * flock -n /var/lock/backup.lock -c '/opt/app/backup.sh >> /var/log/backup.log 2>&1'systemd 也能防重叠
Type=oneshot + 服务本身不允许同时运行多个实例。但它不会跳过,而是会报错或排队。cron+flock 更直接。
五、系统级 cron
bash
ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.weekly//etc/cron.d/ 里的文件格式多一列用户:
ini
# /etc/cron.d/app-jobs
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
*/5 * * * * app /opt/app/healthcheck.sh
0 3 * * * root /opt/app/backup.sh/etc/cron.daily/ 里放可执行脚本即可,由 run-parts 调度,执行时间由 /etc/anacrontab 或 cron 决定,不精确。
访问控制:
bash
cat /etc/cron.allow
cat /etc/cron.deny有 cron.allow 时只允许其中的用户;两者都没有则只有 root 能用。
六、systemd timer
比 cron 强在:日志进 journald、依赖管理、OnCalendar 语义清晰、能补跑(Persistent=true)。
ini
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
After=network-online.target
[Service]
Type=oneshot
User=app
WorkingDirectory=/opt/app
ExecStart=/opt/app/bin/backup.shini
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 03:20
[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.targetbash
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --alltext
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-10-28 03:20:00 CST 5h 27min Mon 2026-10-27 03:20:04 CST 18h ago backup.timer backup.service手动触发测试:
bash
systemctl start backup.service
journalctl -u backup.service -n 50 --no-pagerOnCalendar 语法:
| 表达式 | 含义 |
|---|---|
daily | 每天 00:00 |
*-*-* 03:20:00 | 每天 3:20 |
Mon..Fri 09:00 | 工作日 9 点 |
*-*-1 04:00 | 每月 1 日 4 点 |
*:0/15 | 每 15 分钟 |
验证表达式:
bash
systemd-analyze calendar 'Mon..Fri 09:00'text
Original form: Mon..Fri 09:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Tue 2026-10-28 09:00:00 CST选型建议
- 简单周期任务、系统自带清理脚本 → cron 足够。
- 需要看日志、依赖服务、错过要补跑(比如机器 3 点关着)→ systemd timer。
- 需要精确到秒、或者频繁触发(每分钟多次)→ timer 或应用内调度。
七、时区
cron 用系统时区:
bash
timedatectltext
Local time: Tue 2026-10-27 21:55:03 CST
Time zone: Asia/Shanghai (CST, +0800)容器里如果没有 tzdata,cron 会按 UTC 跑——表现是"任务晚了 8 小时"。修法见 Docker 排错 的时间案例。
八、踩过的坑
| 现象 | 根因 | 处置 |
|---|---|---|
| 手动能跑,cron 不跑 | PATH / 环境变量 | 脚本里写绝对路径,crontab 顶部设 PATH |
| 任务时间少了一半 | % 被当换行 | 转义 \% 或写进脚本 |
| 收不到执行结果 | 没配 MAILTO | 重定向到日志文件 |
日志里报 command not found | 用了 nvm/pyenv 的路径 | 用绝对路径,或在脚本里 source 环境 |
| 任务跑了两份 | 执行时间 > 间隔 | flock -n |
| 改了 crontab 没生效 | 编辑的是别的用户的 | crontab -l 确认 |
| 机器关机错过执行 | cron 不补跑 | 改用 systemd timer + Persistent=true |
| 容器里时间差 8 小时 | 无 tzdata | 装 tzdata + TZ |
crontab -r 误删 | 没有确认 | 定期备份 crontab -l > ~/cron.bak |