cron 对 systemd timers:cron 跑的是行,timers 跑的是单元
同一个 03:30 任务放上两种时钟——没声音的邮件对 journal 日志、错过就跳过对 Persistent=true,以及决定你的下一个任务落在哪的对照表。
自己的 systemd 机器 → timer。单行任务、容器、不是你装的机器 → cron。
cron 默默失败 ——发一封没人看的本地邮件。timer 把每次运行都写进 journal: journalctl -u mytimer.service.
Persistent=true 机器关机期间错过的任务会补跑一次;cron 干脆跳过。
两只时钟
同一个任务——03:30 的夜间备份——放到两只时钟上,看各自留下什么:
CRON · 一行
30 3 * * * /usr/local/bin/backup.sh
墙上时钟一到就跑。失败的话,输出发进一个本地邮箱——通常没人读。03:30 时机器关着? 跳过,无声。
它到底跑没跑? journalctl -u cron -n 20 或 grep CRON /var/log/syslog
SYSTEMD TIMER · 两个单元文件
# foo.timer
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
# foo.service — the job itself
一等单元:状态、依赖、每次运行的日志都在 journal。整点机器在睡觉? Persistent=true 开机补跑一次。
先验一验: systemd-analyze calendar "*-*-* 03:30:00" ——保存之前先看下一次触发时刻
日程裁决:cron 的五个字段是紧凑的现任者;OnCalendar 啰嗦但表达力严格更强——Mon..Fri 09..17:00:00 一行写完,而 RandomizedDelaySec 让一千台机器不会同时对一台服务器开锤
这两只时钟只是从记录自带的命令重新查时——它们读不了你机器的 journal;你的 journal 究竟在哪:第 04、05 节。
实际的差距
- 日志:timer 把 stdout/stderr 按单元写进 journal;cron 顶多给本地用户发邮件。
- 错过的运行:带
Persistent=true的 timer 错过了日程会在开机时补跑一次;cron 只会跳过。 - 依赖:timer 能等
network-online.target或挂载点;cron 得自己在脚本里手搓重试逻辑。 - 语法:cron 一行;timer 两个文件。这就是切换的全部成本。
- 单调时钟选项:
OnBootSec=15min——墙上时钟的乱跳搅不乱它。
04不等就测 timer
三条命令答完所有问题——列出已排了什么、下次何时触发、按 timer 的方式手动跑一遍任务、然后读它的日志:
systemctl list-timers --all # every timer, next + last run
sudo systemctl start backup.service # fire the job now, same unit as the timer
journalctl -u backup.service -f # watch its output live
注意分工: systemctl start backup.timer 负责日程,service 才是任务本体。如果 list-timers 列出了你的 timer,systemctl status backup.service 是绿的,journal 里有你的输出——整条链路都通。
verify: systemctl list-timers --all → 你的 timer,上次 + 下次运行都列着
05手动跑一个 cron 任务
cron 任务跑在精简掉的环境里,所以“我 shell 里好好的,进 cron 就坏”自成一类 bug。想要忠实地复现 cron,把命令经由 sh 跑,用 cron 会用的那个环境:
crontab -l # confirm the exact line
env -i SHELL=/bin/sh PATH=/usr/bin:/bin sh -c '/usr/local/bin/backup.sh'
grep CRON /var/log/syslog | tail # did cron even fire it? (Debian/Ubuntu)
journalctl -u cron -n 20 # same, on systemd distros
这一行 env -i 是诚实的人工测试:裸环境加上 cron 默认的 PATH。cron 的两大经典——光秃秃的 PATH 和没加引号的 %(cron 把它当换行符)——在这个测试里立刻现形。
verify: journalctl -u cron -n 20 → 你命令的 CRON 条目
「我的 timer 不触发」
看起来已死的 timer,通常是它的 service 瞬间就挂了。 四个原因,按排查顺序:
- service 名和 timer 名对不上。
foo.timer触发的是foo.service——手一滑或改了名,timer 就「运行」进虚无。systemctl cat foo.timer一眼看出它瞄准的是谁。 - 改了文件但没重载。 改了单元文件之后要:
sudo systemctl daemon-reload && sudo systemctl restart foo.timer。少了这步,新日程等于没生效。 - 查状态的命令用错了。
systemctl status foo.timer空闲时也显示 active——要看的是list-timers。 - 它触发的 service 瞬间阵亡。
journalctl -u foo.service --since -1h显示出的崩溃,正是list-timers遮住的。
verify: journalctl -u foo.service --since -1h → list-timers 藏掉的那个崩溃
决策表
| cron | systemd timer | |
|---|---|---|
| 搭建 | 一行 crontab | 两个单元文件 |
| 日志 | 本地邮件,通常没人读 | journal,按单元 |
| 错过运行(机器关着) | 跳过 | Persistent=true 开机补跑一次 |
| 依赖 / 先后顺序 | 无——脚本里自己搞 | 完整的单元依赖 |
| 随机延迟 | 自己拿 $RANDOM | RandomizedDelaySec= |
| 试跑 / dry-run 日程 | 没有 | systemd-analyze calendar |
| 哪里都在 | 是——容器、BSD、嵌入式 | 需要 systemd(PID 1) |
| 无 root 的用户任务 | crontab -e | systemd --user 单元 |
拇指法则:往自己的 systemd 机器上交付任务 → timer。临时的个人提醒,或不是你装的机器 → cron。凡是有依赖、有重试、或者必须知道它到底跑没跑的 → 一律 timer。经典套路——夜间 rsync 备份挂在带 Persistent=true 的 timer 下面——哪怕机器在约定的那分钟睡着,备份照样发生,这是 cron 干不了的事。
两只时钟看完 · 未拉取任何东西 · 一切本地