mysurik.ru

systemd timers против cron: как я перевёл рутину сервера на таймеры

Как я понял, что cron меня подвёл

На моём домашнем сервере всегда был cron. Бэкапы в три часа ночи, обновления в пять, проверка дисков по воскресеньям. Всё работало годами, и я даже не смотрел в его сторону. А потом однажды сервер перезагрузился как раз в три часа ночи — обновление ядра уехало само — и бэкап не случился. Cron молча пропустил задачу и даже не пикнул. Бэкапа за ту ночь нет до сих пор, и мне повезло, что диск решил умирать неделей позже, а не в ту самую ночь.

Вот так я и познакомился с systemd timers — таймерами, которые живут внутри systemd и умеют то, чего в cron мне всегда не хватало. В этой статье расскажу, почему я перевёл всю рутину сервера на таймеры, что с ними стало удобнее, где наступил на грабли и что осталось на cron. Если у вас сервер с расписаниями — устраивайтесь поудобнее, будет полезно.

Чем таймеры отличаются от cron на самом деле

Cron — дедушка планировщиков. Простая текстовая табличка: пять полей времени, команда. Работает десятилетиями, и в этом его сила. Но у дедушки скверный характер: если сервер был выключен в момент запуска задачи, задача молча пропадает. Логи разбросаны по syslog, и найти, почему задание не отработало, — тот ещё квест.

Таймер systemd — это пара из двух файлов: myjob.service и myjob.timer. Таймер говорит, когда запускать юнит, а юнит описывает, что именно делать. И вот тут начинается магия. Таймер помнит, когда задача запускалась последний раз. Если сервер был выключен в назначенный час, задача запустится сразу после включения. Пропущенных бэкапов больше нет — и это одна причина, по которой я переехал.

Вторая причина — зависимости. Мой бэкап должен работать только при живой сети и не должен стартовать, пока крутится обновление. В cron это решалось костылями в скрипте. В systemd я просто пишу After=network-online.target — и система сама разруливает порядок.

Мой первый таймер: бэкап, который больше не пропускает ночи

Показываю на живом примере. У меня есть скрипт /usr/local/bin/backup.sh, который делает дамп сайтов и гонит его на второй сервер. Сначала юнит:

[Unit]
Description=Ночной бэкап сайтов

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh

Теперь таймер:

[Unit]
Description=Запуск бэкапа каждую ночь в 3:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

Кладёшь оба файла в /etc/systemd/system/, дальше три команды:

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

Вся соль — в строчке Persistent=true. Именно она заставляет таймер догонять пропущенные запуски. Сервер лежал ночью? Утром бэкап догонит. У cron такого поведения нет в принципе, там разве что анакрон, но это уже отдельная песня с его собственными сюрпризами.

OnCalendar: синтаксис, который пугает первые полчаса

Формат времени у таймеров поначалу пугает. Привык к пяти кроновским звёздочкам, а тут какая-то строка. Но разобравшись, я оценил. Хочешь «каждый день в 3:00» — *-*-* 03:00:00. Хочешь «каждый вторник в полдень» — Tue *-*-* 12:00:00. Хочешь «каждые два часа» — OnCalendar=*-*-* 0/2:00:00. Читается почти по-человечески.

А для интервалов есть отдельная форма: OnUnitActiveSec=6h — каждые шесть часов после запуска. Для проверок живости сервера это удобнее, чем городить кроновские выражения.

Момент Икс: таймеры молчали, и я чуть не откатился на cron

Теперь про ложку дёгтя, из-за которой я чуть не всё откатил назад. Через неделю после переезда я полез проверять логи и обнаружил: один из таймеров не сработал. Вообще. Ни ошибки, ни запуска, тишина. В cron я бы глянул в /var/log/syslog и нашёл строчку. А тут — пусто.

Я уже скачал было конфиги cron обратно, но полез в документацию. И вот оно: таймеры пишут свои логи в journal, и смотреть их надо через journalctl -u myjob.service. А моя беда была в другом — я опечатался в имени юнита в таймере, и systemd просто не находил, что запускать. После того как я подружился с journalctl, жизнь наладилась: journalctl -u backup.service --since yesterday показывает всё: когда стартовало, что вывело, чем кончилось. Честно говоря, теперь логи cron меня раздражают своей разрозненностью.

Ещё одна ловушка поджидала с правами. В cron у меня были строчки от root и от пользователя вперемешку. В systemd юниты от пользователя кладутся в ~/.config/systemd/user/ и запускаются через systemctl --user. Первое время я путал, куда какой класть, и таймеры просто не находились. Запомните: системные — в /etc/systemd/system, пользовательские — в домашней папке, и не смешивайте.

Что я перевёл на таймеры, а что оставил в cron

Честно скажу: cron я не снёс полностью. У него остались задачи, где он хорош. Вот как теперь выглядит мой расклад:

  • Бэкапы сайтов и баз — таймер с Persistent. Это критично, пропусков быть не должно.
  • Обновления безопасности — таймер. Важна история запусков и зависимости от сети.
  • Проверка дисков и SMART — таймер, раз в сутки, логи в journal удобно грепать.
  • Простые пользовательские напоминания и скрипты — оставил в cron, их не жалко.
  • Задачи «раз в минуту» — тоже cron, для такой частоты таймер избыточен.

Кстати, о частоте. У таймеров есть минимальный практический интервал, и спамить их каждую секунду — плохая идея. Для поминутных задач cron остаётся проще и легче. Я за прагматизм: инструмент под задачу, а не мода ради моды.

Удобства, которые я открыл уже после переезда

Пока жил с таймерами, нашёл несколько приятных мелочей. systemctl list-timers --all показывает все таймеры, когда каждый последний раз срабатывал и когда сработает в следующий раз. Одной командой — вся картина расписаний сервера. В cron я такое делал, читая крон таблички всех пользователей, и молился, что ничего не забыл.

Вторая штука — ручной запуск. Хочешь проверить задачу прямо сейчас: systemctl start backup.service. Не надо лезть в crontab и двигать время. Для отладки это золото: поправил скрипт — запустил юнит руками — посмотрел вывод в journalctl. У меня так обкатываются все новые задания, прежде чем получить таймер.

Третья — случайное срабатывание можно ловить по событию, а не только по времени. Есть таймеры на загрузку (OnBootSec=10min) и на активацию юнита. Мой мониторинг дисков стартует через десять минут после загрузки сервера — раньше я для этого городил отдельный скрипт со sleep.

Переезд с cron на таймеры: мой порядок действий

Если соберётесь переезжать, делайте по шагам, а не всё сразу. Сначала перенесите одну некритичную задачу и погоняйте её неделю. Посмотрите логи, поймите синтаксис, прочувствуйте journalctl. Потом переведите бэкапы — самое ценное. И только потом сносите кроновские строчки, которые заменили.

Мой чек-лист на каждую задачу получился такой: написать service-юнит, проверить его ручным запуском, написать timer с Persistent, включить, убедиться в list-timers, подождать первый срабатывание и заглянуть в журнал. На каждую задачу уходит минут двадцать вместе с проверкой. У меня вся рутина переехала за один вечер.

И не выбрасывайте старые crontab сразу — закомментируйте строчки и оставьте на месяц. Когда убедитесь, что таймеры держат удар, удалите. Я так и сделал, и одна строчка, кстати, до сих пор лежит закомментированная — привычка.

Разброс времени: зачем таймеру случайность

Ещё одна фишка, до которой я дошёл не сразу. Если у вас несколько серверов, и на всех бэкап стоит на три часа ночи, то в три ноль ноль все они дружно бьют по хранилищу. Хранилище, понятное дело, не в восторге. В cron я разгребал это руками: на одном сервере 3:00, на другом 3:20, на третьем 3:40. А потом забывал, где какое время, и путался в таблицах.

У таймеров есть RandomizedDelaySec=15min. Задача стартует в назначенное время плюс-минус случайные пятнадцать минут. Серверы сами расходятся по времени, и хранилище дышит ровно. Я выставил такой разброс всем своим машинам, и пиковая нагрузка на бэкап-сервер размазалась по получасу. Мелочь, а нервы бережёт.

Там же рядом живёт AccuracySec — точность срабатывания. По умолчанию таймер может сдвинуть запуск на минуту ради экономии пробуждений. Для бэкапа это без разницы, но если вам нужна секундная точность, поставьте AccuracySec=1s. Я на эту настройку наткнулся, когда моя проверка живости казалась мне подозрительно неторопливой.

Таймеры от пользователя: мой опыт с домашней папкой

Отдельная история — пользовательские таймеры. Не всё на сервере требует root. Мой сборщик статистики, например, работает от обычного пользователя и трогает только его папки. Класть такое в системные юниты неправильно, и systemd предлагает решение: юниты в ~/.config/systemd/user/.

Запускаются они командой systemctl --user enable --now mytimer.timer. Но тут прячется сюрприз, который стоил мне часа ковыряний: пользовательские юниты живут, пока пользователь «в системе». На сервере, где я вхожу по SSH и выхожу, мои таймеры умирали вместе с сессией. Лечится одной командой: loginctl enable-linger myuser. После этого юниты пользователя работают постоянно, даже когда никто не залогинен.

Знаете, что я думаю? Вот за такие неочевидные мелочи я и люблю документацию systemd. Она толстая, но там лежат ответы на вопросы, которые ты ещё не задал.

Частые ошибки при переезде, которые совершу и вы

По своим шишкам собрал список типовых ошибок. Первая — забыть daemon-reload после правки юнитов. systemd продолжает жить со старыми конфигами, и вы ломаете голову, почему правка не применилась. Привыкайте: поменял файл — перечитал конфигурацию.

Вторая — опечатка в OnCalendar. Система её молча проглотит, а таймер просто не заведётся или сработает не тогда. Проверяйте строку времени утилитой systemd-analyze calendar "выражение" — она покажет, как система поняла ваше расписание и когда будут следующие запуски. Я теперь всегда прогоняю выражение через неё, прежде чем положить в таймер.

Третья — забыть [Install] WantedBy=timers.target в таймере. Юнит работает при ручном запуске, а enable не привязывает его к загрузке. После перезагрузки таймера нет, и вы узнаёте об этом по пропавшему бэкапу. Проверяйте systemctl is-enabled.

Четвёртая — длинные скрипты прямо в ExecStart. Юнит с простынёй из команд нечитаем и не отлаживается. Мой правило: ExecStart вызывает один скрипт, а вся логика — в скрипте, где её удобно править и тестировать отдельно от systemd.

Если вкратце

  • Cron молча пропускает задачи, если сервер был выключен; таймеры с Persistent=true догоняют пропущенное.
  • Таймер — это пара файлов: myjob.service (что делать) и myjob.timer (когда делать).
  • Логи живут в journalctl: journalctl -u myjob.service — вся история запусков и вывод.
  • Вся картина расписаний — systemctl list-timers --all: когда сработало и когда сработает.
  • Отладка — ручной systemctl start myjob.service, без сдвигания времени в crontab.
  • Поминутные задачи и простые напоминания можно оставить в cron, там он хорош.
  • Переезжайте по одной задаче, начиная с некритичной, и держите старый crontab закомментированным месяц.

Бэкапы, которые не пропускают ночи, стоят одного вечера переезда. Мой диск это подтвердил — а что подтвердит ваш?

Ваш комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *