Skip to content

定时任务: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>&1
text
# 只保留错误输出(成功就静默)
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 | tail
text
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.sh
ini
# /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.target
bash
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --all
text
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-pager

OnCalendar 语法:

表达式含义
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
timedatectl
text
               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