Ось готовий урок, створений за твоїм майстер-промптом.
🎓 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 це виглядає як тенісний матч 🎾.
Головні герої:
- Клієнт (Client): Той, хто хоче отримати результат.
- Сервер (Server/Worker): Той, хто робить важку роботу.
- RabbitMQ: Посередник (пошта).
Як проходить процес (Step-by-step):
- Запит: Клієнт відправляє повідомлення в чергу завдань (назвемо її
rpc_queue). - Властивості повідомлення (Це важливо!):
Разом із даними клієнт додає два критично важливі мета-дані:
- 📌
reply_to: Ім'я спеціальної черги, куди сервер має покласти відповідь. (Це як зворотна адреса на конверті). - 🆔
correlation_id: Унікальний ідентифікатор запиту. (Той самий "номерок" замовлення в піцерії).
- 📌
- Обробка: Сервер забирає задачу, рахує, пітніє.
- Відповідь: Сервер бере результат і відправляє його в чергу, вказану в
reply_to. І обов'язково прикріплює той самийcorrelation_id. - Фініш: Клієнт слухає чергу
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
Завдання:
- 🔹 Запуск: Створіть два файли (
server.py,client.py) з кодом вище. Запустіть спочатку сервер, потім клієнт. Чи отримали ви число 832040? - 🔹 Масштабування: Запустіть два екземпляри
server.pyв різних терміналах. Тепер запустіть клієнт кілька разів поспіль.- Що ви помітили? RabbitMQ автоматично розподіляє навантаження між серверами (Round Robin). Один рахує, другий відпочиває, і навпаки.
- 🔹 Експеримент: У коді клієнта змініть
correlation_idна статичний рядок (наприклад, "123"). Запустіть клієнт двічі швидко. Чи зламається логіка, якщо прийде дві відповіді з однаковим ID? (Спойлер: так, ви можете переплутати відповіді). - 🔹 Міні-кейс: Уявіть, що це сервіс перевірки промокодів.
- Змініть сервер так, щоб він приймав рядок (наприклад, "SALE2024").
- Якщо код правильний — повертав "10% знижки", якщо ні — "Помилка".
- Клієнт має виводити повідомлення про знижку.
- 🔹 Питання «А що, якщо...»: Що станеться, якщо клієнт впаде (закриється) після відправки запиту, але до отримання відповіді? Куди дінеться повідомлення від сервера? (Підказка: воно залишиться в
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-нути повідомлення! 😉