用 systemd timer 替代 cron:云服务器定时备份改造实录
背景:被 cron 坑过一次之后
一台 2 核 2G 的 Flexus L 云服务器(Ubuntu 22.04)上跑着几个小服务,之前用 crontab 每天凌晨做一次目录备份。某次机房网络抖动导致凌晨 2 点的任务卡住,cron 既不重试也不告诉你,直到磁盘写满才发现备份已经三天没生成。
后来把定时任务从 cron 整体迁到 systemd timer,收益很直接:
- 每次执行都有独立的 journal 日志(
journalctl -u xxx直接看); - 关机错过的任务,开机后能补跑(
Persistent=true); - 失败可以挂
OnFailure=触发告警; - 依赖、超时、并发、运行用户全部写在 unit 文件里,可以进 Git 版本管理。
下面以「每天 02:30 备份 /srv/app 到 /backup,并清理 7 天前的旧包」为例,记录完整改造过程。
一、先看清原来的 cron 长什么样
$ crontab -l
30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
这段配置有三个隐性问题:
cron的PATH很短(通常只有/usr/bin:/bin),脚本里用到/usr/local/bin/tar之流就会「command not found」;- 没有超时控制,卡住就一直占着;
- 日志只有追加,没有轮转(
logrotate得另配)。
二、拆成 service + timer 两个 unit
systemd 的定时任务模型是「timer 负责触发,service 负责干活」,所以先写 service:
# /etc/systemd/system/appbackup.service
[Unit]
Description=Backup /srv/app to /backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=root
ExecStart=/opt/scripts/backup.sh
TimeoutStartSec=30min
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=multi-user.target
再写 timer:
# /etc/systemd/system/appbackup.timer
[Unit]
Description=Daily backup of /srv/app at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=300
Persistent=true
Unit=appbackup.service
[Install]
WantedBy=timers.target
几个参数值得解释:
OnCalendar=*-*-* 02:30:00:日历语法,比 cron 五个字段更可读,还支持Mon-Fri、..区间等;RandomizedDelaySec=300:在多台机器上部署时打散启动时间,避免同时把备份对象压垮;Persistent=true:把上次触发时间记在/var/lib/systemd/timers/,关机期间错过的那次会在开机后补跑一次(只补一次,不会连跑好几天);Type=oneshot:脚本跑完即退出,systemctl start会同步等它结束,手工验证很方便。
三、备份脚本本身也要改
#!/bin/bash
set -euo pipefail # 出错立刻失败,避免半截备份被当成成功
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # systemd 环境里别指望登录 shell 的 PATH
LOG_TAG="backup"
SRC=/srv/app
DST=/backup
STAMP=$(date +%F_%H%M)
ARCHIVE="$DST/app-$STAMP.tar.gz"
mkdir -p "$DST"
logger -t "$LOG_TAG" "start $ARCHIVE"
tar -czf "$ARCHIVE" -C "$(dirname "$SRC")" "$(basename "$SRC")"
# 校验:tar 能列出内容才算写完整
tar -tzf "$ARCHIVE" > /dev/null
logger -t "$LOG_TAG" "ok size=$(stat -c %s "$ARCHIVE")"
# 清理 7 天前的包
find "$DST" -name 'app-*.tar.gz' -mtime +7 -delete
logger -t "$LOG_TAG" "old archives pruned"
写这段踩了两个坑:
坑 1:set -o pipefail 与 tar 的 SIGPIPE。 早期脚本写成 tar -czf - ... | gzip > x.tgz,加了 pipefail 后偶发失败退出码 141,其实是下游提前关闭管道导致的。直接把 -z 交给 tar,管道没了,问题消失。
坑 2:备份目录和源目录在同一块盘上。 有一次根分区写满,备份文件把最后一点空间吃光,服务直接崩。改成先查剩余空间再决定是否备份:
need=$(du -sk "$SRC" | awk '{print $1}')
free=$(df -k "$DST" | awk 'NR==2 {print $4}')
if [ "$need" -gt $((free * 80 / 100)) ]; then
logger -t "$LOG_TAG" "ABORT: free=${free}K need=${need}K"
exit 3
fi
四、启用、验证、看日志
sudo systemctl daemon-reload
sudo systemctl enable --now appbackup.timer
# 看下一次 / 上一次触发时间
systemctl list-timers appbackup.timer
# NEXT LEFT LAST PASSED UNIT
# Fri 2026-10-31 02:31:47 CST 15h left Thu 2026-10-30 02:31:12 CST 10h ago appbackup.timer
# 手工跑一次(不等定时)
sudo systemctl start appbackup.service
systemctl status appbackup.service --no-pager
# 执行日志
journalctl -u appbackup.service -n 50 --no-pager
list-timers 的 LAST 列出现时间,说明 timer 确实触发过;如果 LAST 一直是 n/a,通常是 WantedBy=timers.target 写错、或者只 enable 了 service 没 enable timer。
五、失败告警:OnFailure + 一个 5 行的 unit
# /etc/systemd/system/notify-fail@.service
[Unit]
Description=Notify failure of %i
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'echo "unit %i failed on %HOSTNAME" | /usr/bin/curl -s -X POST --data-binary @- https://example.internal/alert'
然后在 appbackup.service 的 [Unit] 段落加一行:
OnFailure=notify-fail@%n.service
%n 会展开成 appbackup.service,于是模板实例拿到失败单元名。这个模式可以复制给所有 oneshot 任务,不用再在每个脚本里写告警逻辑。
六、迁移收尾清单
sudo systemctl disable --now旧的 cron 条目(直接crontab -e注释掉即可);- 连续观察两三天
journalctl -u appbackup.service,确认LAST每天在推进; - 别忘了
Persistent=true之后,重启机器验证一次补跑:sudo systemctl restart systemd-timers或直接重启主机,开机后journalctl -b -u appbackup.service里应能看到一条补跑记录; - 如果容器里没有
systemd(比如很多 Docker 镜像),timer 不可用,这种场景还是用cron或应用内的调度更合适——迁移前先确认pidof systemd有输出。
小结
cron 不是不能用,而是「出问题的时候你什么都不知道」。换成 systemd timer 后,任务的成功与否、耗时、下次触发时间、失败原因都在 journalctl 和 list-timers 里一目了然;再加一个 OnFailure 模板 unit,就把「备份静默失败」这类最容易埋雷的问题堵死了。整套改造花了不到一小时,但对一台长期跑着业务的云服务器来说非常值。
- 点赞
- 收藏
- 关注作者
评论(0)