Модуль 13

Message persistence та reliability

Ось готовий урок, створений у стилі CS50: енергійний, з життєвими аналогіями та фокусом на архітектурному мисленні.


🎓 CS50-style: Message Persistence та Reliability

(Як не загубити дані, коли все летить шкереберть)

Привіт, друзі! 👋 Сьогодні ми зануримося в одну з найнапруженіших тем backend-розробки.

1. 🔥 Вступ: Кошмар розробника

Уявіть ситуацію. Ви будуєте систему для банку. Клієнт переказує $1,000. Ваша система приймає запит, каже клієнту: "Все супер, гроші пішли!"... і в цю ж мілісекунду в дата-центрі блимає світло, і ваш сервер перезавантажується.

Система піднімається. Клієнт перевіряє рахунок — грошей немає. Отримувач перевіряє — грошей теж немає. Транзакція зникла. Просто розчинилася у повітрі.

Питання до вас: Як ви думаєте, що зробить цей клієнт? (Спойлер: він змінить банк і напише гнівний твіт, а вас звільнять).

Чому це сталося? Тому що ми довірилися оперативній пам’яті (RAM). А RAM — це як дошка в аудиторії: поки світло горить і ми не стерли — інформація є. Вимкнули світло — дошка чиста.

Сьогодні ми навчимося робити наші повідомлення "безсмертними". Ми поговоримо про Persistence (живучість даних) та Reliability (надійність доставки).


2. 🧠 Теоретична база (Що там під капотом?)

Щоб зрозуміти, як захистити дані, давайте згадаємо, як працює класичний Message Broker (наприклад, RabbitMQ або схожі системи). У нас є: 1. Producer (той, хто відправляє). 2. Queue (черга, де повідомлення чекають). 3. Consumer (той, хто розгрібає).

Концепція 1: Persistence (Запис на диск)

За замовчуванням більшість черг тримають повідомлення в RAM, бо це швидко. Щоб повідомлення пережило перезавантаження брокера, нам потрібно зробити дві речі: * Сказати черзі: "Ти — Durable (тривала/міцна), не зникай після рестарту". * Сказати повідомленню: "Ти — Persistent, запишись на жорсткий диск, будь ласка".

Аналогія з офіціантом: * Ненадійно: Офіціант запам’ятав ваше замовлення (RAM). Якщо він по дорозі на кухню перечепиться і вдариться головою — він забуде, що ви хотіли піцу. * Надійно (Persistence): Офіціант записав замовлення у блокнот (Disk). Навіть якщо він втратить свідомість, блокнот залишиться, і інший офіціант зможе прочитати замовлення.

Концепція 2: Acknowledgements (Підтвердження, або ACKs)

Уявіть, що повідомлення на диску збереглося. Брокер віддає його Консюмеру. Консюмер бере задачу... і падає з помилкою до того, як встиг її виконати. Якщо Брокер думає: "Ну, я віддав, далі не мої проблеми" — повідомлення втрачено назавжди.

Тут вступає ACK (Acknowledgement). Це як рекомендований лист. Поштар не просто кидає його в скриньку. Він вимагає підпис. Поки Консюмер не скаже: "Я все зробив, видаляй" (ACK), Брокер тримає копію у себе.


3. 🧪 Приклади (Код і реальність)

Давайте подивимося на це в псевдокоді (схожому на Python/RabbitMQ), щоб зрозуміти логіку.

Приклад 1: "Швидко, але небезпечно" (Transient)

Це дефолтна поведінка.

# Оголошуємо звичайну чергу
channel.queue_declare(queue='hello_world')

# Відправляємо повідомлення без гарантій
channel.basic_publish(exchange='',
                      routing_key='hello_world',
                      body='Купити молоко')
print(" [x] Відправлено 'Купити молоко'")

Що тут не так? Якщо процес RabbitMQ впаде, черга hello_world зникне разом із молоком. Ви залишитесь голодним.


Приклад 2: "Броньований потяг" (Persistent + Durable)

Тепер робимо по-дорослому.

# 1. Оголошуємо чергу як DURABLE (вона переживе рестарт)
channel.queue_declare(queue='task_queue', durable=True)

# 2. Відправляємо повідомлення як PERSISTENT
channel.basic_publish(exchange='',
                      routing_key='task_queue',
                      body='Переказати $1,000,000',
                      properties=pika.BasicProperties(
                         delivery_mode = 2, # <-- Магічне число! 2 означає "зберегти на диск"
                      ))

Очікування студента: Тепер все ідеально? Реальність: Майже. Ми захистилися від падіння Брокера. Але ми ще не захистилися від падіння Консюмера (воркера).


Приклад 3: "Повна надійність" (Manual ACKs)

На стороні отримувача (Consumer):

def callback(ch, method, properties, body):
    print(f" [x] Отримав задачу: {body}")

    # Імітуємо важку роботу
    time.sleep(5) 

    print(" [x] Задача виконана!")

    # ОСЬ ВОНО! Ручне підтвердження
    # Ми відправляємо ACK тільки КОЛИ робота завершена
    ch.basic_ack(delivery_tag=method.delivery_tag)

# Вимикаємо авто-підтвердження (auto_ack=False)
channel.basic_consume(queue='task_queue', on_message_callback=callback, auto_ack=False)

Чому це працює: Якщо воркер "помре" під час time.sleep(5), він не надішле basic_ack. Брокер побачить, що з’єднання розірвано, а ACK немає. Він зрозуміє: "Ага, цей хлопець не впорався", і поверне повідомлення в чергу, щоб його підхопив інший, живий воркер.


4. 🛠 Практична частина

Теорія без практики — це просто галюцинація. Давайте це відчуємо.

Завдання 1: Краш-тест (Memory Loss)

  1. Створіть просту чергу (non-durable).
  2. Надішліть туди 10 повідомлень. Не запускайте консюмера.
  3. Перезавантажте сервіс брокера (наприклад, рестарт Docker-контейнера RabbitMQ).
  4. Перевірте чергу. Пусто? Висновок: RAM ненадійна.

Завдання 2: Безсмертна черга

  1. Змініть код: додайте durable=True для черги і delivery_mode=2 для повідомлень.
  2. Повторіть трюк з рестартом.
  3. Повідомлення на місці? Вітаю, ви зберегли дані!

Завдання 3: "Вбивця процесів"

  1. Напишіть консюмер, який обробляє повідомлення 10 секунд.
  2. Вимкніть auto_ack (ручне підтвердження).
  3. Надішліть повідомлення.
  4. Поки консюмер "думає" (перші 5 секунд) — вбийте скрипт консюмера (Ctrl+C або kill).
  5. Запустіть консюмера знову.
  6. Питання: Чи прийшло повідомлення повторно? (Має прийти!).

Завдання 4: Міні-кейс "Логи vs Гроші"

У вас є два типи повідомлень: 1. Логи ("Користувач зайшов на сайт"). 2. Платежі ("Користувач оплатив підписку").

Завдання: Напишіть (на папері або в коді) архітектуру. * Для якого типу ви увімкнете Persistence? * Для якого вимкнете? * Поясніть, чому не вмикати Persistence для всього підряд? (Підказка: швидкість).


5. 💡 Мислення як у розробника

Ось де новачки роблять помилки, а сеньйори — ні.

1. Пастка "Auto-Ack" Багато бібліотек мають налаштування auto_ack=True за замовчуванням. Це означає: "Брокер віддав повідомлення -> Брокер одразу видалив". * Новачок: Залишає як є, бо код коротший. * Профі: Завжди вимикає це для важливих задач. Він знає: мережа глючить, бази даних падають, код має баги.

2. Ціна надійності (Trade-offs) Запис на диск — це найповільніша операція в комп’ютері (навіть з SSD). * Думка профі: "Мені справді треба зберігати кожен лайк в Instagram так само надійно, як банківську транзакцію?" * Відповідь: Ні. Якщо втратимо пару лайків при аварії — ніхто не помре. Якщо втратимо гроші — біда. Тому ми балансуємо між швидкістю (in-memory) і надійністю (persistence).

3. Ідемпотентність (Страшне слово, проста суть) Якщо ми використовуємо retry (повторну доставку), є ризик, що повідомлення обробиться двічі. * Приклад: Списати кошти -> Світло блимнуло -> ACK не дійшов -> Повторна доставка -> Списати кошти ще раз. * Рішення: Ваш код має перевіряти: "Я вже обробляв ID транзакції 123?". Якщо так — ігноруємо, але відсилаємо ACK.


6. 🧩 Підсумок

Отже, що ми сьогодні поклали собі в "Persistence-пам'ять" мозку?

  1. Пам’ять (RAM) — це лава, диск — це острів. Хочеш вижити — стрибай на острів (durable + persistent).
  2. Довіряй, але перевіряй. Не вважай роботу зробленою, поки не отримав підтвердження (ACK).
  3. Не все золото, що блищить. Надійність коштує продуктивності. Використовуй її там, де дані критичні.

Тепер ви вмієте будувати системи, які не бояться висмикнутого з розетки шнура живлення. Це рівень надійності Enterprise.

🔜 У наступній серії: А що, якщо згорить сам жорсткий диск? Ми поговоримо про Реплікацію та Кластеризацію. Бо один сервер — це добре, а три — це вже розподілена система!

Кодуйте з розумом! 💻