Модуль 36

Оптимізація продуктивності FastAPI

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


🚀 Тема уроку: Оптимізація продуктивності FastAPI

Привіт, друзі! Це CS50 (умовно 😉), і сьогодні ми говоримо про швидкість.

1. 🔥 Вступ: Коли "Fast" у назві FastAPI перестає працювати

Уявіть ситуацію. Ви створили стартап — новий, крутий сервіс доставки піци. Ви обрали FastAPI, бо, ну знаєте, він же називається FastAPI. У документації написано, що він найшвидший.

Ви запускаєте сервіс. Приходить 10 клієнтів — все літає. Приходить 100 — все окей. Але раптом маркетинг спрацював на відмінно, і до вас стукає 10 000 користувачів одночасно. І тут... ваш сервер "лягає". Запити висять по 10 секунд. Клієнти йдуть до конкурентів.

Питання: Чому так сталося? Хіба фреймворк не обіцяв нам магічну швидкість?

Відповідь: FastAPI — це як Ferrari. У нього потужний двигун. Але якщо ви, як водій, тиснете на гальма (пишете блокуючий код) або заливаєте погане паливо (неефективна робота з базою даних), навіть Ferrari не поїде швидко.

Сьогодні ми розберемося, як не бути тим водієм, що гальмує суперкар.


2. 🧠 Теоретична база: Що відбувається під капотом?

Щоб оптимізувати, треба розуміти, як воно працює.

Головний герой: Event Loop (Цикл подій)

Уявіть собі одного офіціанта в дуже популярному ресторані. Це ваш FastAPI процес. * Синхронний код (Sync): Офіціант приймає замовлення, йде на кухню і чекає, поки кухар приготує страву. Він просто стоїть і дивиться на пательню. В цей час інші клієнти чекають. Це погано. * Асинхронний код (Async): Офіціант приймає замовлення, передає його на кухню і одразу йде обслуговувати наступного клієнта. Коли страва готова, дзвоник дзеленчить, і він повертається забрати її.

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

FastAPI працює на одному потоці (здебільшого). Якщо ви зробите щось "важке" (наприклад, складні математичні обчислення або синхронний запит до стороннього API) прямо в цьому потоці — ви заблокуєте "офіціанта". Весь сервер зупиниться для всіх користувачів.

🧩 Інтуїтивне розуміння:

  • I/O Bound (Введення/Виведення): Робота з базою, мережею, файлами. Тут ідеально працює async/await. Ми "чекаємо", поки хтось інший зробить роботу.
  • CPU Bound (Процесор): Обробка зображень, складні формули, ML-моделі. async тут не допоможе, бо процесор реально зайнятий. Тут потрібні інші трюки (про них згодом).

3. 🧪 Приклади: Від гальм до швидкості

Приклад 1: Класична помилка (Вбивця продуктивності)

Багато хто пише так, думаючи, що async def — це магічне заклинання.

import time
from fastapi import FastAPI

app = FastAPI()

@app.get("/slow")
async def slow_endpoint():
    # Симулюємо довгу роботу (наприклад, важкий запит)
    time.sleep(5)  
    return {"message": "Я прокинувся!"}

Питання до вас: Якщо я відправлю 3 запити одночасно на цей ендпоінт, скільки часу сумарно чекатиме третій користувач? 5 секунд чи 15?

... ...

Відповідь: Він чекатиме 15 секунд (плюс-мінус). Чому? time.sleep(5) блокує весь Event Loop. Поки сервер "спить" для першого клієнта, він не може навіть прийняти з'єднання від другого.


Приклад 2: Виправляємо ситуацію (Asyncio)

import asyncio
from fastapi import FastAPI

app = FastAPI()

@app.get("/fast")
async def fast_endpoint():
    # Віддаємо управління назад в Event Loop
    await asyncio.sleep(5)  
    return {"message": "Я прокинувся, але не блокував інших!"}

Тепер, якщо ми кинемо 100 запитів одночасно, вони всі завершаться приблизно за 5.1 секунди. Офіціант (FastAPI) прийняв замовлення і пішов далі, поки таймер тікав.


Приклад 3: Турбо-режим з ORJSON

Стандартний JSON-серіалізатор у Python повільний. Якщо ваш API повертає великі списки даних (наприклад, 10 000 товарів), серіалізація може займати більше часу, ніж запит до БД.

Використовуємо ORJSONResponse (він написаний на Rust і дуже швидкий).

from fastapi.responses import ORJSONResponse

@app.get("/items", response_class=ORJSONResponse)
async def read_items():
    # Уявіть тут величезний список словників
    large_data = [{"id": i, "name": f"Item {i}"} for i in range(10000)]
    return large_data

Результат: Просто змінивши клас відповіді, ми можемо прискорити віддачу даних у 5-10 разів.


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

Час закотити рукави. Виконайте ці завдання, щоб відчути різницю.

  1. 🔹 Повтори та відчуй: Запусти код із Прикладу 1 ("поганий"). Відкрий дві вкладки браузера і спробуй завантажити сторінку одночасно. Переконайся, що друга вкладка "висить", поки перша не закінчить.
  2. 🔹 Магічне виправлення: Заміни код на Приклад 2. Зроби те саме. Відчуваєш різницю?
  3. 🔹 Реальна задача (БД): Уяви, що в тебе є функція get_users_from_db(), яка не є асинхронною (використовує стару бібліотеку).
    • Завдання: Як змусити її працювати у FastAPI, не блокуючи цикл?
    • Підказка: Визнач ендпоінт просто як def (без async), або використай run_in_executor.
  4. 🔹 Міні-кейс: Тобі треба зробити ендпоінт, який приймає зображення, зменшує його розмір (це CPU-навантаження) і повертає назад.
    • Питання: Чи допоможе тут await asyncio.sleep()? Чому ні?
    • Рішення: Напиши псевдокод, як би ти це реалізував (варто подивитися в бік BackgroundTasks або окремого воркера типу Celery).
  5. 🔹 А що, якщо... Що буде, якщо ми запустимо FastAPI не в один процес, а в 4 (через gunicorn -w 4 ...)? Як це вплине на Приклад 1? (Спойлер: це частково вирішить проблему, але тільки для 4 користувачів).

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

Як відрізнити новачка від сеньйора в оптимізації?

❌ Помилка новачка:

Він намагається оптимізувати все підряд. Переписує цикли, змінює назви змінних. Це "економія на сірниках". Або він пхає async усюди, навіть там, де бібліотека це не підтримує (наприклад, стандартний модуль requests у Python є синхронним — ніколи не використовуйте його всередині async def у FastAPI! Беріть httpx).

✅ Думки профі:

Досвідчений розробник спочатку вимірює. Він ставить питання: 1. Де пляшкове горлечко? Це База Даних? Це серіалізація JSON? Чи це мій Python-код? 2. I/O чи CPU? Якщо це очікування (I/O) — робимо асинхронним. Якщо це обчислення (CPU) — виносимо в окремий процес або фонову задачу. 3. Кешування. Навіщо генерувати відповідь, якщо вона не змінилася? (Redis — ваш найкращий друг).

Порада: Використовуйте профайлери (наприклад, PyInstrument) або middleware для заміру часу виконання запиту. Не гадайте на кавовій гущі.


6. 🧩 Підсумок

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

  1. FastAPI швидкий, тільки якщо ми не блокуємо Event Loop.
  2. time.sleep() — ворог, asyncio.sleep() — друг.
  3. Синхронні операції (старі драйвери БД, важкі обчислення) вбивають продуктивність асинхронного сервера.
  4. Використання ORJSONResponse — це "безкоштовне" прискорення для великих JSON-ів.

Тепер ви вмієте не просто писати код, який працює, а код, який літає і витримує навантаження.

👉 Наступного разу: Ми поговоримо про те, де зберігати дані, щоб це було так само швидко. Готуйтеся зануритися у світ Redis та Кешування.

А поки що — щасти вам у коді!