用 systemd timer 替代 cron:云服务器定时备份改造实录

举报
yd_238266496 发表于 2026/10/30 10:20:01 2026/10/30
【摘要】 背景:被 cron 坑过一次之后一台 2 核 2G 的 Flexus L 云服务器(Ubuntu 22.04)上跑着几个小服务,之前用 crontab 每天凌晨做一次目录备份。某次机房网络抖动导致凌晨 2 点的任务卡住,cron 既不重试也不告诉你,直到磁盘写满才发现备份已经三天没生成。后来把定时任务从 cron 整体迁到 systemd timer,收益很直接:每次执行都有独立的 jou...

背景:被 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

这段配置有三个隐性问题:

  1. cron 的 PATH 很短(通常只有 /usr/bin:/bin),脚本里用到 /usr/local/bin/tar 之流就会「command not found」;
  2. 没有超时控制,卡住就一直占着;
  3. 日志只有追加,没有轮转(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 任务,不用再在每个脚本里写告警逻辑。

六、迁移收尾清单

  1. sudo systemctl disable --now 旧的 cron 条目(直接 crontab -e 注释掉即可);
  2. 连续观察两三天 journalctl -u appbackup.service,确认 LAST 每天在推进;
  3. 别忘了 Persistent=true 之后,重启机器验证一次补跑:sudo systemctl restart systemd-timers 或直接重启主机,开机后 journalctl -b -u appbackup.service 里应能看到一条补跑记录;
  4. 如果容器里没有 systemd(比如很多 Docker 镜像),timer 不可用,这种场景还是用 cron 或应用内的调度更合适——迁移前先确认 pidof systemd 有输出。

小结

cron 不是不能用,而是「出问题的时候你什么都不知道」。换成 systemd timer 后,任务的成功与否、耗时、下次触发时间、失败原因都在 journalctl 和 list-timers 里一目了然;再加一个 OnFailure 模板 unit,就把「备份静默失败」这类最容易埋雷的问题堵死了。整套改造花了不到一小时,但对一台长期跑着业务的云服务器来说非常值。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。