Парадокс зайвої години: чому переведення годинників ламає бази даних, Cron та нерви інженерів
🇬🇧 Read this article in English
Щороку в останню неділю жовтня мільйони людей радіють: «Ура, ми поспимо на одну годину довше!».
Але в цей самий момент тисячі DevOps-інженерів, системних адміністраторів та бекенд-розробників прокидаються в холодному поту. Для серверної інфраструктури, фінансових систем і баз даних переведення стрілок на зимовий час (Daylight Saving Time / DST fallback) — це не зайва година сну, а щорічний сеанс чорної магії, коли фізичний час робить сальто назад.
О 04:00 ранку стрілки годинника перестрибують назад на 03:00. Проміжок між 03:00 та 04:00 відбувається двічі за одну ніч.
Що в цей момент робить ваш автоматичний білінг? Ваші щогодинні бекапи? Системи логування та моніторингу?
Сьогодні розберемося, чому робота з часом у програмуванні — це одне з найнебезпечніших мінних полів в IT, які катастрофи спричиняє «зайва година» і як налаштувати свій стек, щоб ніколи не стикатися з часовими парадоксами.
Мал. 1. Анатомія часового парадоксу: різниця між «настінним» часом із дубльованою годиною та лінійним часом UTC
1. Атака клонів у Cron: подвійне списання грошей
Почнемо з класики, на якій обпікалися навіть гіганти індустрії.
Уявіть типовий запис у системному планувальнику crontab, який запускає щоденне списання коштів за підписку або формує фінансовий звіт:
30 3 * * * /usr/bin/python3 /opt/billing/charge_subscriptions.py
Якщо ваш сервер налаштований на локальний часовий пояс (наприклад, Europe/Kyiv):
- 03:30 (літній час): Cron спрацьовує і успішно списує гроші з карток користувачів.
- 04:00: Настає переведення годинника. Операційна система відкочує стрілки назад на 03:00.
- 03:30 (зимовий час): Cron дивиться на годинник, бачить
03:30і запускає списання ще раз!
Клієнти бачать подвійне списання коштів, бухгалтерія хапається за серце, а підтримка захлинається гнівними тікетами.
А що відбувається навесні (Spring Forward)?
У березні о 03:00 стрілки стрибають одразу на 04:00. Години між 03:00 та 04:00 не існує взагалі. Будь-який скрипт, запланований на цей проміжок, просто не запуститься жодного разу: нічні бекапи пропущені, кеші не прогріті, індекси не перераховані.
2. Часові парадокси в базах даних: TIMESTAMP WITHOUT TIME ZONE
Якщо запитати досвідченого DBA про його головні тригери, на першому місці буде тип даних TIMESTAMP WITHOUT TIME ZONE.
Розглянемо просту таблицю логів або замовлень:
1
2
3
4
5
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
amount DECIMAL(10, 2)
);
В ніч переведення часу два клієнти роблять замовлення:
- Клієнт А замовив піцу о 03:45 (літній час).
- Через 20 хвилин стрілки переводять на годину назад.
- Клієнт Б замовляє піцу о 03:05 (зимовий час).
У вашій базі даних з’являються записи:
- Замовлення №1:
2026-10-25 03:45:00 - Замовлення №2:
2026-10-25 03:05:00
Спробуйте зробити простий запит:
1
SELECT * FROM orders ORDER BY created_at ASC;
База даних поверне Замовлення №2 раніше, ніж Замовлення №1, хоча фізично воно сталося пізніше! Якщо на основі часу вираховується черга обробки чи знижка першому покупцю — логіка системи ламається вщент.
3. Від’ємна математика: «Операція тривала -30 хвилин»
Типова помилка розробників-початківців — використання так званого «настінного» часу (Wall Clock) для вимірювання тривалості операцій:
1
2
3
4
5
6
7
8
9
10
11
12
13
import datetime
import time
start_time = datetime.datetime.now() # Наприклад: 03:50:00
# Виконуємо довгу операцію (тривалістю 20 хвилин)...
# Під час роботи скрипта годинник сервера відкочується назад на 1 годину!
end_time = datetime.datetime.now() # Стає: 03:10:00
duration = end_time - start_time
print(f"Тривалість виконання: {duration}")
# Виведе: -1 day, 23:20:00 (тобто -40 хвилин!)
До чого призводить від’ємний час у продакшені:
- Rate Limiter (обмежувачі частоти запитів): алгоритми Token Bucket або Leaky Bucket ділять кількість токенів на
delta_time. Ділення на від’ємне число або призводить до фатального краху бекенда, або повністю відкриває ворота для нескінченних запитів і DDoS. - SLA та Alerting: системи моніторингу (Prometheus / Grafana) бачать від’ємну затримку мережі (latency
-30000ms) і починають спамити чергових інженерів алертами.
Як це лікується в коді: Монотонний годинник (Monotonic Clock)
Для вимірювання тривалості ніколи не можна використовувати поточний календарний час. Потрібно використовувати монотонний таймер, який лічить тіки процесора з моменту завантаження системи і ніколи не йде назад:
1
2
3
4
5
import time
start = time.monotonic() # Монотонний лічильник
# ... операція ...
elapsed = time.monotonic() - start # Завжди гарантовано додатнє і точне число!
4. Справжні світові катастрофи через час
Думаєте, це тільки теоретичні проблеми? Ось кілька реальних випадків:
- Медичне обладнання в лікарнях: У 2012 році в США розслідували інцидент, коли комп’ютеризована система введення ліків пацієнтам у реанімації заблокувала подачу знеболювального, оскільки через стрибок годинника вирішила, що попередню дозу ввели «в майбутньому».
- Авіарейси та стикування: У багатьох авіакомпаній рейси тривалістю 50 хвилин у внутрішніх системах раптово отримували тривалість
-10 хвилин. Літак вилітав о 03:30, а прилітав о 03:20 за місцевим часом, що спричиняло падіння валідаторів розкладу. - Microsoft Azure Outage (2012): Хмарний сервіс Azure частково впав на день через баг високосного року та обробки часових зон у гостьових віртуальних машинах.
5. База IANA (tzdb): один волонтер тримає весь інтернет
Як комп’ютери взагалі знають, коли і в якій країні переводити час?
Для цього існує всесвітня база IANA Time Zone Database (tzdb), створена Артуром Девідом Олсоном ще в 1980-х роках. Багато років її практично власноруч підтримує один інженер — Пол Еггерт (Paul Eggert).
Це гігантський текстовий архів, де записані всі історичні зміни таймзон у світі:
1
2
3
4
Zone Europe/Kyiv 2:02:04 - LMT 1880
2:00 Russia EEST 1991 Sep 29 2:00s
2:00 C-Eur EE%sT 1996 May 13
2:00 EU EE%sT
Головний біль сучасності — політики. Уряди різних країн обожнюють приймати закони про скасування літнього часу за 3 дні до самого переведення.
- Якщо оновлення пакету
tzdataне встигли розкотити на всі операційні системи, смартфони та сервери — починається повний хаос: частина систем уже живе за новим часом, а частина — за старим.
Золоті правила для розробника та DevOps: як вижити
Щоб ваша інфраструктура спала спокійно в будь-яку неділю року, дотримуйтесь 4 залізних заповідей:
1. Сервери живуть ВИКЛЮЧНО за UTC
На серверах ніколи не повинно бути локального часу! Жодних Europe/Kyiv, America/New_York чи Asia/Tokyo. Перевірте свій сервер прямо зараз:
1
timedatectl
Якщо там не UTC — виправте це негайно:
1
sudo timedatectl set-timezone UTC
У часовому поясі UTC (Coordinated Universal Time) переведення годинників не існує в принципі. Там завжди рівномірні, монотонні 60 хвилин на годину, 24 години на добу.
2. Бази даних: тільки UTC або Unix Timestamp
- У PostgreSQL використовуйте
TIMESTAMPTZ(Timestamp with time zone). При збереженні Postgres автоматично переводить час в UTC, а при віддачі — форматує під клієнта. - Або зберігайте час як Unix Epoch (кількість секунд або мілісекунд, що минули з 1 січня 1970 року) — це абсолютне число, яке не залежить від часових поясів.
3. Завдання за розкладом: прив’язуйте до UTC або інтервалів
- Якщо завдання критичне (наприклад, списання коштів), налаштовуйте Cron виключно за часом UTC (наприклад, о 01:00 UTC).
- Або використовуйте Systemd Timers з фіксованими інтервалами замість конкретного часу доби:
1 2 3
[Timer] OnUnitActiveSec=24h AccuracySec=1m
4. Локальний час — тільки на фронтенді
Конвертація в локальний час користувача повинна відбуватися в останній момент у браузері або додатку. У JavaScript це робиться одним рядком через нативний API:
1
const localDate = new Date(utcTimestamp).toLocaleString();
Підсумки
Переведення годинників — це чудовий маркер якості інженерної культури. Якщо ваша система пережила останню неділю жовтня без дубльованих платежів, пропущених бекапів і падіння баз даних — вітаємо, ваш бекенд побудовано на совість!
А якщо ви ще не перевели сервери на UTC — саме час зробити це сьогодні під чашечку ранкової кави. ☕🕰️
