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 отправляет вывод задачи пользователю на почту. В нашем образе нет почтового сервера, поэтому в журнале появилось (CRON) info (No MTA installed, discarding output), и вывод пропал. Перенаправляйте его сами, как в первом примере: >> /var/log/backup.log 2>&1.

Ловушка 2: знак процента. В crontab % завершает команду и превращает остаток в стандартный ввод. Мы запланировали date +%F > /tmp/today. Журнал показывает, что cron запустил на самом деле: CMD (date +). Файл не появился, и никакой ошибки до нас не дошло. Пишите \% или вынесите команду в скрипт и планируйте скрипт.

Ловушка 3: другое окружение. Cron запускает задачи через /bin/sh, а не bash, в домашнем каталоге и с коротким окружением. На Ubuntu 24.04 PATH оказался лучше своей репутации — полный системный путь, включая /usr/local/bin и /snap/bin, и для root, и для обычного пользователя. Чего по-прежнему нет, так это всего из домашнего каталога: ~/.local/bin, node из nvm, 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 по воскресеньям и в 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Первого числа каждого месяца в 04:00
*-*-* 03:00 Europe/Kyiv03: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. Что выбрать

CronТаймер systemd
Настройка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 — как запустить скрипт 24/7 на Windows Server. Хороший первый таймер — из нашего руководства по бэкапу через restic; автоматические обновления безопасности — это таймер, который Ubuntu уже поставляет, apt-daily-upgrade.timer, один из 17, работающих на свежем сервере.

Частые вопросы

Почему задача работает в терминале, но не в cron?

Три обычные причины. Cron запускает задачу через sh, а не bash, поэтому синтаксис только для bash падает. Инструменты из домашнего каталога, например nvm, pipx или cargo, не попадают в PATH cron, поэтому пишите полные пути. А любой % в строке команды обрывает её: на нашем сервере date +%F превратилось в date +. Экранируйте его как \% или вынесите команду в скрипт.

Куда девается вывод cron?

По умолчанию уходит на почту. В нашем образе нет почтового сервера, поэтому cron пишет в журнал 'No MTA installed, discarding output', и вывод пропадает. Добавьте к задаче >> /var/log/myjob.log 2>&1 или перейдите на таймер systemd: его вывод сам попадает в журнал.

Чем таймер 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.

Комментарии

Пока нет комментариев. Будьте первым.

Оставить комментарий

Комментарии проходят модерацию перед публикацией.