EQVPS
控制台开始使用

如何在 VPS 上用 cron 和 systemd 定时器安排任务

按计划运行备份、清理和脚本。五分钟上手 cron,以及三个会悄悄搞坏任务的陷阱;然后是 systemd 定时器:日志、停机后补跑和精确时间。下面的每种行为都在一台全新的 Ubuntu 24.04 服务器上验证过。

服务器的价值在于你睡觉时它还在干活:备份、日志轮转、证书续期、九点的报告。Linux 为此提供了两种调度器。cron 已有四十年历史,一行就够。systemd 定时器需要两个小文件,换来的是日志、停机后补跑和精确时间。本指南在 Ubuntu 24.04 上介绍这两者,每种行为都在用我们镜像创建的新服务器上验证过,包括会让 cron 任务一声不吭就失败的三个错误。

你需要一个有 sudo 权限的用户;Ubuntu 设置指南会创建一个。

1. 五分钟上手 cron

cron 已安装在我们的 Ubuntu 和 Debian 镜像上并在运行。每个用户都有一张任务表;用下面的命令编辑你自己的:

crontab -e

每一行是五个时间字段加一条命令:

# min hour day month weekday  command
  30  3    *   *     *        /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
  */5 *    *   *     *        /usr/local/bin/check.sh
  0   9    *   *     1-5      /usr/local/bin/report.sh
  @reboot                     /usr/local/bin/start-tunnel.sh

也就是:每晚 03:30、每 5 分钟、工作日 09:00,以及每次开机后一次。*/5 表示每隔五个值,1-5 表示范围,1,15 表示列表。使用的是服务器的时区,我们的镜像上是 UTC;用主机名和时区指南设置一次,cron 就会跟随。

用 journalctl -u cron 查看运行过什么。每次运行都会记录用户和命令,三个陷阱也会在那里现形。

陷阱 1:输出消失。 cron 会把任务的输出用邮件发给用户。我们的镜像没有邮件服务器,所以 journal 里写着 (CRON) info (No MTA installed, discarding output),输出就没了。请自己重定向,像第一个例子那样用 >> /var/log/backup.log 2>&1。

陷阱 2:百分号。 在 crontab 里,% 会结束命令,并把其余部分变成标准输入。我们安排了 date +%F > /tmp/today。journal 显示 cron 实际执行的是:CMD (date +)。没有写出任何文件,也没有任何错误传到我们这里。请写成 \%,或者把命令放进脚本再安排这个脚本。

陷阱 3:不同的环境。 cron 用 /bin/sh 而不是 bash,在你的主目录里、以精简的环境运行任务。在 Ubuntu 24.04 上,PATH 比它的名声要好:无论 root 还是普通用户,都是包含 /usr/local/bin 和 /snap/bin 的完整系统路径。仍然缺少的是主目录下的一切:~/.local/bin、nvm 的 node、pipx 和 cargo。在任务里使用完整路径,或者让这一行以 . $HOME/.profile; 开头。

属于系统而非某个用户的任务,放在 /etc/cron.d/ 中,作为多一个字段(用户)的文件:

30 3 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Ubuntu 还会运行 /etc/cron.hourly、cron.daily、cron.weekly 和 cron.monthly 里的所有内容:每小时第 17 分钟、06:25、周日 06:47 以及每月 1 日 06:52。把可执行脚本放进去,它就会和其他脚本一起运行。

2. systemd 定时器

一个定时器由两个单元组成:干活的服务,以及决定何时运行的定时器。先创建脚本,再创建单元。

sudo nano /usr/local/bin/backup.sh
sudo chmod +x /usr/local/bin/backup.sh
sudo nano /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
sudo nano /etc/systemd/system/backup.timer
[Unit]
Description=Run the backup every night at 03:30

[Timer]
OnCalendar=*-*-* 03:30
Persistent=true
AccuracySec=1m

[Install]
WantedBy=timers.target

启用的是定时器,而不是服务:

sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer

list-timers 显示下一次运行、距离它还有多久以及上一次运行。日志无需配置:journalctl -u backup.service 里有每次启动、每次结束和每一行输出,都带时间戳。要立刻运行任务,用 sudo systemctl start backup.service;在信任计划之前,也用这个方法测试脚本。

日历语法。 OnCalendar 的读法是 星期 年-月-日 时:分:秒,* 代表任意值。使用前先测试表达式:

systemd-analyze calendar "Mon..Fri *-*-* 03:00" --iterations 2

它会输出规范化后的形式和接下来的日期。我们检查过的一些表达式:

OnCalendar含义
*-*-* 03:30每天 03:30
Mon..Fri *-*-* 09:00工作日 09:00
*:0/15每 15 分钟
weekly周一 00:00
*-*-01 04:00每月 1 日 04:00
*-*-* 03:00 Europe/Kyiv基辅时间 03:00;在我们的测试中于 01:00 UTC 运行

值得了解的三个设置。 Persistent=true 会把上一次运行记录到磁盘,并补跑服务器关机或定时器停止期间错过的执行。我们停掉每两分钟一次的定时器,等一次计划运行过去后再启动它:错过的那次在同一秒内就开始了。没有 Persistent 的话,它只会等待下一个时间点。

意外在于 AccuracySec。systemd 默认把定时器合并到一分钟的窗口里以减少唤醒,所以我们的第一次运行出现在 07:42:18,而不是 07:42:00。设为 AccuracySec=1s 后,之后的运行精确到秒地落在 07:46:00 和 07:48:00。需要精确到分钟时设置它;夜间备份保持默认即可。

RandomizedDelaySec=10m 则有意反其道而行:它把启动时间分散到一个窗口内,避免十台服务器在同一秒冲击同一个备份存储。

一次性任务根本不需要文件:

sudo systemd-run --on-active=30m --unit=cleanup-once /usr/local/bin/cleanup.sh

它会创建一个临时定时器,运行一次后消失;在那之前,systemctl list-timers 会显示它。

3. 该用哪个

cronsystemd 定时器
配置1 行2 个文件
日志只有重定向输出时才有journalctl -u name.service
服务器关机期间错过的运行丢失配合 Persistent=true 在开机时运行
到下一个时间点任务还在运行启动第二份跳过,直到第一份结束
精确时间精确到分钟配合 AccuracySec=1s 精确到秒
状态无list-timers、systemctl status

一年后还能一眼看懂的简单一行,用 cron。凡是需要看到失败、不能重叠、或者错过重启后也必须运行的任务,用定时器:备份、证书续期、数据库清理。如果 cron 任务可能重叠,就把它包起来:flock -n /tmp/backup.lock /usr/local/bin/backup.sh,第二份会立即退出。

两种调度器都是启动任务后就结束。需要全天候运行、崩溃后能恢复的程序,要的是服务而不是计划;请参阅如何创建 systemd 服务,Windows 则参阅如何在 Windows Server 上 24/7 运行脚本。一个不错的第一个定时器来自我们的 restic 备份指南;自动安全更新本身就是 Ubuntu 自带的定时器 apt-daily-upgrade.timer,是新服务器上运行的 17 个定时器之一。

常见问题

为什么任务在终端里能跑,在 cron 里却不行?

常见原因有三个。cron 用 sh 而不是 bash 运行任务,所以 bash 专有的语法会失败。安装在主目录里的工具,比如 nvm、pipx 或 cargo,不在 cron 的 PATH 里,所以要用完整路径。另外命令行中的任何 % 都会让命令在那里结束:在我们的服务器上 date +%F 变成了 date +。请写成 \% 或把命令放进脚本。

cron 的输出去哪了?

默认发到邮件。我们的镜像没有邮件服务器,所以 cron 记录 'No MTA installed, discarding output',输出就丢了。给任务加上 >> /var/log/myjob.log 2>&1,或者改用 systemd 定时器,它的输出会自动进入 journal。

systemd 定时器比 cron 好在哪?

日志在 journalctl 里;仍在运行的任务不会再启动第二份;配合 Persistent=true 能补跑服务器关机期间错过的执行;还有一行状态显示下一次和上一次运行。cron 胜在简洁:一行而不是两个文件。

我的定时器晚了 18 秒才运行,为什么?

systemd 默认会把定时器合并到一分钟的窗口内,以减少唤醒系统的次数,这就是 AccuracySec 设置。我们每两分钟一次的定时器在 07:42:18 而不是 07:42:00 运行。设为 AccuracySec=1s 后,它恰好在 07:46:00 运行。需要精确到分钟时就设置它。

cron 和定时器用哪个时区?

用服务器的时区,在我们的镜像上是 UTC。用 timedatectl 设置一次(参见我们的主机名和时区指南),cron 和定时器都会跟随。定时器也可以带自己的时区:OnCalendar=*-*-* 03:00 Europe/Kyiv 在我们的测试中于 01:00 UTC 运行。

评论

暂无评论。来做第一个吧。

发表评论

评论在显示前会经过审核。