Ось готовий урок, створений спеціально для тебе у стилі CS50. Вмикай уяву, ми починаємо!
🎓 CS50-Style: RabbitMQ у FastAPI-проєктах
Привіт, друзі! Мене звати [Твоє Ім'я], і сьогодні ми поговоримо про те, як не змушувати ваших користувачів чекати.
1. 🔥 Вступ: Чому все так повільно?
Уявіть, що ви прийшли у популярну кав'ярню вранці. Ви замовляєте лате. Касир приймає замовлення, а потім... сам йде молоти каву, збивати молоко, малювати сердечко на пінці, мити чашку, і тільки потім повертається до каси, щоб прийняти замовлення наступної людини в черзі.
Риторичне питання: Що станеться з чергою? Вона розтягнеться до вулиці! Люди будуть лютувати. Бізнес втратить гроші.
А тепер подивіться на ваш FastAPI-додаток. Користувач натискає кнопку «Зареєструватися». Ваш сервер починає: 1. Зберігати дані в БД (швидко). 2. Генерувати PDF-звіт (довго). 3. Відправляти вітальний email через зовнішній SMTP (дуже довго).
Поки все це відбувається, браузер користувача «крутиться». Він чекає. Якщо це займе 10 секунд — користувач піде.
У чому проблема? Ви робите все це синхронно (або навіть в одному event loop, блокуючи його логікою CPU). Рішення: Нам потрібен «бармен» (Worker), який робить каву, поки «касир» (FastAPI) просто приймає замовлення і видає чеки.
Цей механізм передачі замовлення від касира до бармена — це і є Брокер Повідомлень. І сьогодні наш герой — RabbitMQ.
2. 🧠 Теоретична база: Як це працює «під капотом»
Давайте розберемо це без складних слів. RabbitMQ — це, по суті, поштове відділення.
У нас є 4 головні дійові особи:
- Producer (Продюсер): Це ваш FastAPI. Його робота — лише створити повідомлення («Треба відправити лист користувачу X») і кинути його в скриньку. Він не чекає, поки лист дійде!
- Exchange (Обмінник): Це сортувальний центр. Він вирішує, у яку саме чергу покласти повідомлення (про це пізніше, поки уявіть, що він просто передає далі).
- Queue (Черга): Це власне поштова скринька. Буфер, де лежать повідомлення і чекають.
- Consumer (Консюмер / Воркер): Це окремий скрипт (працівник), який постійно моніторить чергу. Щойно там щось з'являється — він хапає це і починає працювати (відправляє email, обробляє відео тощо).
🔑 Що треба запам'ятати залізно:
- Decoupling (Розчеплення): FastAPI не знає, хто і коли обробить задачу. Йому байдуже. Його діло — швидко відповісти клієнту «202 Accepted» (Задачу прийнято).
- ACK (Підтвердження): Коли Воркер забрав задачу, він має сказати RabbitMQ: «Я все зробив, можеш видаляти». Якщо Воркер впаде під час роботи і не дасть ACK — RabbitMQ поверне задачу в чергу для іншого Воркера. Надійність!
3. 🧪 Приклади: Від теорії до коду
Для роботи нам знадобиться бібліотека aio-pika (вона асинхронна, ідеальна для FastAPI).
🔹 Крок 1: Встановлення
pip install fastapi uvicorn aio-pika
# Також переконайтеся, що у вас запущений RabbitMQ (наприклад, через Docker):
# docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management
🔹 Крок 2: Продюсер (FastAPI)
Ми не будемо відправляти email, ми просто скажемо RabbitMQ: "Зроби це".
main.py
from fastapi import FastAPI
import aio_pika
import json
app = FastAPI()
# Підключення до RabbitMQ (спрощено)
async def get_rabbitmq_channel():
connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
return await connection.channel()
@app.post("/send-email/")
async def send_welcome_email(email: str):
channel = await get_rabbitmq_channel()
# Тіло повідомлення
message_body = json.dumps({"type": "welcome_email", "email": email})
# Відправляємо в чергу "email_queue"
await channel.default_exchange.publish(
aio_pika.Message(body=message_body.encode()),
routing_key="email_queue"
)
return {"message": "Задачу на відправку пошти створено! Ми це зробимо фоново."}
Що тут відбулося? Ви натиснули "Send". Сервер відповів миттєво. Але пошта ще не пішла. Лист лежить у RabbitMQ.
🔹 Крок 3: Консюмер (Воркер)
Це окремий Python-файл. Він буде запущений в іншому терміналі. Він імітує важку роботу.
worker.py
import asyncio
import aio_pika
import json
import time
async def process_message(message: aio_pika.IncomingMessage):
async with message.process(): # Це автоматично відправить ACK, якщо код виконається без помилок
data = json.loads(message.body.decode())
print(f" [x] Отримано задачу: {data}")
# Імітуємо важку роботу (відправка пошти)
print(f" [⏳] Відправляю лист на {data['email']}...")
await asyncio.sleep(5) # Спимо 5 секунд
print(f" [✅] Лист успішно відправлено!")
async def main():
connection = await aio_pika.connect_robust("amqp://guest:guest@localhost/")
channel = await connection.channel()
# Оголошуємо ту ж саму чергу
queue = await channel.declare_queue("email_queue")
print(" [*] Чекаю на повідомлення. Натисніть CTRL+C для виходу")
# Слухаємо чергу
await queue.consume(process_message)
# Тримаємо скрипт запущеним
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
Очікування студента: Коли ми запустимо worker.py, він нічого не робитиме.
Реальність: Як тільки ми зробимо запит до FastAPI, воркер "прокинеться", напише "Отримано задачу", почекає 5 секунд і скаже "Готово".
4. 🛠 Практична частина
Час забруднити руки! Відкрийте два термінали. В одному — uvicorn main:app --reload, в іншому — python worker.py.
- Завдання 1 (Базове): Зробіть POST-запит на
/send-email/з вашою поштою. Переконайтеся, що FastAPI відповів миттєво, а Worker почав "думати". - Завдання 2 (Навантаження): Швидко відправте 5 запитів підряд. Подивіться на лог Воркера. Він обробляє їх по черзі (один за одним). Черга працює!
- Завдання 3 (Масштабування): Запустіть ще один термінал з
python worker.py(тепер у вас два воркера). Знову відправте 5-10 запитів.- Питання: Як розподілилися задачі? (RabbitMQ використовує Round-Robin — по колу: тобі, тобі, тобі...).
- Завдання 4 (Помилка): У файлі
worker.pyпередawait asyncio.sleepдодайтеraise Exception("Пошта впала!"). Запустіть.- Що сталося? Повідомлення не зникло назавжди? Чи спробував RabbitMQ віддати його знову? (Якщо
message.process()не завершився успішно, повідомлення повертається в чергу).
- Що сталося? Повідомлення не зникло назавжди? Чи спробував RabbitMQ віддати його знову? (Якщо
- Міні-кейс: Створіть новий ендпоінт
/resize-image/, який приймає назву файлу. Воркер має писати: "Змінюю розмір картинки..." і чекати 2 секунди.
5. 💡 Мислення як у розробника
Ви тепер вмієте перекидати JSON-и. Але як думає Senior Developer?
⚠️ Типова помилка новачка: "Передача слона"
Ніколи не передавайте сам файл (картинку, PDF) через RabbitMQ. Повідомлення мають бути маленькими (до кількох кілобайт).
* Погано: { "image_data": "<base64_string_5mb>" }
* Добре: { "image_path": "/s3/bucket/avatar_123.jpg", "user_id": 55 }
Воркер отримає шлях, сам скачає файл зі сховища, обробить і збереже назад.
⚠️ Типова помилка №2: Забутий Connection
У прикладі вище ми створюємо з'єднання при кожному запиті. Це дорого!
* Як профі: У FastAPI використовують події startup та shutdown, щоб відкрити з'єднання один раз при запуску сервера і використовувати його скрізь (або Dependency Injection).
🧩 Ідемпотентність
Складне слово, проста суть: "Що буде, якщо одну задачу виконати двічі?" Іноді мережа глючить, і RabbitMQ може віддати задачу вдруге. Ваш воркер має бути готовим до цього. Наприклад, перевіряти в БД: "А чи не відправив я вже цей лист 5 хвилин тому?".
6. 🧩 Підсумок
Що ми сьогодні зробили? Ми розірвали жорсткий зв'язок між прийомом замовлення (HTTP Request) і його виконанням. Тепер ваш FastAPI може обробляти тисячі запитів на секунду, навіть якщо відправка пошти займає хвилину. Ви просто додаєте більше воркерів у фоні, не чіпаючи основний сайт.
Ви вмієте:
1. Запускати RabbitMQ.
2. Відправляти повідомлення з FastAPI (aio-pika).
3. Писати фонові воркери, які розгрібають ці завали.
Тизер наступного уроку: А що, якщо нам треба, щоб воркер повернув результат назад у FastAPI (наприклад, згенерований текст від ChatGPT)? RabbitMQ — це дорога в один кінець? Ні! На наступному уроці розберемо RPC (Remote Procedure Call) патерн.
А поки що — це був CS50. (Стук коду по клавішах). Успіхів!