Лаборатория кодера

Справочник · Терминал и система

Cron: расписание задач без ошибок

Пять полей crontab, шаги и диапазоны, готовые примеры расписаний и подводные камни: окружение, символ %, часовые пояса и одновременное условие на день месяца и день недели.

Обновлено · 7 мин чтения

Cron запускает команды по расписанию. Он есть почти в каждом дистрибутиве и прост, пока не выясняется, что скрипт, прекрасно работающий из терминала, по расписанию молча ничего не делает. Ниже синтаксис и разбор таких случаев.

Формат строки

text
┌───────── минута        0–59
│ ┌─────── час           0–23
│ │ ┌───── день месяца   1–31
│ │ │ ┌─── месяц         1–12 или jan–dec
│ │ │ │ ┌─ день недели   0–7 или sun–sat (0 и 7 = воскресенье)
│ │ │ │ │
* * * * *  команда
ЗаписьЗначениеПримерЧитается как
*любое значение* * * * *каждую минуту
5конкретное значение5 * * * *в 5 минут каждого часа
1,15список0 9 1,15 * *1-го и 15-го числа в 9:00
1-5диапазон0 9 * * 1-5по будням в 9:00
*/10шаг*/10 * * * *каждые 10 минут
8-18/2шаг в диапазоне0 8-18/2 * * *в 8, 10, 12, 14, 16 и 18 часов

Проверить, когда сработает выражение, можно в разборе cron-выражений: он показывает описание словами и ближайшие запуски.

Готовые расписания

ВыражениеКогда
*/5 * * * *каждые 5 минут
0 * * * *в начале каждого часа
30 2 * * *каждый день в 2:30
0 0 * * 0по воскресеньям в полночь
0 9 * * 1-5по будням в 9:00
0 18 * * friпо пятницам в 18:00
0 0 1 * *первого числа каждого месяца
0 3 1 1,4,7,10 *раз в квартал, 1-го числа в 3:00
15 4 * * 1#1так нельзя: # есть не во всех реализациях cron
0 12 L * *так тоже нельзя в классическом cron: L из Quartz

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

Короткие формы поддерживаются в cronie и Vixie cron, то есть почти везде:

ЗаписьЭквивалент
@rebootодин раз при запуске демона cron
@hourly0 * * * *
@daily или @midnight0 0 * * *
@weekly0 0 * * 0
@monthly0 0 1 * *
@yearly или @annually0 0 1 1 *

Управление crontab

bash
crontab -e            # редактировать свои задачи
crontab -l            # показать
crontab -r            # удалить ВСЕ свои задачи без вопросов
sudo crontab -u www-data -l   # задачи другого пользователя

Кроме личных crontab есть системные файлы /etc/crontab и каталог /etc/cron.d/. В них между расписанием и командой стоит имя пользователя:

text
# /etc/cron.d/backup
30 2 * * * root /usr/local/bin/backup.sh

Файлы в /etc/cron.d/ должны принадлежать root, не быть доступными на запись группе и остальным, а в имени не должно быть точки. Файл backup.sh в этом каталоге будет просто проигнорирован.

Почему задача не запускается

Список причин в порядке того, как часто я на них натыкался.

  1. Другое окружение. Cron запускает команду с минимальным PATH, обычно /usr/bin:/bin, без вашего .bashrc и без переменных из терминала. Пишите полные пути к программам или задайте PATH в начале crontab.
  2. Оболочка sh, а не bash. Конструкции вроде [[ ]] или {1..5} в самой строке crontab не сработают. Укажите SHELL=/bin/bash или вызывайте скрипт.
  3. Символ %. В crontab он означает перевод строки, всё после первого % уходит команде на вход. date +%F нужно писать как date +\%F.
  4. Нет прав на запуск. Скрипту нужен бит x или явный вызов bash script.sh.
  5. Текущий каталог. Команда выполняется в домашнем каталоге пользователя. Относительные пути в скрипте ведут не туда, где вы их тестировали.
  6. Нет перевода строки в конце файла. Некоторые реализации игнорируют последнюю строку без завершающего перевода строки.

Хорошая заготовка начала crontab:

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

*/10 * * * * cd /srv/app && ./bin/sync >> /var/log/app-sync.log 2>&1

MAILTO="" отключает отправку вывода почтой. Без перенаправления в файл вывод задачи потеряется, а с 2>&1 в журнал попадут и ошибки.

Где смотреть журнал

bash
grep CRON /var/log/syslog            # Debian, Ubuntu
journalctl -u cron --since today     # Debian, Ubuntu с systemd
journalctl -u crond --since today    # Fedora, RHEL

В журнале видно, что задача была запущена и от какого пользователя. Что она напечатала, видно только если вы сами перенаправили вывод в файл.

День месяца и день недели одновременно

Самая неочевидная часть синтаксиса. Если оба поля ограничены, то есть ни одно из них не *, задача сработает, когда выполнено любое из условий, а не оба сразу.

text
0 9 13 * 5     # каждое 13-е число И каждую пятницу, а не «пятница 13-е»

Чтобы получить «пятницу 13-е», ограничьте одно поле, а второе проверьте в команде:

text
0 9 13 * * [ "$(date +\%u)" = 5 ] && /usr/local/bin/party.sh

Тот же приём даёт последний день месяца, которого в классическом cron нет:

text
55 23 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && /usr/local/bin/month-close.sh

Часовой пояс и перевод часов

Cron работает по часовому поясу системы. На серверах его обычно ставят в UTC, и тогда 0 9 * * * сработает в 9 утра по UTC, то есть в 12:00 по Москве. Проверить пояс можно командой timedatectl.

В зонах с переходом на летнее время задачи между 2 и 3 часами ночи в день перевода могут выполниться дважды или ни разу. Поэтому важные задачи не ставят на этот промежуток, а серверам задают UTC. Подробнее про пояса в отдельной статье.

Некоторые реализации, например cronie, понимают переменную CRON_TZ=Europe/Moscow в начале crontab, но это не переносимо.

Защита от наложения запусков

Если задача иногда работает дольше интервала, два экземпляра начнут мешать друг другу. Утилита flock из пакета util-linux не даст запустить второй:

text
*/5 * * * * flock -n /tmp/sync.lock /srv/app/bin/sync

С ключом -n второй запуск сразу завершится, если первый ещё держит файл.

systemd timers как альтернатива

На системах с systemd то же можно сделать таймером. Это два файла: служба и таймер.

ini
# /etc/systemd/system/backup.service
[Unit]
Description=Ночная резервная копия

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ini
# /etc/systemd/system/backup.timer
[Unit]
Description=Запуск резервной копии каждую ночь

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers              # когда следующий запуск
journalctl -u backup.service       # вывод задачи уже в журнале

Плюсы таймеров: журнал без перенаправлений, Persistent=true догоняет пропущенный запуск после выключения машины, легко ограничить ресурсы. Минус: два файла вместо одной строки. Для пары личных задач cron по-прежнему удобнее.