systemd timer 还是 crontab:定时任务的五个现实差异
先给结论
临时脚本、跑在容器里、需要精确到「每小时第 15 分钟」的,用 crontab。跑在 systemd 系统上、需要补跑、需要看结构化日志、需要依赖其他服务的,用 systemd timer。
下面说具体差异,都是踩过坑之后才注意到的。
差异一:错过执行会不会补
机器在凌晨 3 点重启,而任务定在 2:30。
crontab 的行为是:睡过去就睡过去了,不补。你只能在日志里发现「今天没跑」。
systemd timer 有个 Persistent=true 选项,关机期间错过的任务会在开机后立刻补跑一次:
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=300
RandomizedDelaySec 是另一个好东西,最多随机延后 5 分钟触发,避免几十台机器在同一秒同时打向同一个下游。crontab 想做到这点只能自己 sleep $((RANDOM % 300)),写起来很别扭。
差异二:日志去哪了
crontab 的输出默认会发本地邮件,而大多数服务器根本没配 MTA,于是输出直接消失。要么重定向到文件,要么加 MAILTO="" 眼不见为净。
systemd 天然对接 journald:
journalctl -u mytask.service --since today
journalctl -u mytask.service -o json | jq '.MESSAGE'
带时间戳、带优先级、支持结构化字段、支持按时间段过滤,还能配合 journalctl -f 实时看。排障效率的差距是数量级的。
差异三:环境变量不一样
这是 crontab 最经典的坑。cron 的执行环境和你的 shell 环境几乎是两个世界:PATH 通常只有 /usr/bin:/bin,~/.bashrc 不会被加载,所以交互式跑得好好的脚本,在 cron 里报 command not found。
解决办法是在脚本开头自己兜住:
!/usr/bin/env bash
set -euo pipefail
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"
cd "$(dirname "$0")"
systemd 则是显式声明,没有隐式环境,反而更可控:
Environment=NODE_ENV=production
EnvironmentFile=/etc/mytask/env
WorkingDirectory=/opt/mytask
注意 systemd 的 EnvironmentFile 格式比较朴素:不支持引号包复杂值、不支持变量展开,FOO="$BAR" 会原样传给程序。
差异四:依赖顺序
任务依赖网络就绪,或者依赖某个数据库先起来。
crontab 没有任何依赖概念,最多在脚本里循环 until ping -c1 db; do sleep 2; done。
systemd 可以声明:
[Unit]
After=network-online.target postgresql.service
Wants=network-online.target
After 只保证启动顺序,Wants 才会尝试拉起依赖。语义分得清楚,比在脚本里写重试循环干净。
差异五:并发与资源
同一个任务上一次还没跑完,下一次又来了。
crontab 会直接再起一个进程,两个实例同时操作同一份文件,数据就烂了。常见的土办法是 flock:
*/5 * * * * flock -n /tmp/mytask.lock /opt/mytask/run.sh
systemd 有内置机制,一行配置解决:
[Service]
Type=oneshot
ExecStart=/opt/mytask/run.sh
TimeoutStartSec=600
oneshot 类型的 service 在启动完成前不会重复启动,timer 触发时会自动跳过。另外 TimeoutStartSec 能防止任务卡死后永远挂着,配合 Restart=on-failure 还能自动重试。
一个完整的 systemd 例子
/etc/systemd/system/mytask.service:
[Unit]
Description=Daily data sync
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=app
WorkingDirectory=/opt/mytask
EnvironmentFile=/etc/mytask/env
ExecStart=/usr/bin/node dist/index.js
TimeoutStartSec=1800
/etc/systemd/system/mytask.timer:
[Unit]
Description=Run data sync daily at 06:00
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
RandomizedDelaySec=120
Unit=mytask.service
[Install]
WantedBy=timers.target
启用:
sudo systemctl daemon-reload
sudo systemctl enable --now mytask.timer
systemctl list-timers --all # 确认下次触发时间
systemctl start mytask.service # 手动跑一次验证
systemctl list-timers 输出的 NEXT 列非常实用,能直接确认时区算得对不对。如果服务器是 UTC 而你以为它是东八区,OnCalendar 会以 UTC 解释,实际执行时间差 8 小时——这个错误每年都有人犯。
怎么选
· 只有一台机器、任务简单、已经在用 cron 管理一堆东西 → 继续用 cron,加 flock 和日志重定向就够。
· 跑在 systemd 系统上、任务重要、需要补跑和结构化日志 → 用 timer,多花的配置时间会还回来。
· 跑在容器里 → 两个都不用,容器编排本身就有 cron 语义(K8s 的 CronJob),或者干脆用外部调度。
· 云函数 → 用平台自己的定时触发器,别在函数里再套一层 cron。
最后一句:不管用哪个,都要有「任务没跑」的告警。定时任务最可怕的失败方式不是报错,而是安静地什么都没做,两周后才发现数据一直没更新。
- 点赞
- 收藏
- 关注作者
评论(0)