Ой, ничего не найдено!

К сожалению, по вашему запросу пока ничего нет (но это только пока!), зато вы можете подписаться на нашу замечательную email-рассылку, чтобы не пропустить самое интересное в будущем.

  • 19

Cron, systemd-таймеры и скрипты: автоматизируем рутину сервера

  • 3 минуты на чтение

Часть 12 из 14 · Серия «Сервер в бою»

За одиннадцать статей у нас накопился целый список обещаний со словами «по расписанию»: prune для Docker и autoremove для пакетов (статья про диск), сброс кэшей (кэширование), heartbeat для мониторинга (Uptime Kuma), продление сертификатов, бэкапы, наконец. Автоматизация — это клей, который держит всё остальное: правильный сервер обслуживает себя сам, а человеку оставляет только чтение отчётов.

Но у этого клея есть фирменный дефект, вынесенный в анонс: cron молча падает. Точнее — молча не работает, что ещё хуже: упавший сервис вы заметите по алертам, а cron-задача, которая полгода тихо завершается с ошибкой, обнаруживается в самый интересный момент — когда нужен вчерашний бэкап, а последний датирован мартом. Сегодня разберём, почему так происходит (спойлер: по пяти разным причинам, и все — классика), как заставить любую задачу отчитываться о жизни и смерти, и когда вместо дедушки crontab стоит взять systemd-таймеры — у них половина кроновских болезней не предусмотрена конструкцией.

Cron за два абзаца — и сразу к болезням

Синтаксис знают все, поэтому телеграфно: crontab -e — личное расписание пользователя, пять полей времени плюс команда:

# мин час день месяц день_недели команда
  30   4    *     *       0      /usr/local/bin/backup.sh   # вс, 4:30
  */5  *    *     *       *      /usr/local/bin/heartbeat.sh # каждые 5 минут

Есть ещё системный /etc/cron.d/ и папки cron.daily/weekly/monthly — для задач, не привязанных к пользователю. На этом хорошие новости заканчиваются: написать строчку — одна минута, а вот сделать так, чтобы через год она всё ещё работала и вы об этом знали, — тема остатка статьи.

Пять причин, почему cron молча падает

Причина 1, царица всех остальных: другое окружение. Скрипт, отлаженный в терминале, попадает в cron голым: PATH обрезан до /usr/bin:/bin, никакого .bashrc, никаких переменных вашей сессии, рабочая директория — не та, что вы думаете. Отсюда великий феномен «руками работает, в кроне — нет»: скрипт зовёт docker или certbot, которых нет в кроновском PATH, — и умирает на первой строке. Лечение — привычка: в скриптах для cron все пути абсолютные (/usr/bin/docker, cd /var/www/site1 первой строкой), а нужные переменные объявляются явно.

Поддержка 24/7
Что-то сломалось? Рядом живой человек, а не бот
Живая поддержка рядом с первого дня, а не бот с заготовленными ответами. Поможем настроить, перенести и не паниковать.
Подключиться

Причина 2: вывод уходит в никуда. По задумке 1975 года cron отправляет вывод задачи почтой локальному пользователю. На типичном VPS почтовая система не настроена — и все сообщения об ошибках буквально испаряются. Задача может год печатать Permission denied — в пустоту. Минимальное лечение — руками направить вывод в файл:

30 4 * * 0 /usr/local/bin/backup.sh >> /var/log/cron/backup.log 2>&1

(2>&1 — «ошибки туда же»; и не забудьте этот лог в logrotate — пятая статья предупреждала.)

Причина 3: скрипт «успешно» игнорирует собственные ошибки. Shell по умолчанию исполняет скрипт до конца, что бы ни падало по дороге: дамп базы не создался — а архивирование и удаление старых копий прекрасно отработали. Итог: бэкапы «делаются», ротация работает, файлы пустые. Три волшебные буквы в начало каждого скрипта:

#!/bin/bash
set -euo pipefail
# -e: любая ошибка — стоп; -u: необъявленная переменная — стоп;
# pipefail: ошибка в середине пайпа — тоже ошибка

Причина 4: наложение запусков. Задача раз в 5 минут однажды выполняется 7 минут (база подросла, диск занят) — и поверх неё стартует вторая копия. Два rsync в один каталог, два дампа одной базы — дальше фантазируйте сами. Лечение штатное — flock, замок на файле:

*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh

-n — «замок занят — молча выйти», вторая копия просто не стартует.

Причина 5: символ процента. Вишенка для коллекции: в crontab символ % — спецсимвол (означает перевод строки), и невинное date +%F в строке задачи ломает её тихо и намертво. Либо экранировать (date +\%F), либо — правильнее — не писать логику в crontab вообще: одна строка = вызов одного скрипта, вся логика внутри скрипта.

от 0 ₽ на старте
Свой VPS дешевле, чем кажется
Прозрачные тарифы без сюрпризов в чеке. Платите за то, что реально используете, и масштабируйтесь, когда вырастете.
Смотреть тарифы

Ловим падения: правило «тишина — это тоже сигнал»

Логи из причины 2 — это возможность расследовать падение. Но кто скажет, что расследовать пора? Читать по утрам логи двадцати задач вы не будете, будем честны. Нужен механизм, который сам поднимет тревогу, — и мы его уже построили в четвёртой статье, осталось применить: push-мониторы Uptime Kuma, они же dead man's switch. Логика обратная обычной: не «сообщи, если умер», а «сообщай, что жив, — а замолчишь, я подниму тревогу».

Заводим в Kuma push-монитор «Weekly backup» с периодом чуть больше интервала задачи, получаем персональный URL — и дописываем в конец скрипта одну строку, которая выполнится только если всё выше прошло без ошибок (спасибо set -e):

curl -fsS --retry 3 "https://ваша-kuma/api/push/AbCdEf123" > /dev/null

Всё. Бэкап не создался, диск кончился, сервер лежал, скрипт удалили при переустановке, крон забыли перенести на новый сервер — любой сценарий молчаливой смерти теперь заканчивается одинаково: пуш не пришёл, Kuma прислала тревогу в Telegram. Это самые окупаемые полминуты настройки на задачу во всей серии: класс проблем «узнал через полгода» закрывается целиком.

Собирём всё в шаблон, который стоит копировать для каждой новой задачи:

#!/bin/bash
set -euo pipefail
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

cd /var/www/site1

# --- полезная работа, абсолютные пути ---
/usr/bin/mysqldump --single-transaction site1_db | /usr/bin/gzip > /backup/site1-$(date +\%F).sql.gz
/usr/bin/find /backup -name "site1-*.sql.gz" -mtime +14 -delete

# --- дошли сюда = всё живо ---
curl -fsS --retry 3 "https://ваша-kuma/api/push/AbCdEf123" > /dev/null

Systemd-таймеры: тот же cron, но с прожектором

Теперь честный разговор про альтернативу. Systemd-таймеры — это «запускать юнит по расписанию», и большинство кроновских болезней у них не лечатся, а отсутствуют по конструкции:

  • Вывод и ошибки идут в journalctl — в тот самый журнал из второй статьи: journalctl -u backup.service — и вся история запусков с полным выводом, без самодельных редиректов. Причина 2 не существует.
  • Наложение невозможно: сервис — одиночка, пока экземпляр работает, второй не стартует. Причина 4 не существует.
  • Persistent=true: если сервер был выключен в момент по расписанию (перезагрузка после обновлений, помните шестую статью?) — задача выполнится при старте. Cron пропущенное просто пропускает: воскресный бэкап, совпавший с ребутом, у крона не случится вовсе.
  • systemctl list-timers — вся картина: что, когда запускалось, когда запустится. У cron аналога нет в принципе.

Цена — многословность: вместо одной строки — два файла. Тот же бэкап, /etc/systemd/system/backup.service:

[Unit]
Description=Weekly site backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Запуск за 2 минуты
Идея есть — пусть будет где её запустить
VPN, Telegram-бот, своё облако или пет-проект — арендуйте сервер за пару минут и проверьте идею в деле уже сегодня.
Создать сервер

и /etc/systemd/system/backup.timer:

[Unit]
Description=Run backup weekly

[Timer]
OnCalendar=Sun 04:30
Persistent=true
RandomizedDelaySec=10m   ; не толпиться с другими задачами секунда в секунду

[Install]
WantedBy=timers.target
systemctl daemon-reload && systemctl enable --now backup.timer
systemctl list-timers backup*      # проверить расписание
systemctl start backup.service     # прогнать руками, не дожидаясь воскресенья

Последняя команда — отдельное удовольствие: задача по расписанию тестируется штатно, тем же путём, каким запустится ночью, — с тем же окружением и тем же журналом. У cron «проверить как оно запустится» — всегда народное творчество.

Вердикт без фанатизма: важное — на таймеры, мелочь — можно на cron. Бэкапы, обновление сертификатов, синхронизации — то, чья молчаливая смерть дорога, — заслуживают журнала, одиночности и Persistent. Пятиминутный heartbeat или чистка tmp — переживут и crontab. А push в Kuma вешайте и там и там: journalctl расскажет почему умерло, но что умерло — первым скажет монитор.

Контрольный пример: сертификаты, которые продлеваются сами (правда ли?)

Соберём всю статью на одном жизненном кейсе. Certbot при установке сам заводит себе таймер — проверьте:

systemctl list-timers | grep certbot

Продление работает из коробки, и тут прячется самая коварная грабля темы: сертификат продлевается, а nginx продолжает раздавать старый — файл-то на диске обновился, но nginx держит в памяти тот, с которым стартовал. Итог: «у нас всё автоматизировано», а через 90 дней сайт встречает посетителей предупреждением браузера. Лечение — deploy-hook, команда, которую certbot выполнит после успешного продления:

# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx

(chmod +x не забыть.) Репетиция всего пайплайна, не дожидаясь настоящего продления:

certbot renew --dry-run
Перенос бесплатно
Хостинг стал тесным — переезжайте на свой VPS
Бесплатно поможем перенести проект без простоя и потери данных. Больше ресурсов, полный root-доступ и никаких тесных рамок.
Переехать на Siteko

И — по заветам этой статьи — страховка на случай, если вся эта автоматика однажды молча развалится: алерт Kuma «сертификат истекает через 7 дней» из четвёртой статьи. Заметьте архитектуру: автоматика делает работу, dry-run проверяет автоматику, внешний монитор проверяет результат независимо ни от чего. Три слоя — и тема сертификатов закрыта навсегда.

Грабли: сводная таблица

Грабля Симптом Лечение
Относительные пути / голый PATH «Руками работает, в cron — нет» Абсолютные пути, PATH в скрипте
Вывод в несуществующую почту Ошибки испаряются годами >> лог 2>&1 — или таймер с journalctl
Скрипт без set -e Бэкапы «делаются», файлы пустые set -euo pipefail в каждый скрипт
Наложение запусков Два rsync дерутся за каталог flock -n — или таймер (там не бывает)
% в crontab Задача с date молча мертва Логика — в скрипт, crontab — одна строка
Ребут в момент запуска Воскресный бэкап не случился Persistent=true у таймера
Никто не заметит смерть задачи «Последний бэкап — за март» Push-монитор Kuma на каждую важную задачу
Сертификат обновился, nginx — нет Через 90 дней — ошибка браузера deploy-hook с reload + --dry-run + алерт

Что в итоге

Рутина сервера теперь замкнута: диск чистится сам, сертификаты продлеваются сами (и перечитываются nginx'ом), бэкапы делаются сами — и, главное, о смерти любого из этих «сами» вы узнаете за часы, а не за месяцы, потому что каждая задача обязана регулярно доказывать, что жива. Правило, которое стоит унести: автоматизация без наблюдения — это отложенная авария; автоматизация с dead man's switch — это действительно снятая с человека работа.

Осталась одна большая рутина, которую мы ещё не автоматизировали, — доставка кода. Если вы всё ещё выкатываете сайт перетаскиванием файлов в FileZilla — следующая статья для вас: git pull вместо FTP, хук, который сбрасывает кэши (все обещания из статей 9 и 11 — в одном месте), выкатка одной командой и откат без слёз. CI/CD на своём VPS — звучит громко, делается за вечер.

Хостинг Siteko

VDS с первого дня

Свой сервер Siteko под полным контролем

NVMe-диски, root-доступ и честные ресурсы без «соседей». Поднимите VPN, бота или прод-проект и масштабируйтесь без тесных рамок.

  • Root-доступ полная свобода настройки под ваш стек.
  • NVMe и свежее железо скорость заметна с первого запроса.
  • Перенос бесплатно поможем переехать без простоя.
Выбрать VDS