Модуль 19

RPC поверх RabbitMQ

Ось готовий урок, створений за твоїм майстер-промптом.


🎓 CS50-style Lesson: RPC поверх RabbitMQ

Привіт, друзі! 👋 Радий бачити вас на занятті.

Сьогодні ми розберемо тему, яка на перший погляд звучить як складне заклинання з Гаррі Поттера — RPC поверх RabbitMQ. Але обіцяю: до кінця цього уроку ви не просто зрозумієте, як це працює, а й відчуєте себе справжніми архітекторами розподілених систем.

Готові? Поїхали! 🚀


1. 🔥 Вступ: Коли HTTP вже не рятує?

Уявіть, що ви будуєте крутий стартап. Скажімо, сервіс, який перетворює фотографії вашого кота на картини в стилі Ван Гога за допомогою нейромереж. 🎨🐈

Ви робите простий веб-сайт. Користувач завантажує фото, натискає «Перетворити», і ваш сервер починає думати... і думати... і думати. Обробка займає 30 секунд. У світі вебу 30 секунд — це вічність. Браузер скаже "Timeout", користувач подумає, що все зламалося, і піде до конкурентів.

Питання до вас: Як зробити так, щоб користувач отримав результат, але ми не тримали з’єднання відкритим пів хвилини, блокуючи весь сервер?

Тут на сцену виходять черги повідомлень (RabbitMQ). Ми вже знаємо, як відправити задачу в чергу («зроби це колись»). Але стривайте... Як нам отримати відповідь назад?

Ми вміємо казати «Привіт, сервер, зроби роботу». Але як сервер скаже: «Ей, клієнте, я все зробив, ось твій результат!»?

Ось тут нам і потрібен RPC (Remote Procedure Call).

Аналогія з життя: 🍕 Звичайний HTTP-запит — це коли ви стоїте біля каси в фастфуді й чекаєте, поки вам зберуть бургер. Ви не відходите, ви блокуєте чергу.

RPC через RabbitMQ — це коли ви робите замовлення, отримуєте номерок (чек) і йдете сісти за стіл. Коли замовлення готове, кухар вигукує ваш номер, і ви (саме ви, а не хтось інший) забираєте їжу.

Сьогодні ми навчимося видавати ці «номерки» і будувати двосторонній зв’язок через черги.


2. 🧠 Теоретична база: Як це працює «під капотом»

Давайте розберемо механіку. RPC розшифровується як Remote Procedure Call (Віддалений виклик процедури). Ідея проста: ви викликаєте функцію на одному комп'ютері, а виконується вона на іншому.

У випадку з RabbitMQ це виглядає як тенісний матч 🎾.

Головні герої:

  1. Клієнт (Client): Той, хто хоче отримати результат.
  2. Сервер (Server/Worker): Той, хто робить важку роботу.
  3. RabbitMQ: Посередник (пошта).

Як проходить процес (Step-by-step):

  1. Запит: Клієнт відправляє повідомлення в чергу завдань (назвемо її rpc_queue).
  2. Властивості повідомлення (Це важливо!): Разом із даними клієнт додає два критично важливі мета-дані:
    • 📌 reply_to: Ім'я спеціальної черги, куди сервер має покласти відповідь. (Це як зворотна адреса на конверті).
    • 🆔 correlation_id: Унікальний ідентифікатор запиту. (Той самий "номерок" замовлення в піцерії).
  3. Обробка: Сервер забирає задачу, рахує, пітніє.
  4. Відповідь: Сервер бере результат і відправляє його в чергу, вказану в reply_to. І обов'язково прикріплює той самий correlation_id.
  5. Фініш: Клієнт слухає чергу reply_to. Він бачить нове повідомлення, перевіряє correlation_id. Якщо ID збігається з тим, що він відправляв — ура! Це наша відповідь.

💡 Що треба запам’ятати залізно:

  • Ми використовуємо дві черги: одну для запитів, іншу — для відповідей.
  • Без correlation_id клієнт не зрозуміє, на який саме запит прийшла відповідь (особливо якщо запитів тисячі).

3. 🧪 Приклади: Від теорії до коду

Ми використаємо Python і бібліотеку pika, але логіка однакова для будь-якої мови (Java, Go, JS).

Сценарій: Калькулятор Фібоначчі

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

Частина 1: Сервер (Той, хто працює)

Що очікуєте побачити? Код, який слухає чергу і щось повертає.

# rpc_server.py
import pika

# Підключення до RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()

# Створюємо чергу для вхідних задач
channel.queue_declare(queue='rpc_queue')

def fib(n):
    # Просто рекурсивна функція (імітація важкої роботи)
    if n == 0: return 0
    elif n == 1: return 1
    else: return fib(n-1) + fib(n-2)

def on_request(ch, method, props, body):
    n = int(body)
    print(f" [.] Отримав задачу: fib({n})")

    response = fib(n)

    # ВІДПОВІДАЄМО!
    # Публікуємо результат у чергу, яку вказав клієнт (props.reply_to)
    # І додаємо той самий ID (props.correlation_id)
    ch.basic_publish(exchange='',
                     routing_key=props.reply_to,
                     properties=pika.BasicProperties(correlation_id = props.correlation_id),
                     body=str(response))

    # Підтверджуємо виконання
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_qos(prefetch_count=1) # Не набирати більше 1 задачі за раз
channel.basic_consume(queue='rpc_queue', on_message_callback=on_request)

print(" [x] Чекаю на RPC запити...")
channel.start_consuming()

Чому так? Сервер "сліпий". Він не знає, хто його питає. Він просто дивиться на reply_to і шле туди відповідь. Це і є магія розчеплення (decoupling).

Частина 2: Клієнт (Той, хто питає)

Тут трохи хитріше. Клієнт має створити тимчасову чергу для відповіді, відправити запит і чекати.

# rpc_client.py
import pika
import uuid

class FibonacciRpcClient:
    def __init__(self):
        self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
        self.channel = self.connection.channel()

        # Створюємо унікальну тимчасову чергу для відповідей тільки цьому клієнту
        result = self.channel.queue_declare(queue='', exclusive=True)
        self.callback_queue = result.method.queue

        # Підписуємось на цю чергу
        self.channel.basic_consume(
            queue=self.callback_queue,
            on_message_callback=self.on_response,
            auto_ack=True)

    def on_response(self, ch, method, props, body):
        # Якщо ID збігається з тим, що ми чекаємо — зберігаємо результат
        if self.corr_id == props.correlation_id:
            self.response = body

    def call(self, n):
        self.response = None
        self.corr_id = str(uuid.uuid4()) # Генеруємо унікальний ID

        print(f" [x] Запитуємо fib({n})")
        self.channel.basic_publish(
            exchange='',
            routing_key='rpc_queue', # Шлемо в загальну чергу задач
            properties=pika.BasicProperties(
                reply_to=self.callback_queue, # "Відповідай сюди"
                correlation_id=self.corr_id,  # "Ось номерок замовлення"
            ),
            body=str(n))

        # Чекаємо, поки не прийде потрібна відповідь
        while self.response is None:
            self.connection.process_data_events()

        return int(self.response)

fib_rpc = FibonacciRpcClient()
response = fib_rpc.call(30)
print(f" [.] Отримав результат: {response}")

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

Тепер ваша черга! Відкрийте термінал. Якщо у вас встановлено Docker, запустіть RabbitMQ однією командою: docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management

Завдання:

  1. 🔹 Запуск: Створіть два файли (server.py, client.py) з кодом вище. Запустіть спочатку сервер, потім клієнт. Чи отримали ви число 832040?
  2. 🔹 Масштабування: Запустіть два екземпляри server.py в різних терміналах. Тепер запустіть клієнт кілька разів поспіль.
    • Що ви помітили? RabbitMQ автоматично розподіляє навантаження між серверами (Round Robin). Один рахує, другий відпочиває, і навпаки.
  3. 🔹 Експеримент: У коді клієнта змініть correlation_id на статичний рядок (наприклад, "123"). Запустіть клієнт двічі швидко. Чи зламається логіка, якщо прийде дві відповіді з однаковим ID? (Спойлер: так, ви можете переплутати відповіді).
  4. 🔹 Міні-кейс: Уявіть, що це сервіс перевірки промокодів.
    • Змініть сервер так, щоб він приймав рядок (наприклад, "SALE2024").
    • Якщо код правильний — повертав "10% знижки", якщо ні — "Помилка".
    • Клієнт має виводити повідомлення про знижку.
  5. 🔹 Питання «А що, якщо...»: Що станеться, якщо клієнт впаде (закриється) після відправки запиту, але до отримання відповіді? Куди дінеться повідомлення від сервера? (Підказка: воно залишиться в callback_queue або зникне, якщо черга exclusive).

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

Ви тепер знаєте синтаксис, але як думає Senior Developer?

  • Типова помилка новачка: Створення нової черги відповідей (callback_queue) для кожного окремого запиту.
    • Чому це погано? Створення черги — це повільна операція.
    • Як правильно: Створіть одну чергу відповідей при старті клієнта і використовуйте її для всіх майбутніх запитів, просто міняючи correlation_id.
  • Таймаути: У реальному світі сервер може зависнути. Ваш клієнтський цикл while self.response is None може крутитися вічно.
    • Pro Tip: Завжди додавайте логіку таймауту. Якщо відповіді немає 5 секунд — кидайте помилку і пробуйте ще раз.
  • Навіщо це взагалі? Чому не REST API?
    • RPC через черги дозволяє буферизувати запити. Якщо у вас раптом прийшло 10,000 запитів, а серверів всього 2, HTTP-сервер впаде. RabbitMQ просто складе їх у чергу, і сервери спокійно розгрібатимуть завали. Система не впаде, вона просто стане повільнішою на деякий час.

6. 🧩 Підсумок

Отже, що ми сьогодні зробили? Ми розірвали прямий синхронний зв'язок між Клієнтом і Сервером. Тепер вони спілкуються через посередника, обмінюючись "номерками" (correlation_id).

Що ви тепер вмієте: 1. Організовувати двосторонній зв'язок у асинхронних системах. 2. Розумієте паттерн Request/Reply у брокерах повідомлень. 3. Знаєте, як масштабувати важкі обчислення, просто додаючи нові скрипти-воркери.

🔍 Тизер наступного уроку: Сьогодні ми надсилали повідомлення конкретному воркеру. А що, якщо ми хочемо, щоб повідомлення отримали всі сервіси одночасно? Наприклад, коли реєструється новий користувач, ми хочемо і відправити Email, і створити запис в БД, і нарахувати бонуси. На наступному занятті ми розберемо Exchanges (Обмінники) та патерн Publish/Subscribe. Це буде 🔥!

А поки — кодіть, експериментуйте і не забувайте ack-нути повідомлення! 😉