cron это старейший из живых инструментов Unix: идея будить задания по расписанию родилась в конце 1970-х, а современный облик планировщик получил в реализации Пола Викси на рубеже 1990-х. За десятилетия интерфейс не изменился ни на йоту, и это честная характеристика: проще уже некуда. При этом простота обманчива, и половина историй про "cron не работает" упирается не в планировщик, а в его молчаливые правила: минимальное окружение, символ процента в команде, строгие права на файлы расписаний. Ниже разобрано всё для уверенной ежедневной работы: где живут расписания, как читать строку задания, что делать с переменными и логами, как проверять расписание перед боем и как диагностировать ошибки за минуты, а не за вечер.

Как устроен cron и где живут его расписания

Служба cron читает таблицы заданий и просыпается раз в минуту, сверяя текущее время с расписаниями. Проверить, что она жива, стоит первой командой в любой диагностике:

systemctl status cron

Расписания живут в четырёх местах, и у каждого своя роль. Личное расписание пользователя редактируется командой crontab -e и просматривается как crontab -l, сами файлы лежат в спуле планировщика, и руками их не трогают: утилита перечитывает спул сама, а прямая правка ничего не даёт. Системная таблица /etc/crontab отличается полем пользователя в каждой строке. Каталог /etc/cron.d предназначен для пакетов и скриптов установки, каждый файл в нём это самостоятельное расписание. Наконец, каталоги cron.hourly, cron.daily, cron.weekly и cron.monthly содержат готовые сценарии, которые система прогоняет по календарю: ежедневные в ночь, еженедельные и ежемесячные с нарастающим интервалом.

Для машин, которые не работают круглосуточно, существует anacron: он не требует, чтобы сервер был включён ровно в три ночи, а догоняет пропущенные ежедневные задачи после старта. На ноутбуке это спасает сценарии обновлений и чисток, на круглосуточном сервере разницы почти нет. Ещё одна деталь про время: расписание способно уточнить собственный часовой пояс переменной в шапке файла, но по умолчанию планировщик живёт в системной зоне, и сервер с чужой зоной обязательно удивит расписанием на три часа раньше.

Отдельный класс вопросов рождается на серверах с разными зонами: до всех нестыковок, кто в какой зоне живёт, доходит не сразу, и расписание, казавшееся ночным, оказывается полуденным. Привычка смотреть date перед правкой расписаний дешевле любого другого метода диагностики.

Синтаксис строки crontab с примерами на разные случаи

Строка задания состоит из пяти полей времени и команды. Пять полей в начале строки означают следующее:

  1. минута, значения от 0 до 59;
  2. час, значения от 0 до 23;
  3. день месяца, значения от 1 до 31;
  4. месяц года, числа от 1 до 12 или короткие английские имена;
  5. день недели, числа от 0 до 7, где и ноль, и семь равны воскресенью.

Поверх значений работают спецсимволы: звёздочка означает любое значение, слэш задаёт шаг, дефис диапазон, запятая список. Чтение строк проще заучивания, и несколько примеров закрывают почти все жизненные случаи:

# каждый день в 3:30
30 3 * * * /root/backup.sh
# каждые пять минут
*/5 * * * * /usr/local/bin/healthcheck.sh
# по будням с 9 до 18, каждый час
0 9-18 * * 1-5 /root/report.sh
# 15-го числа каждого месяца в 4:10
10 4 15 * * /root/monthly.sh
# каждую среду и пятницу в полдень
0 12 * * 3,5 /root/check.sh
# каждые 6 часов, на границе смены
0 */6 * * * /root/sync.sh
# с 1 по 10 число месяца в 6 утра
0 6 1-10 * * /root/decade-check.sh

Диапазон со шагом читается как от и до с шагом: запись 10-18/2 в поле часа разбудит задание в 10, 12, 14, 16 и 18. Имена месяцев и дней недели допустимы вместо чисел и делают строки читаемее, JAN и MON понятнее единиц. Пустое поле означает никогда, поэтому звёздочки в примерах обязательны: строка без звёздочек в полях дня месяца и недели просто не совпадёт с большинством дат. Смешивать имена и числа внутри одного поля нельзя, а вот в разных полях это два независимых решения. Для самопроверки строка читается вслух от первого поля к последнему: минута, час, число, месяц, день недели. Словесное чтение ловит опечатки в разы быстрее, чем беглый взгляд на цифры.

Специальные строки и переменные окружения в crontab

Частые расписания имеют короткие псевдонимы: @reboot выполняется при старте службы, @yearly и @monthly, @weekly и @daily говорят сами за себя, @hourly равносилен записи ноль и четырём звёздочкам. Псевдоним ставится вместо всех пяти полей:

@daily /root/daily-cleanup.sh
@reboot /root/warm-cache.sh

Окружение задания задаётся в начале файла, и здесь живут два самых коварных умолчания. Оболочкой по умолчанию назначается sh, а не bash, и скрипты с башизмами падают с непонятными ошибками. Путь по умолчанию урезан до пары системных каталогов, и команда, которая находится в терминале, в cron внезапно оказывается ненайденной. Обе проблемы лечатся тремя строками в шапке:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

Переменная MAILTO управляет почтой: по умолчанию вывод каждого задания уходит локальной почтой владельцу расписания, и на серверах без настроенного почтового стека эти письма годами копятся мёртвым грузом. Пустое значение отключает рассылку, конкретный адрес перенаправляет вывод администратору, что удобно для заданий, чей вывод и есть отчёт.

И главная ловушка синтаксиса, символ процента. В поле команды процент означает перевод строки, и команда с форматом даты разрезается на первой же встрече. Экранирование обратным слэшем спасает:

0 0 * * * cp /var/log/app.log /var/backups/app-$(date +\%F).log

Без экранирования такой запуск выполнит обрезанную команду, а хвост уйдёт в стандартный ввод, и в журнале будет тишина вместо ошибки. Процент встречается в date, printf и SQL-сценариях чаще, чем кажется, и это первое, что проверяют в неработающей строке с датами.

Проверка расписания и защита от повторных запусков

Новое расписание проверяется до боя, и процедура занимает пять минут. Сначала снимается копия текущей таблицы, чтобы откат стал секундным:

crontab -l > /root/crontab-backup

Потом добавляется тестовая строка с минутным интервалом и маркером в файл, и по файлу видно, что планировщик подхватил правку. Отработав, тестовая строка удаляется, а настоящее задание запускается вручную теми же командами: это отделяет ошибки расписания от ошибок сценария.

Второй ритуал раздела это защита от повторных запусков. Длинное задание, например бэкап на медленном канале, может не успеть завершиться до следующего расписания, и второй прогон ляжет поверх первого, портя оба. Лечение однострочное: утилита flock занимает лок, и новый запуск при живом локе завершается сразу:

30 3 * * * flock -n /var/run/backup.lock /root/backup.sh

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

Логирование заданий и наблюдение за их выполнением

Сам планировщик пишет о запусках в системный журнал, и первые две команды диагностики выглядят так:

grep CRON /var/log/syslog | tail -10
journalctl -u cron --since "1 hour ago"

Записи покажут момент запуска и владельца, но не результат: вывод задания попадает в почту или в никуда. Для собственного журнала задание снабжается перенаправлением:

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

Сценарии, которые пишут собственные сообщения, удобнее отправлять командой logger с тегом: строка попадает в системный журнал и ищется по имени без отдельного файла. Журналы заданий тоже ротируются, для собственного файла достаточно короткого сценария в logrotate с ежедневной ротацией и хранением за месяц. И наблюдение строится на простой метрике, когда был последний успешный прогон: метка из прошлого раздела и две строки на проверяющем сервере закрывают вопрос без внедрения тяжёлых систем. Полезная привычка журналировать и старт, и финиш: строка в начале сценария и строка в конце дают длительность прогона бесплатным бонусом, а длительность бэкапа, выросшая вдвое за месяц, расскажет о проблеме раньше, чем она станет аварией.

Системные расписания в /etc/cron.d и права на них

Каталог /etc/cron.d хранит расписания системных задач, и у него жёсткий характер. Файл обязан принадлежать root, иметь права на чтение без исполняемого бита, и в его имени не должно быть точки: файлы с точками планировщик молча игнорирует, это защита от временных файлов редакторов. Внутри обязательным шестым полем идёт пользователь:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

30 3 * * * root /root/backup.sh

Право на собственное расписание настраивается файлами cron.allow и cron.deny: пустой cron.allow запрещает всем, кроме перечисленных, пустой cron.deny разрешает всем, кроме перечисленных. Полезная дисциплина для серверов, где работают коллеги: список тех, кому можно, всегда перед глазами.

Скрипты в каталогах cron.daily наоборот требуют исполняемого бита: это каталоги для готовых программ, а не расписаний. Различие запоминается по логике: в cron.d лежат тексты расписаний, в cron.daily лежат сами задания. Свои системные задачи разумно держать отдельными файлами в cron.d с говорящими именами: через полгода будет видно, кто положил расписание и зачем, а корневой crontab останется коротким и личным. Чужие таблицы администратор смотрит командой crontab -u имя -l, это работает и для диагностики заданий сервисных учёток. Для повторяемости файлы cron.d раздаются на все серверы средствами конфигурационного управления: расписание из общего репозитория можно отревьюить вместе с кодом, а расписание, живущее только в памяти одного сервера, со временем превращается в легенду, которую никто не проверит.

Типичные ошибки cron и их решение по шагам

Первая и самая частая ошибка звучит как "работает в терминале, но не в cron". Причина почти всегда в окружении: урезанный путь и оболочка sh вместо bash. Решение расписано в разделе о переменных, а быстрый тест делает скрипт, выводящий окружение в файл, сравнение с терминалом занимает минуту.

Вторая ошибка, процент в команде, разрезает строку с форматами дат и лечится экранированием. Третья: задание запускается, но ничего не делает, у сценария нет исполняемого бита или первой строки-интерпретатора, и в журнале остаётся только факт запуска без ошибок. Четвёртая: не хватает перевода строки в конце файла, последняя строка расписания может молча не подхватиться, поэтому crontab всегда заканчивается пустой строкой.

Пятая: файл в cron.d игнорируется, точки в имени, исполняемый бит или владелец не root. Шестая: служба остановлена или замаскирована, systemctl status отвечает раньше любых подозрений в собственном коде. Седьмая тонкость семантики дат: если одновременно ограничены день месяца и день недели, сработает любое из совпадений, а не их пересечение, и задание "первое число и понедельник" запустится в оба дня. Восьмая: переходы на летнее время, в ночь смены часов задания могут выполниться дважды или не выполниться вовсе, поэтому критичные сценарии пишут идемпотентными, повторный запуск не должен ломать результат. Девятая: переменные, экспортированные в .bashrc, до планировщика не долетают, всё нужное задаётся в шапке расписания или внутри сценария. И десятая мелочь на закуску: пути с пробелами в поле команды требуют кавычек, иначе планировщик честно выполнит только первый кусок пути. Кавычки в команде работают как в оболочке, и это спасает сценарии с путями вроде папки с пробелом в имени.

cron остаётся рабочей лошадкой десятилетия за десятилетием: у таймеров systemd есть свои козыри, но простота текстового расписания не устареет. Знание десятка мелочей превращает планировщик из источника загадок в скучный и надёжный механизм, а скучная инфраструктура это комплимент.