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 с примерами на разные случаи
Строка задания состоит из пяти полей времени и команды. Пять полей в начале строки означают следующее:
- минута, значения от 0 до 59;
- час, значения от 0 до 23;
- день месяца, значения от 1 до 31;
- месяц года, числа от 1 до 12 или короткие английские имена;
- день недели, числа от 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 есть свои козыри, но простота текстового расписания не устареет. Знание десятка мелочей превращает планировщик из источника загадок в скучный и надёжный механизм, а скучная инфраструктура это комплимент.