14 мар. 2026 г.·7 мин чтения

Ошибки часовых поясов в глобальных продуктах: дрейф cron и способы исправления

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

Ошибки часовых поясов в глобальных продуктах: дрейф cron и способы исправления

Почему планирование ломается между часовыми поясами

График может выглядеть правильно на сервере и при этом разочаровывать пользователей. Сервер следует одному времени. Ваши пользователи — своему локальному времени в своём городе с собственными правилами перехода на летнее время.

Эта рассинхронизация сначала наносит тихий вред. Выставление счетов срабатывает не в полночь по локальному времени. Письма-напоминания приходят на час позже. Ежедневные отчёты включают лишние данные или пропускают первый час дня. Команды часто замечают баг только после того, как копятся обращения в поддержку.

Несколько областей обычно падают первыми:

  • счета и возобновления подписок около полуночи
  • напоминания, привязанные к рабочим часам
  • ежедневные и ежемесячные отчёты
  • окна бронирования и крайние сроки

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

Простая запись в cron не ответит на эти вопросы за вас. Она лишь следует тому времени, которое вы ей дали. Если продукт говорит «отправлять в 9:00 по местному времени», вам нужно больше, чем серверный cron. Нужны сохранённая часовая зона, правило для сдвигов DST и способ решить, что считается одним запланированным запуском.

Простой простой простой выбор появляется и при простое системы. Если она была отключена на три часа, пропускаете ли вы пропущенные задания или воспроизводите их после восстановления? Ни один из вариантов не универсален. Пропущенное напоминание о приёме у врача всё ещё может иметь значение. Воспроизведение списания за услугу может создать ещё большую проблему.

Большинство этих багов возникают не из-за сломанного кода, а из-за отсутствия правил. Система часто работает ровно так, как написано. Проблема в том, что никто не прописал, как должно вести себя время.

Часы, которых на самом деле придерживается ваш продукт

Баги планирования обычно начинаются, когда продукт смешивает несколько часов и считает их одним — а это не одно и то же.

Сервер имеет своё время. Пользователь живёт в локальном часовом поясе. Бизнес может руководствоваться совсем другим правилом, например «каждый рабочий день в 9:00 по Нью‑Йорку». Если вы смешаете их, проблемы с таймингом проявятся быстро.

Храните метки времени событий в UTC. Это даёт одну стабильную запись о том, когда что‑то произошло, независимо от того, где работает сервер и где пользователь открывает приложение. Логи, аудиты, история заданий и очереди легче сортировать, когда у них одна базовая временная шкала.

Но UTC недостаточен для будущих расписаний. Если пользователь говорит «Отправлять каждый день в 8:00», вам также нужна IANA часовая зона пользователя, например America/New_York или Europe/Berlin. Не храните только «+02:00». Смещение — это снимок. Оно не подскажет, что произойдёт при начале или окончании перехода на летнее время.

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

Простая модель помогает:

  • Время события — когда что‑то действительно произошло, сохраняется в UTC.
  • Время пользователя — то, что человек ожидает увидеть на экране.
  • Время сервера — машинный часы, которыми запускается код.
  • Бизнес‑время — правило, которое важно для компании.

Эти часы могут указывать в разные стороны. Зарплатное задание может выполняться по лондонскому бизнес‑времени, даже если сервер в Вирджинии, а менеджер в Сингапуре. Как только это явно, многие ошибки планирования исчезают.

Где cron помогает, а где — нет

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

Проблемы начинаются, когда команды ожидают от cron понимания бизнес‑времени. Большинство cron‑настроек использует часовой пояс машины, если не сконфигурировать иначе. Это значит, что «запуск в 09:00» часто означает 09:00 на том сервере, а не 09:00 для клиента, офиса или рынка, которые вам важны.

Незаметные изменения в инфраструктуре могут сместить время без изменения строки cron. Перенесли задачу на новый сервер, пересобрали контейнер с другим часовым поясом или изменили настройки хоста при обслуживании — задача всё ещё запустится, просто в неправильный час.

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

Cron также не гарантирует ровные интервалы между успешными запусками. Команды часто думают, что «каждые 5 минут» значит идеальный пяти‑минутный интервал навсегда. В реальных системах есть очереди, повторы и медленные воркеры. Если один запуск начался с опозданием, следующий может поставить в очередь позади него, или повтор может перенести работу в следующее окно. Со временем этот дрейф попадёт в отчёты, счета, напоминания и синхронизации.

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

Что делает с расписаниями переход на летнее время

Переход на летнее время ломает простое предположение, что каждый день длится 24 часа. При весеннем переходе во многих местах время прыгает с 01:59 на 03:00. Если задача стоит на 02:30 по локальному времени, этот запуск не произойдёт, потому что такого момента не существует.

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

Это быстро становится дорого. Ежедневный отчёт может охватывать 23 часа весной и 25 часов осенью. Суммы меняются. Графики выглядят странно. Пороговые оповещения могут сработать слишком рано или слишком поздно. Если отчёт попадает в расчёт зарплаты, комиссий или счетов, один пропущенный или повторяющийся час может превратиться в серьёзный спор.

Те же ошибки повторяются снова и снова:

  • напоминания исчезают, когда запланированная минута пропускается
  • письма уходят дважды, когда час повторяется
  • ежедневные сводки охватывают более короткие или длинные периоды, чем ожидается
  • окна выставления счетов перекрываются или оставляют пробелы

Самое безопасное — не называть задачу «ежедневной», пока вы не определите, что это значит. Она запускается по локальному времени каждого пользователя? Или каждые 24 часа в UTC? Если локальное время пропущено, пропустить ли запуск или перенести на следующую валидную минуту? Если локальное время повторяется, запускать один раз или дважды?

Финансовые задания требуют более строгих правил. Определите окна выставления счетов точными UTC‑метками, а затем отображайте их в локальном времени в интерфейсе. Это сохраняет стабильность расчётов, даже когда локальные часы меняются.

«Запускать каждый день в 02:30» — это не полноценное требование. Одного предложения обычно достаточно, чтобы решить, будет ли система казаться надёжной или сломанной дважды в год.

Простая история с отказом

План надежного восстановления
Решите, когда пропускать, воспроизводить или объединять пропущенные задания после простоя.

Магазин по подписке обещал клиентам напоминание о продлении в 9:00 утра по их городу. Звучало просто. Команда сохранила выбранное пользователем время и смещение UTC на день регистрации, затем планировала отправки по этому смещению.

Сначала всё выглядело нормально. Клиент в Нью‑Йорке зарегистрировался в январе, когда смещение было UTC‑5, поэтому приложение отправляло напоминание в 14:00 UTC. Каждый месяц система повторяла то же смещение.

Затем наступил сезонный переход. Нью‑Йорк перешёл на летнее время и стал UTC‑4, но приложение всё ещё считало клиента как UTC‑5. Напоминание пришло в 8:00 вместо 9:00.

Ничто не упало. Тревога не сработала. Задание выполнилось ровно по своим правилам, но правило было неверным.

Поддержка заметила это первой. Люди писали: «Я просил 9:00, почему это пришло в 8:00?» Некоторые отключили напоминания. Кто‑то отписался, потому что сообщение пришло в неподходящее время и выглядело небрежно. Команда думала, что это проблема рассылки, но настоящая проблема была в расписании.

Исправление оказалось простым. Приложению нужно было сохранить фактическую часовую зону, например America/New_York, а не только смещение, которое было при регистрации. Затем перед каждой отправкой нужно было пересчитывать, что значит 9:00 по местному времени в ту дату.

Смещение — это снимок. Часовая зона — набор правил. Если вы храните снимок и выбрасываете правило, дрейф — лишь вопрос времени.

Прописание правил планирования, которые выдерживают

Начните с предложения, которое может проверить кто‑то вне инженерии. «Отправлять напоминание о счёте в 9:00 по местному времени клиента в первый рабочий день месяца» — лучше, чем просто строка cron. Это простое предложение и есть настоящее правило.

Затем превратите его в несколько решений:

  • Решите, следует ли задание UTC или локальным часам. Резервные копии, синхронизации данных и сбросы лимитов часто подходят для UTC. Клиентские напоминания, открытия магазинов и расчёт зарплаты обычно требуют локального времени.
  • Храните следующий запланированный запуск, а не только последний успешный запуск. Также сохраняйте версию правила, чтобы знать, какая логика сгенерировала расписание.
  • Определите поведение для отсутствующих времён. Если город пропускает 02:30 при весеннем переходе, у системы должно быть прописанное правило: пропустить, запустить в 03:00 или в ближайшую валидную минуту.
  • Определите поведение для повторяющихся времён. Когда 01:30 случается дважды осенью, решите, запускать ли задачу один раз или дважды. Большинство задач, ориентированных на клиентов, должны запускаться один раз.
  • Протестируйте хотя бы один весенний и один осенний переход перед выпуском. Если вы тестируете только обычные недели, вы пропустите важные случаи.

Выбор способа хранения важен сильнее, чем многие команды ожидают. Если сервис падает на три часа, процесс восстановления должен сравнить «что должно было произойти» с «что произошло». Если вы храните только время последнего запуска, расписание может понемногу дрейфовать после каждого простоя, пока разрыв не станет трудным для объяснения.

Небольшой пример. Магазин отправляет напоминание о самовывозе в 8:00 по местному времени. Если вы планируете в UTC, клиенты в Берлине и Чикаго получат разное локальное поведение. Если планируете по локальному времени, но игнорируете весенний переход, некоторые напоминания вовсе не уйдут. Если вы бездумно воспроизведёте всё после восстановления, некоторые клиенты получат два напоминания.

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

Как должны работать восстановительные задания после простоя

Нагрузочное тестирование планировщика
Пройдитесь по переходам на DST, простоям и повторам вместе с Fractional CTO.

Когда планировщик возвращается после простоя, он не должен догадываться. Нужно иметь правило для каждого пропущенного запуска. Если оставить выбор на поведение cron по умолчанию или на ad hoc код, пользователи получат дубли писем, поздние отчёты или пропущенные данные без понятного объяснения.

Большинству команд достаточно трёх вариантов:

  • Пропустить пропущенный запуск, если старая работа больше не важна — например, обновление live дашборда.
  • Воспроизвести каждый пропущенный запуск, если важен каждый временной слот — например, расчёт зарплаты, выставление счетов или выгрузки для соответствия.
  • Объединить пропущенные запуски в один восстановительный запуск, если пользователям важен итоговый статус, а не каждое промежуточное состояние.

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

Установите жесткое окно воспроизведения. Это не даст одной плохой ночи превратиться в очередь на два дня. Можно воспроизводить до 24 пропущенных почасовых запусков или до 7 пропущенных ежедневных запусков, а всё старше — пропускать и помечать явно. Без такого лимита восстановительный трафик может обрушить базу данных в момент, когда система и так перегружена.

Каждое воспроизведение должно быть идемпотентным. Если один и тот же запланированный запуск выполнится дважды, результат всё равно должен быть корректным. Используйте стабильный идентификатор запуска, привязанный к намеренному времени запуска, а не ко времени восстановления. Это предотвратит двойные списания, дубли уведомлений и повторные экспорты файлов.

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

Короткая заметка для поддержки тоже помогает: «Сервис был недоступен с 02:10 до 03:00 UTC. Мы воспроизвели два пропущенных биллинговых запуска. Пользователи могут увидеть задержанные квитанции, но дублирующих списаний нет.» Чёткий язык снижает панику после восстановления.

Ошибки, которые вызывают дрейф

Многие команды всё ещё хранят «UTC+2» вместо Europe/Berlin. Этот упрощённый приём работает часть года, а затем локальные часы меняются, и задание в 9:00 начинает срабатывать в 8:00 или 10:00. Реальные часовые зоны включают изменения правил. Смещения — нет.

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

Повторы ухудшают ситуацию. Допустим, письмо‑напоминание таймаутнуло, и воркер пытается снова через три минуты. Без токена дедупликации пользователь может получить два письма. Тогда другой воркер может записать вторую попытку как официальную отправку, и ваша история начнёт расходиться с тем, что пользователи реально увидели.

Распределённые системы добавляют ещё одну ловушку. Один воркер загрузил новое расписание, другой держит старую версию в памяти, и оба продолжают работать. Один узел срабатывает в 09:00, другой — в 09:15, и рассинхрон остаётся незамеченным, пока клиенты не начнут жаловаться. Общий статус расписания, версионированная конфигурация и ясные правила перезагрузки предотвращают такое рассечение.

Старая и всё ещё распространённая ошибка — считать, что каждый день длится 24 часа. Некоторые дни — 23, некоторые — 25. При весеннем переходе 02:30 может не существовать. При осеннем 01:30 повторяется. Если пользователь просил «каждый день в 9:00 по локальному времени», не превращайте это незаметно в «каждые 24 часа». Это разные правила.

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

Проверки перед релизом

Аудит ваших правил времени
Пусть Oleg проверит логику DST, cron и восстановительных задач, пока пользователи не заметили ошибки.

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

Начните с самого правила. Если вас спросили, что происходит при начале и окончании DST, команда должна ответить одним предложением. «Запускать в 9:00 по местному времени, пропускать отсутствующие времена при начале DST и запускать один раз после окончания DST» — ясно. «Запускать каждые 24 часа» — это другое правило.

Перед релизом проверьте несколько вещей:

  • Напишите по одному примеру для начала и конца DST с реальными датами и одной именованной часовой зоной.
  • Убедитесь, что только один воркер может заявить выполнение запланированного запуска. Блокировка, аренда или уникальная запись задания лучше, чем надежда.
  • Протестируйте восстановление после простоя. Система должна знать, пропускать, воспроизводить или объединять пропущенные запуски, и не должна отправлять одно и то же письмо дважды или списывать деньги дважды.
  • Дайте поддержке трассируемый путь. Она должна видеть правило, запланированное время, фактическое время выполнения и итог в одном месте.
  • Прогоните те же сценарии для пользователей в Токио, Берлине и Нью‑Йорке. Если поведение меняется, вы должны уметь объяснить, почему.

Так команды обнаруживают, готовы ли их логи. Если клиент говорит «Моё напоминание пришло на час позже», поддержке не должно приходиться рыться в сырых логах трёх систем. Ей нужен один запись, показывающая часовую зону пользователя, сохранённое правило, вычисленное следующее время запуска и то, что реально случилось.

Если вы можете объяснить эти случаи до релиза, расписание, вероятно, готово. Если нет — код ещё не закончен.

Что делать дальше

Начните с инвентаризации. Команды обычно помнят очевидные задания, вроде ночных отчётов или биллинга, но забывают тихие: напоминания о продлении, истечение пробного периода, сбросы дневных лимитов, выгрузки по зарплате и повторы, привязанные к локальным рабочим часам. Запишите всё и отметьте, какие задания зависят от локального времени.

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

Дайте продукту, поддержке и инженерии один общий формат расписания. Он должен быть достаточно прост, чтобы сотрудник поддержки прочитал и не гадал. Хорошее правило звучит так: «Запускать в 9:00 по часовому поясу аккаунта, один раз в календарный день, пропускать дубли при переводе часов назад и догонять только если пропуск меньше 6 часов.»

Проведите столовую тренировку перед следующим релизом. Выберите день со «весенним вперёд», день со «осенним назад» и один простой с отключением длительнее интервала задания. Спросите каждую команду, что должно произойти. Если ответы расходятся — спецификация всё ещё слишком расплывчата.

Именно здесь начинается большинство проблем. Код может быть в порядке, но правило расплывчато. Отсюда и дрейф cron, дублирующие отправки и пропущенные запуски в продакшене.

Если ваш продукт работает в нескольких регионах, проверка со стороны того, кто уже видел такие отказы, может сэкономить много исправлений. Oleg Sotnikov на oleg.is делает такую работу Fractional CTO, включая обзоры правил планирования, логики восстановления и инфраструктурных решений. Один внешний проход часто дешевле, чем исправление просчёта по зарплате или день с дублирующимися сообщениями клиентам.