
cron 能跑,但你不知道它有没有跑成功
一台跑了三年的小服务器,某天发现备份目录里最新的文件是两个月前的。查 crontab,任务还在;查日志,什么都没有。cron 最大的问题不是功能不够,是它默认沉默:成功了不说话,失败了也不说话,除非你主动把输出重定向到某个文件。
systemd timer 是同一位作者生态里的替代品。它把“什么时候跑”和“跑什么”拆成两个文件,把每次执行变成一个有状态、有日志、可查询的单元。本文给出一份可以直接抄的配置,并列出五个真实踩过的坑。
1. 什么时候该换,什么时候不必换
先说结论:如果你只是每天跑一条 rsync,cron 完全够用,换过去是给自己找麻烦。真正值得换的是这几种情况。
- 需要知道上次执行成功还是失败,并且失败时能自动重试
- 任务偶尔超过预期时长,需要超时保护,避免进程堆叠
- 多个任务挤在同一时刻启动,需要错峰
- 需要把输出进 journald,和系统其它日志放在一起查
- 需要在开机后补跑错过的任务
2. 三个位置,先搞清楚文件放哪
systemd 的单元文件有三个常见位置,优先级从高到低:/etc/systemd/system/(管理员自定义,推荐)、/run/systemd/system/(运行时临时)、/usr/lib/systemd/system/(软件包安装,不要手改)。
用户级任务则放在 ~/.config/systemd/user/,配合 systemctl –user 使用。用户级的好处是不需要 root,坏处是用户没登录时默认不运行,要额外执行 loginctl enable-linger 才行。
3. 一份能直接用的 .service
先写执行体。文件名 backup.service,放在 /etc/systemd/system/:
[Unit]
Description=Daily backup to external disk
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
TimeoutStartSec=2h
Nice=10
IOSchedulingClass=idle
几个要点。Type=oneshot 表示这是一次性任务,执行完就退出,不要写 simple。TimeoutStartSec 是超时上限,超过就杀掉,防止一次卡死的任务拖到第二天。Nice 和 IOSchedulingClass 用来降低优先级,备份不该和线上服务抢 IO。
4. 定时器:OnCalendar 与错峰
再写 backup.timer,同名同目录:
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=1800
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target
RandomizedDelaySec=1800 是关键一行,它让任务在 03:30 之后的半小时内随机启动。如果你有十台机器都在这时备份,不加这一行就会同时打到同一个存储上。Persistent=true 负责补跑:机器在 03:30 时是关机的,开机后会把错过的这一次补上,这正是 cron 做不到的。
OnCalendar 的写法比 cron 好读:daily 等价于每天零点,Mon..Fri 04:00 是工作日凌晨四点,*-*-1 05:00 是每月一号。要验证表达式是否正确,用 systemd-analyze calendar 直接算给你看。
systemd-analyze calendar "*-*-* 03:30:00"
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --all
5. 失败重试怎么写
systemd 本身提供重启策略,但对 oneshot 任务,Restart=on-failure 需要配合 RestartSec 才有效,而且默认只重启几次就放弃:
Restart=on-failure
RestartSec=10min
StartLimitIntervalSec=1h
StartLimitBurst=3
这段的意思是:失败后等十分钟重试,一小时内最多尝试三次。StartLimit 必须写在 [Unit] 段,写在 [Service] 里会被忽略,这是最容易出错的地方,因为 systemd 不会报错,只是静默不生效。
如果重试三次仍失败,更好的做法是让脚本自己发通知,而不是无限重启。重试解决的是瞬时故障,不是持续故障,后者重启一百次也没用。
6. 日志:别再重定向到文件
systemd 会把标准输出和标准错误自动收进 journald,不需要在脚本里写 >> /var/log/xxx.log。查最近一次执行:
journalctl -u backup.service -n 50
journalctl -u backup.service --since "24 hours ago"
systemctl status backup.service
要注意 journald 默认会限制日志体积,长期日志需要配置 SystemMaxUse 或转发到 rsyslog,否则一个月前的记录可能已经被轮转掉了。查历史失败记录时,这一步经常是决定性的。
7. 五个真实踩过的坑
第一个:环境变量不一样。cron 里的 PATH 很短,systemd 里的 PATH 也不完整。脚本里用了绝对路径还好,一旦调用 python、rsync 这类命令,最好在 service 里显式写 Environment=PATH=/usr/local/bin:/usr/bin:/bin。
第二个:工作目录不是你以为的那个。默认 WorkingDirectory 是 /,脚本里写相对路径会直接失败。加上 WorkingDirectory=/opt/backup 更稳妥。
第三个:忘了 daemon-reload。改完单元文件不执行 systemctl daemon-reload,改动不会生效,而 systemctl status 看起来一切正常。
第四个:timer 启用了,service 没被触发。常见原因是 timer 文件里没写 [Install] 段,或者用 enable 的是 service 而不是 timer。要启用的是 .timer,不是 .service。
第五个:脚本没有执行权限。ExecStart 指向的脚本必须有 x 位,否则报 203/EXEC,错误信息只有一行,很容易被当成路径写错。
8. 迁移检查清单
| 检查项 | cron 写法 | systemd 写法 |
|---|---|---|
| 执行时间 | 30 3 * * * | OnCalendar=*-*-* 03:30:00 |
| 错峰 | 脚本里 sleep $RANDOM | RandomizedDelaySec=1800 |
| 错过补跑 | 不支持 | Persistent=true |
| 失败重试 | 需自己写循环 | Restart=on-failure |
| 超时控制 | 需 timeout 命令包一层 | TimeoutStartSec=2h |
| 日志 | 手动重定向 | journalctl -u 自动收集 |
迁移顺序建议:先写 service,手动 systemctl start 跑通一次,再写 timer 并 enable。反过来的话,定时器到了点没反应,你会同时怀疑两个文件。
写在最后
systemd timer 的学习成本确实比 crontab 高,一行 vs 两个文件。但它把“任务”变成了系统里的一等公民:有状态、有日志、有超时、有依赖,可以被别的单元引用。
对于每天只跑一条命令的场景,这个代价不划算。对于“必须知道它到底跑没跑成”的场景,cron 的沉默本身就是风险。
你还在用 cron 吗?有没有被“任务静默失败”坑过一次?

评论 (0)