凌晨 3 点的数据库备份任务没跑,早上 9 点才发现——这种事故在 cron 世界里只能自认倒霉,因为 cron 假设你的服务器 3 点一定开着机。systemd timer 用一行 Persistent=true 解决了这个问题:错过的时间点在开机后补跑一次。这一行配置,就是本文建议把生产环境的定时任务逐步迁到 timer 的核心理由。
cron 诞生于 1975 年前后的 Unix 时代,语法五年一改都嫌多。timer 是 systemd 2013 年随 v197 加入的调度机制,设计目标就是替代 cron。四十多年的差距摆在这里,答案不会是简单的谁取代谁——本文按场景给结论,不和稀泥。
一张表看懂核心差异
| 维度 | cron | systemd timer |
|---|---|---|
| 配置方式 | crontab -e 加一行 | .service + .timer 两个文件 + systemctl enable |
| 关机/宕机错过的任务 | 直接丢失,无补跑 | Persistent=true 补跑一次 |
| 日志排查 | 邮件或自己重定向到文件 | journal 结构化日志,journalctl -u 直接查 |
| 环境变量 | 继承极少(SHELL、PATH 通常只有 /usr/bin:/bin) | unit 文件内 Environment= 声明,想加就加 |
| 依赖与顺序 | 无,靠脚本内部 sleep 硬等 | After=、Requires=、Wants= 声明依赖 |
| 资源限制 | 无原生支持 | MemoryMax=、CPUQuota= 直接限制 |
| 上一轮运行状态 | 看日志或自己记录 | systemctl list-timers 显示下次/上次触发时间 |
| 学习成本 | 五个字段背一次,全行业通用 | 要理解 unit 文件体系,前置知识多 |
cron 在「配置成本」和「通用性」两格占优,timer 在其余六格占优。这个比分决定了选型的基本盘:任务越接近生产关键路径,timer 的优势越值钱;任务越是一次性小脚本,cron 的一行式越省事。
补跑:cron 欠下的债,timer 一行还清
cron 的调度模型基于「到点检查」:到点时机器开着就执行,不开就跳过。数据库备份、证书续期、日志清理这类任务错跑一次的代价不同,但共同点是——你不会在凌晨 3 点守着服务器看它执行。
timer 的调度模型基于「登记截止时间」:OnCalendar 定义时间点,Persistent=true 声明如果上次触发点被错过(关机、宕机、维护窗口),系统启动后立即补跑一次。补跑只补最后一次,跳过多次只跑一次,避免堆积。
写一个每天凌晨 3:30 备份数据库的 timer,完整两个文件:
# /etc/systemd/system/db-backup.service
[Unit]
Description=Nightly database backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Run db-backup daily at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
service 文件不需要 [Install] 区块,因为被 enable 的是 timer,service 由 timer 拉起。激活并查看调度状态:
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
systemctl list-timers db-backup.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-10-06 03:30:00 CST 16h left n/a - db-backup.timer db-backup.service
list-timers 直接给出下次触发时间、剩余倒计时和上次运行时间。cron 要拿到同样的信息,得自己去翻 syslog 或者写日志文件。能一眼看到「上次到底跑没跑」,是排查定时任务时省下最多时间的一件事。
日志与环境:排查一个 timer 不用猜
cron 任务的输出默认发到本地邮件(MAILTO),大多数服务器没配邮件服务,输出直接蒸发。剩下的实践是每条 cron 尾部手工追加 >> /var/log/xxx.log 2>&1,漏写一条就等于盲跑一个任务。
timer 拉起的任务自动写入 journal。查某次执行有没有出错:
journalctl -u db-backup.service --since "2026-10-05"
Oct 05 03:30:01 srv01 systemd[1]: Starting Nightly database backup...
Oct 05 03:30:04 srv01 db-backup.sh[3312]: mysqldump: Got error: 1044 ...
Oct 05 03:30:04 srv01 systemd[1]: db-backup.service: Failed with result 'exit-code'.
时间戳、退出码、完整输出一次给齐,配合 journalctl -u db-backup.service -f 可以实时盯输出。另外注意:timer 拉起的 service 空闲时会显示 inactive,这是正常状态,任务本身可能在几秒前刚正常结束。
环境变量的问题在 cron 上很典型:crontab 里的任务继承的环境极简,PATH 通常只有 /usr/bin:/bin,脚本里调用的命令在交互 shell 能跑、进 cron 就报 command not found。timer 在 unit 文件里显式声明:
[Service]
Type=oneshot
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=/etc/db-backup/env
ExecStart=/usr/local/bin/db-backup.sh
timer 还能直接复用 systemd 管理服务的通用配置,User=、Restart=、资源限制项全部适用。同一个任务的「跑什么」和「什么时候跑」被拆进两个文件后,改调度时间不碰执行逻辑,改执行逻辑不碰调度时间。
高级调度:OnCalendar 语法五个例子
timer 的日历语法比 cron 五字段表达力强,但写法更啰嗦。五个常用例子(systemd-analyze calendar 可验证任意表达式并打印未来触发点,写错不会静默丢任务):
# 每天凌晨 4 点
OnCalendar=*-*-* 04:00:00
# 每周一 12 点(weekly 等价写法)
OnCalendar=Mon *-*-1..7 12:00:00
# 每月 1 号凌晨 2 点
OnCalendar=*-*-01 02:00:00
# 工作日 22:30、周末 20:00——一个 timer 写两条规则
OnCalendar=Mon..Fri 22:30
OnCalendar=Sat,Sun 20:00
# 每月前四天且仅周一或周二,12 点
OnCalendar=Mon,Tue *-*-01..04 12:00:00
逐字段格式为 DayOfWeek Year-Month-Day Hour:Minute:Second,* 匹配任意值,.. 表示连续区间,逗号枚举。daily、weekly 这类别名代表固定时刻,多个 timer 撞在同一别名上会同秒齐发;加 RandomizedDelaySec=15min 错开峰值,这是多任务服务器的标配。
还有一类 cron 根本没有的能力:相对触发。OnBootSec=15min 表示开机 15 分钟后触发,OnUnitActiveSec=1w 表示上次运行一周后再次触发。后者的调度基准是「上次运行结束点」而非日历时间,特别适合「任务耗时波动大、要求间隔恒定」的场景。临时任务还可以一行命令直接创建瞬时 timer,不落任何文件:
sudo systemd-run --on-active=30 /bin/touch /tmp/marker
分场景结论
| 你是谁 | 建议 | 理由 |
|---|---|---|
| 生产环境备份/证书/监控任务 | systemd timer | 补跑、日志、依赖三项都是关键能力,配置成本一次性投入 |
| 需要跨机器复用的运维脚本 | systemd timer | unit 文件可进 git 版本管理,crontab 是机器本地状态 |
| 每分钟级高频轮询 | 两者皆可 | timer 加 OnUnitActiveSec=1min 或 cron * * * * *,无实质差别 |
| 一次性小脚本、临时任务 | cron 或 systemd-run | 一行配置解决,两个 unit 文件是杀鸡用牛刀 |
| 全是 cron 的存量老服务器 | 保持 cron | 迁移收益要逐任务评估,为迁而迁没有意义 |
一句话立场:新写的生产级定时任务直接用 timer,存量 cron 任务在下次排障时顺手迁移,一次性小活继续 cron。
常见问题
Q:timer 配好后一直不触发,怎么排查?
A:三步走。systemctl list-timers --all 确认 timer 处于活跃状态;systemd-analyze calendar '你的表达式' 验证表达式合法并查看预期触发点;确认 timer 已 systemctl enable 且执行过 daemon-reload。若时间戳错乱,删除 /var/lib/systemd/timers/ 下对应的 stamp-* 空文件后重启 timer。
Q:Persistent=true 会不会把错过的多次执行全部补齐?
A:只补最后一次。错过 5 个触发点只补跑 1 次,这是设计行为,防止任务风暴。
Q:cron 和 timer 能共存吗? A:能,两者互不干扰。很多服务器长期处于混合状态,按任务重要性逐步迁移即可。
Q:timer 任务失败了能自动重试吗?
A:在 service 的 [Service] 区块加 Restart=on-failure 和 RestartSec=60,失败后 60 秒重试。cron 没有原生等价物,只能脚本内部自己写重试逻辑。
Q:OnCalendar 表达式写错了会有提示吗?
A:systemctl enable 或 daemon-reload 时 systemd 会校验并报错。写之前用 systemd-analyze calendar '表达式' 预检更稳,它会打印表达式解析结果和未来几次触发时间。
新写生产定时任务时,先把「错过要不要补跑」想清楚。要补跑,timer 是 Ubuntu 24.04 上唯一开箱即用的答案;不用补跑,cron 那一行依然够用。调度器选型的分水岭从来都在这一个问题里。