Справочник · Терминал и система
Cron: расписание задач без ошибок
Пять полей crontab, шаги и диапазоны, готовые примеры расписаний и подводные камни: окружение, символ %, часовые пояса и одновременное условие на день месяца и день недели.
Cron запускает команды по расписанию. Он есть почти в каждом дистрибутиве и прост, пока не выясняется, что скрипт, прекрасно работающий из терминала, по расписанию молча ничего не делает. Ниже синтаксис и разбор таких случаев.
Формат строки
┌───────── минута 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 |
@hourly | 0 * * * * |
@daily или @midnight | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly или @annually | 0 0 1 1 * |
Управление crontab
crontab -e # редактировать свои задачи
crontab -l # показать
crontab -r # удалить ВСЕ свои задачи без вопросов
sudo crontab -u www-data -l # задачи другого пользователяКроме личных crontab есть системные файлы /etc/crontab и каталог /etc/cron.d/. В них между расписанием и командой стоит имя пользователя:
# /etc/cron.d/backup
30 2 * * * root /usr/local/bin/backup.shФайлы в /etc/cron.d/ должны принадлежать root, не быть доступными на запись группе и остальным, а в имени не должно быть точки. Файл backup.sh в этом каталоге будет просто проигнорирован.
Почему задача не запускается
Список причин в порядке того, как часто я на них натыкался.
- Другое окружение. Cron запускает команду с минимальным
PATH, обычно/usr/bin:/bin, без вашего.bashrcи без переменных из терминала. Пишите полные пути к программам или задайтеPATHв начале crontab. - Оболочка
sh, а неbash. Конструкции вроде[[ ]]или{1..5}в самой строке crontab не сработают. УкажитеSHELL=/bin/bashили вызывайте скрипт. - Символ
%. В crontab он означает перевод строки, всё после первого%уходит команде на вход.date +%Fнужно писать какdate +\%F. - Нет прав на запуск. Скрипту нужен бит
xили явный вызовbash script.sh. - Текущий каталог. Команда выполняется в домашнем каталоге пользователя. Относительные пути в скрипте ведут не туда, где вы их тестировали.
- Нет перевода строки в конце файла. Некоторые реализации игнорируют последнюю строку без завершающего перевода строки.
Хорошая заготовка начала crontab:
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>&1MAILTO="" отключает отправку вывода почтой. Без перенаправления в файл вывод задачи потеряется, а с 2>&1 в журнал попадут и ошибки.
Где смотреть журнал
grep CRON /var/log/syslog # Debian, Ubuntu
journalctl -u cron --since today # Debian, Ubuntu с systemd
journalctl -u crond --since today # Fedora, RHELВ журнале видно, что задача была запущена и от какого пользователя. Что она напечатала, видно только если вы сами перенаправили вывод в файл.
День месяца и день недели одновременно
Самая неочевидная часть синтаксиса. Если оба поля ограничены, то есть ни одно из них не *, задача сработает, когда выполнено любое из условий, а не оба сразу.
0 9 13 * 5 # каждое 13-е число И каждую пятницу, а не «пятница 13-е»Чтобы получить «пятницу 13-е», ограничьте одно поле, а второе проверьте в команде:
0 9 13 * * [ "$(date +\%u)" = 5 ] && /usr/local/bin/party.shТот же приём даёт последний день месяца, которого в классическом cron нет:
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 не даст запустить второй:
*/5 * * * * flock -n /tmp/sync.lock /srv/app/bin/syncС ключом -n второй запуск сразу завершится, если первый ещё держит файл.
systemd timers как альтернатива
На системах с systemd то же можно сделать таймером. Это два файла: служба и таймер.
# /etc/systemd/system/backup.service
[Unit]
Description=Ночная резервная копия
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh# /etc/systemd/system/backup.timer
[Unit]
Description=Запуск резервной копии каждую ночь
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers # когда следующий запуск
journalctl -u backup.service # вывод задачи уже в журналеПлюсы таймеров: журнал без перенаправлений, Persistent=true догоняет пропущенный запуск после выключения машины, легко ограничить ресурсы. Минус: два файла вместо одной строки. Для пары личных задач cron по-прежнему удобнее.