Post

Парадокс зайвої години: чому переведення годинників ламає бази даних, Cron та нерви інженерів

Парадокс зайвої години: чому переведення годинників ламає бази даних, 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):

  1. 03:30 (літній час): Cron спрацьовує і успішно списує гроші з карток користувачів.
  2. 04:00: Настає переведення годинника. Операційна система відкочує стрілки назад на 03:00.
  3. 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. Справжні світові катастрофи через час

Думаєте, це тільки теоретичні проблеми? Ось кілька реальних випадків:

  1. Медичне обладнання в лікарнях: У 2012 році в США розслідували інцидент, коли комп’ютеризована система введення ліків пацієнтам у реанімації заблокувала подачу знеболювального, оскільки через стрибок годинника вирішила, що попередню дозу ввели «в майбутньому».
  2. Авіарейси та стикування: У багатьох авіакомпаній рейси тривалістю 50 хвилин у внутрішніх системах раптово отримували тривалість -10 хвилин. Літак вилітав о 03:30, а прилітав о 03:20 за місцевим часом, що спричиняло падіння валідаторів розкладу.
  3. 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 — саме час зробити це сьогодні під чашечку ранкової кави. ☕🕰️

This post is licensed under CC BY 4.0 by the author.