Cron и часовые пояса: CRON_TZ, TZ и летнее время
У строки crontab нет часового пояса. 0 9 * * 1-5 означает 9 утра в том поясе, в котором работает демон cron, а на облачном сервере это часто UTC, а не пояс людей, которым важна задача. Здесь — как узнать, в каком поясе работает cron, как его поменять и что происходит при переходе на летнее время.
В каком часовом поясе работает cron?
Cron использует системный пояс, который был установлен при запуске демона. Проверьте:
timedatectl | grep "Time zone" date
Если вы меняете системный пояс командой timedatectl set-timezone Europe/Moscow, перезапустите cron (systemctl restart cron, на RHEL — crond), иначе он может продолжить работать по старому. В Docker-контейнерах пояс часто не задан вовсе, и cron работает по UTC, пока вы не зададите TZ и не установите пакет tzdata.
Вариант 1: сервер в UTC, время пересчитываем
Многие команды держат все серверы в UTC и пишут расписания в UTC намеренно: логи со всех машин совпадают по времени, и ничего не сдвигается дважды в год. Цена — пересчитывать время самому. 9 утра по Москве — это 06:00 UTC круглый год, потому что в России нет перехода на летнее время. А вот 9 утра в Берлине — 08:00 UTC зимой и 07:00 UTC летом, и строка в UTC сдвигается на час относительно местного времени при переводе часов.
Вариант 2: CRON_TZ в crontab
cronie — cron в RHEL, Rocky, AlmaLinux, Fedora и Arch — поддерживает часовой пояс для расписания:
CRON_TZ=Europe/Moscow 0 9 * * 1-5 /srv/report.sh
Строки после CRON_TZ читаются в этом поясе. Cron в Debian и Ubuntu этого не поддерживает: переменная передаётся задаче как любая другая, а расписание остаётся в системном поясе. Прежде чем на это полагаться, загляните в man 5 crontab на своём сервере.
TZ=... в crontab — другое. Эта переменная задаёт пояс, который видит задача, так что date внутри скрипта печатает местное время, но само расписание не сдвигается.
Вариант 3: таймер systemd с часовым поясом
Таймеры systemd принимают пояс в конце OnCalendar:
[Timer] OnCalendar=Mon..Fri 09:00 Europe/Moscow Persistent=true
Как systemd это понимает, проверяет systemd-analyze calendar "Mon..Fri 09:00 Europe/Moscow". Остальная настройка — в гайде по таймерам systemd.
Kubernetes и другие планировщики
- Kubernetes CronJob: задайте
spec.timeZone, напримерtimeZone: "Europe/Moscow". Без него расписание идёт по поясу kube-controller-manager, обычно UTC. Подробнее — в гайде по CronJob. - Laravel:
->timezone('Europe/Moscow')у задачи илиschedule_timezoneвconfig/app.php. - Celery beat: настройка
timezoneилиcrontab(..., nowfun=...)для отдельной задачи. - node-cron: опция
timezoneуcron.schedule.
Летнее время
В России часы не переводят с 2014 года, но если ваши пользователи или серверы в Европе или США, это касается и вас. Дважды в год местное время либо пропускает час, либо повторяет его. В США и большей части Европы весной часы переводят на час вперёд, осенью — на час назад, ночью. Страдают задачи, назначенные на этот час:
- Весной времени 2:30 не существует. В зависимости от планировщика задача запустится сразу после перевода или не запустится в этот день вовсе.
- Осенью один час наступает дважды: с 1:00 до 2:00 в США и с 2:00 до 3:00 в Центральной Европе. Задача на 1:30 или 2:30 может запуститься два раза.
Cron в Debian особо обрабатывает переводы меньше трёх часов: задачи на фиксированное время, попавшие в пропущенный промежуток, запускаются сразу после перевода, а задачи в повторяющемся часе — один раз. В других реализациях по-разному. Надёжнее всего:
- Не ставить задачи на фиксированное время в промежуток 1:00–3:00 по местному времени в поясах с летним временем.
- Делать задачи безопасными для повторного запуска, например проверять, не создан ли уже сегодняшний отчёт.
- Или планировать по UTC, где летнего времени нет вовсе.
Мониторьте в том же поясе
Пропущенный запуск проще всего поймать, когда мониторинг точно знает расписание, в том же поясе, что и задача. В Heartcron у каждой проверки своё cron-выражение и свой часовой пояс, летнее время учитывается, так что 0 9 * * 1-5 в Europe/Berlin ожидается в 9 утра по Берлину круглый год:
heartcron checks create weekday-report --cron "0 9 * * 1-5" --tz Europe/Berlin --grace 15m
Потом добавьте пинг в задачу. Расшифровщик cron-выражений показывает ближайшие запуски в любом поясе — быстрый способ проверить, что значит выражение, до перевода часов и после.