FastAPI: архітектура, async і production-патерни¶
Довідник до уроків 38–51
Розділи 1–5 — до уроку 38 (там вони працюють на проєкті news_hub і з реальними вимірами), 6–8 — до уроку 39 (база даних), 9–10 — до уроків 49–51 (Docker, деплой).
Складність: intermediate → advanced
Зміст¶
- Що таке FastAPI?
- ASGI vs WSGI
- Три типи ендпоінтів та їх поведінка
- Blocking Code: Каскадний Ефект
- Dependency Injection (DI)
- Async Connection Pool з asyncpg
- Repository Pattern
- Unit of Work Pattern
- Мікросервісна Архітектура
- Production Deployment
- Mermaid Діаграми
1. Що таке FastAPI?¶
FastAPI — сучасний Python вебфреймворк для побудови REST API, порівнянний за швидкістю з Node.js та Go. Побудований на двох стовпах:
| Компонент | Роль |
|---|---|
| Starlette | ASGI toolkit — async HTTP, WebSocket, middleware |
| Pydantic | Валідація типів, серіалізація JSON, автодокументація |
Ключові можливості¶
Автоматична документація — FastAPI генерує Swagger UI та ReDoc з коду:
http://localhost:8000/docs ← Swagger UI (інтерактивний)
http://localhost:8000/redoc ← ReDoc (читабельний)
http://localhost:8000/openapi.json ← OpenAPI schema
Type hints → Валідація — Pydantic перехоплює неправильні дані до входу в бізнес-логіку:
@app.get("/users/{user_id}")
async def get_user(user_id: int, limit: int = 10):
# FastAPI автоматично:
# 1. Парсить user_id з URL як int (422 якщо не число)
# 2. Читає limit з query string (default=10)
# 3. Валідує через Pydantic
...
Dependency Injection — система ін'єкцій залежностей вбудована у фреймворк (детальніше у розділі 5).
2. ASGI vs WSGI¶
WSGI (синхронний): ASGI (асинхронний):
┌──────────────┐ ┌──────────────┐
│ Nginx/Proxy │ │ Nginx/Proxy │
└──────┬───────┘ └──────┬───────┘
│ │
┌──────▼────────────────┐ ┌──────▼────────────────┐
│ Gunicorn │ │ uvicorn │
│ 4 worker processes │ │ 1 async process │
│ (4 × Python interp.) │ │ Event Loop │
└──────┬───────┬────────┘ └──────┬────────────────┘
│ │ │
[Thread] [Thread] [Coroutine × 10000]
per request all in one thread
Чому ASGI виграє для I/O-bound навантажень¶
| WSGI (Gunicorn) | ASGI (uvicorn) | |
|---|---|---|
| Модель | 1 process / request | 1 coroutine / request |
| Пам'ять/з'єднання | ~8 MB (thread) | ~2 KB (coroutine) |
| Max concurrent | ~100–500 | ~10 000+ |
| WebSocket | Складно | Нативно |
| CPU-bound | ОК (процеси) | Потрібен executor |
3. Три типи ендпоінтів та їх поведінка¶
Це критично важливо зрозуміти перед написанням будь-якого FastAPI коду:
from fastapi import FastAPI
import asyncio, time
app = FastAPI()
# ────────────────────────────────────────────────────────────────
# ТИП 1: async def + await → Event Loop (НАЙКРАЩЕ для I/O)
# ────────────────────────────────────────────────────────────────
@app.get("/type1")
async def type1():
await asyncio.sleep(2) # ✅ Event Loop вільний
return {"type": 1}
# ────────────────────────────────────────────────────────────────
# ТИП 2: async def + blocking → КАТАСТРОФА
# ────────────────────────────────────────────────────────────────
@app.get("/type2")
async def type2():
time.sleep(2) # ⛔ Замораживає весь сервер!
return {"type": 2}
# ────────────────────────────────────────────────────────────────
# ТИП 3: def (sync) → Автоматичний ThreadPool (безпечно, обмежено)
# ────────────────────────────────────────────────────────────────
@app.get("/type3")
def type3():
time.sleep(2) # ⚠️ OK — FastAPI детектує def → threadpool
return {"type": 3}
Шкала виконання для 1000 клієнтів¶
ТИП 1 (async + await):
[T=0ms] 1000 coroutines стартують
[T=0.5ms] Всі 1000 await asyncio.sleep() — Event Loop ВІЛЬНИЙ
[T=2000ms] Всі 1000 прокидаються одночасно
Throughput: ~500 req/s ✅
ТИП 2 (async + blocking):
[T=0ms] Запит #1 → time.sleep(2) → Event Loop ЗАМЕРЗ
[T=2000ms] Запит #1 відповів → Запит #2 стартує
[T=4000ms] Запит #2 відповів → Запит #3...
Throughput: 0.5 req/s ❌ (1000× гірше!)
ТИП 3 (sync def → threadpool):
FastAPI: виявлено def → spawn thread
40 threads обслуговують 40 запитів паралельно
960 запитів чекають у черзі
Throughput: ~20 req/s ⚠️
4. Blocking Code: Каскадний Ефект¶
Чому один blocking виклик руйнує весь сервер¶
Уяви ресторан з одним офіціантом (Event Loop). Якщо офіціант йде на кухню і сам готує страву (blocking call) — всі інші клієнти чекають. Ніхто не приймає нових замовлень, ніхто не отримує рахунки.
Клієнт A: ──────[ЗАМОВЛЕННЯ]──────[time.sleep 2s — весь сервер чекає]──[ВІДПОВІДЬ]
Клієнт B: ─────────────────────────────────────────────[Тепер може стартувати]──[відповідь]
Клієнт C: ──────────────────────────────────────────────────────────────────────[стартує]
Таблиця замін¶
| Blocking (ЗАБОРОНЕНО в async def) | Non-blocking (ПРАВИЛЬНО) |
|---|---|
time.sleep(n) |
await asyncio.sleep(n) |
requests.get(url) |
await session.get(url) (aiohttp / httpx async) |
open(path).read() |
await aio_file.read() (aiofiles) |
psycopg2.execute(sql) |
await conn.fetch(sql) (asyncpg) |
subprocess.run(cmd) |
await asyncio.create_subprocess_exec(...) |
| CPU-bound loop | await loop.run_in_executor(process_pool, func) |
5. Dependency Injection (DI)¶
FastAPI має вбудовану DI-систему через Depends(). Це один з найпотужніших інструментів фреймворку.
Базовий приклад¶
from fastapi import FastAPI, Depends, Header, HTTPException
app = FastAPI()
# Залежність: перевірка API ключа
async def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != "secret-key":
raise HTTPException(status_code=401, detail="Invalid API key")
return x_api_key
# Залежність: pagination параметри
def get_pagination(skip: int = 0, limit: int = 100):
return {"skip": skip, "limit": min(limit, 1000)}
# Ендпоінт використовує обидві залежності
@app.get("/items")
async def list_items(
api_key: str = Depends(verify_api_key),
pagination: dict = Depends(get_pagination),
):
return {"items": [], **pagination}
DI Lifecycle: від Request до Response¶
HTTP Request
│
▼
FastAPI Router розбирає маршрут
│
▼
DI System викликає всі Depends() ─────────────────────────────┐
│ │
├── verify_api_key(header) ──────────────── перевірка │
├── get_pagination(query) ──────────────── параметри │
└── get_db_connection() ──────────────── з'єднання з БД│
│ (yield conn) │
▼ │
Ендпоінт-функція отримує готові залежності │
│ │
▼ │
Генерує відповідь │
│ │
DI System завершує залежності (після yield) ◄─────────────┘
│ (закриття з'єднань, cleanup)
▼
HTTP Response
yield-залежності — гарантований cleanup¶
async def get_db_connection(pool):
async with pool.acquire() as conn:
yield conn # ← передаємо в ендпоінт
# Тут автоматично: conn.release() назад у pool
# НАВІТЬ якщо ендпоінт кинув виняток!
6. Async Connection Pool з asyncpg¶
Чому connection pool критично важливий¶
Відкриття нового TCP-з'єднання з PostgreSQL — дорога операція (~50ms, TCP handshake + auth). Якщо 1000 клієнтів відкривають нові з'єднання одночасно — БД відмовляє через max_connections (~100 за замовчуванням).
Connection pool тримає N вже відкритих з'єднань. Клієнти беруть з'єднання з пулу (~0.1ms) і повертають після запиту.
Реалізація з asyncpg¶
import asyncpg
from fastapi import FastAPI, Depends, HTTPException
from contextlib import asynccontextmanager
db_pool: asyncpg.Pool = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global db_pool
# ─── Startup ───────────────────────────────────────────────
db_pool = await asyncpg.create_pool(
dsn="postgresql://user:pass@localhost/mydb",
min_size=5, # Мінімум відкритих з'єднань (warm)
max_size=20, # Максимум одночасних з'єднань
command_timeout=30, # Timeout для SQL запиту
)
yield
# ─── Shutdown ──────────────────────────────────────────────
await db_pool.close() # Закрити всі з'єднання
app = FastAPI(lifespan=lifespan)
# DI: отримати з'єднання з пулу на час запиту
async def get_db():
async with db_pool.acquire() as conn:
yield conn # FastAPI гарантує release після запиту
@app.get("/users/{user_id}")
async def get_user(user_id: int, conn = Depends(get_db)):
row = await conn.fetchrow(
"SELECT id, name, email FROM users WHERE id = $1",
user_id
)
if not row:
raise HTTPException(status_code=404)
return dict(row)
Execution Timeline для двох одночасних запитів¶
[T=0ms] Клієнт A: db_pool.acquire() → з'єднання #1 (миттєво з пулу)
[T=0ms] Клієнт B: db_pool.acquire() → з'єднання #2 (миттєво з пулу)
[T=1ms] A: SQL → байти → мережевий сокет до PostgreSQL
[T=1ms] B: SQL → байти → мережевий сокет до PostgreSQL
[T=2ms] A: await conn.fetch() → SUSPENDED (Event Loop вільний!)
[T=2ms] B: await conn.fetch() → SUSPENDED (Event Loop вільний!)
[T=52ms] PostgreSQL відповів B → Event Loop прокидає B
[T=60ms] PostgreSQL відповів A → Event Loop прокидає A
[T=61ms] A завершено → з'єднання #1 повернуто в пул
[T=61ms] B завершено → з'єднання #2 повернуто в пул
7. Repository Pattern¶
Проблема: "Сирий SQL" в ендпоінтах¶
# ❌ Порушення SRP — ендпоінт знає про SQL, asyncpg, схему БД
@app.get("/users/{user_id}")
async def get_user(user_id: int, conn = Depends(get_db)):
row = await conn.fetchrow(
"SELECT id, name, email FROM users WHERE id = $1", user_id
)
return dict(row) if row else None
Проблеми цього підходу: - Тестування вимагає реальної бази даних - Зміна БД вимагає зміни ендпоінтів - SQL розсипаний по всьому коду
Рішення: Repository абстракція¶
import abc
from typing import Optional
from pydantic import BaseModel
# ─── Доменна модель (бізнес-об'єкт) ──────────────────────────────────────────
class User(BaseModel):
id: int
name: str
email: str
# ─── Абстрактний порт (інтерфейс) ─────────────────────────────────────────────
class AbstractUserRepository(abc.ABC):
@abc.abstractmethod
async def get(self, user_id: int) -> Optional[User]:
...
@abc.abstractmethod
async def add(self, user: User) -> None:
...
# ─── Конкретний адаптер (asyncpg) ─────────────────────────────────────────────
class AsyncpgUserRepository(AbstractUserRepository):
def __init__(self, conn):
self._conn = conn
async def get(self, user_id: int) -> Optional[User]:
row = await self._conn.fetchrow(
"SELECT id, name, email FROM users WHERE id = $1", user_id
)
return User(**dict(row)) if row else None
async def add(self, user: User) -> None:
await self._conn.execute(
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
user.id, user.name, user.email
)
# ─── Fake репозиторій для тестів ──────────────────────────────────────────────
class InMemoryUserRepository(AbstractUserRepository):
def __init__(self):
self._store: dict[int, User] = {}
async def get(self, user_id: int) -> Optional[User]:
return self._store.get(user_id)
async def add(self, user: User) -> None:
self._store[user.id] = user
# ─── DI: вибір реалізації ─────────────────────────────────────────────────────
async def get_user_repo(conn = Depends(get_db)) -> AbstractUserRepository:
return AsyncpgUserRepository(conn)
# ─── Ендпоінт — не знає про SQL і asyncpg ─────────────────────────────────────
@app.get("/users/{user_id}", response_model=User)
async def get_user(
user_id: int,
repo: AbstractUserRepository = Depends(get_user_repo)
):
user = await repo.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
Переваги Repository Pattern¶
| Перевага | Деталь |
|---|---|
| Тестованість | InMemoryUserRepository — тести за мілісекунди без БД |
| Розділення відповідальності | Ендпоінт ≠ SQL, ендпоінт ≠ ORM |
| Заміна БД | Новий MongoUserRepository → нуль змін в ендпоінтах |
| Читабельність | repo.get(user_id) читається як бізнес-логіка |
8. Unit of Work Pattern¶
Проблема: Незавершені транзакції¶
Якщо твій ендпоінт виконує два SQL запити і падає між ними — дані стають несумісними:
# ❌ Небезпечно: якщо insert_order кине виняток — баланс вже знятий!
await conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
await conn.execute("INSERT INTO orders VALUES (...)") # Може впасти!
Unit of Work (UoW) гарантує атомарність: або обидві операції успішні (commit), або обидві відкочені (rollback).
Реалізація¶
import abc
# ─── Абстракція UoW ───────────────────────────────────────────────────────────
class AbstractUnitOfWork(abc.ABC):
users: AbstractUserRepository # Доступ до репозиторіїв
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, tb):
await self.rollback() # Безпечно за замовчуванням!
@abc.abstractmethod
async def commit(self): ...
@abc.abstractmethod
async def rollback(self): ...
# ─── Конкретна реалізація для asyncpg ────────────────────────────────────────
class AsyncpgUnitOfWork(AbstractUnitOfWork):
def __init__(self, pool):
self._pool = pool
async def __aenter__(self):
self._conn = await self._pool.acquire()
self._tx = self._conn.transaction()
await self._tx.start()
self.users = AsyncpgUserRepository(self._conn)
return self
async def __aexit__(self, exc_type, exc_val, tb):
await super().__aexit__(exc_type, exc_val, tb)
await self._pool.release(self._conn)
async def commit(self):
await self._tx.commit()
async def rollback(self):
await self._tx.rollback()
# ─── Fake UoW для тестів ───────────────────────────────────────────────────────
class FakeUnitOfWork(AbstractUnitOfWork):
def __init__(self):
self.users = InMemoryUserRepository()
self.committed = False
async def __aenter__(self): return self
async def __aexit__(self, *args): pass
async def commit(self): self.committed = True
async def rollback(self): pass
# ─── Сервісний шар (бізнес-логіка) ───────────────────────────────────────────
async def transfer_funds(
from_id: int, to_id: int, amount: float,
uow: AbstractUnitOfWork
) -> None:
async with uow:
sender = await uow.users.get(from_id)
receiver = await uow.users.get(to_id)
if sender.balance < amount:
raise ValueError("Insufficient funds")
sender.balance -= amount
receiver.balance += amount
await uow.users.update(sender)
await uow.users.update(receiver)
await uow.commit() # Атомарно: обидва UPDATE або нічого
# ─── FastAPI ендпоінт ─────────────────────────────────────────────────────────
async def get_uow():
yield AsyncpgUnitOfWork(db_pool)
@app.post("/transfer")
async def transfer_endpoint(
from_id: int, to_id: int, amount: float,
uow: AbstractUnitOfWork = Depends(get_uow)
):
# Ендпоінт не містить ні SQL, ні транзакцій, ні БД деталей!
await transfer_funds(from_id, to_id, amount, uow)
return {"status": "transferred"}
UoW Execution Timeline¶
[T=0] HTTP POST /transfer
[T=1] DI: get_uow() → AsyncpgUnitOfWork(pool)
[T=2] async with uow: → __aenter__
├── pool.acquire() → з'єднання #5
└── transaction.start() → BEGIN TRANSACTION (SQL)
[T=3] transfer_funds() виконується
[T=4] commit() → COMMIT TRANSACTION (SQL)
[T=5] __aexit__: rollback() ігнорується (вже committed)
[T=6] pool.release(conn) → з'єднання #5 повернуто
[T=7] HTTP 200 Response
--- Якщо виникає виняток між T=3 і T=4: ---
[T=E] Exception кинуто
[T=E+1] __aexit__(exc_type=...) → rollback() → ROLLBACK (SQL)
[T=E+2] pool.release(conn)
[T=E+3] HTTP 500 Response (дані не змінені!)
Тестування з FakeUoW¶
import pytest
@pytest.mark.asyncio
async def test_transfer_funds():
uow = FakeUnitOfWork()
uow.users._store = {
1: User(id=1, balance=500),
2: User(id=2, balance=100),
}
await transfer_funds(1, 2, 200, uow)
assert uow.committed is True
assert uow.users._store[1].balance == 300
assert uow.users._store[2].balance == 300
# Тест за мілісекунди, без реальної БД!
9. Мікросервісна Архітектура¶
FastAPI у мікросервісній системі¶
Клієнти
┌────────┴────────┐
Browser Mobile App
└────────┬────────┘
│
┌──────▼──────┐
│ API Gateway │ (nginx / Traefik)
│ Load Balancer│
└──┬───┬───┬──┘
│ │ │
┌────────┘ │ └────────┐
│ │ │
┌─────▼──────┐ ┌───▼────┐ ┌────▼──────┐
│ FastAPI │ │FastAPI │ │ FastAPI │
│ Users │ │Orders │ │ Payments │
│ Service │ │Service │ │ Service │
└─────┬──────┘ └───┬────┘ └────┬──────┘
│ │ │
┌─────▼──────┐ ┌───▼────┐ ┌────▼──────┐
│PostgreSQL │ │MongoDB │ │ Redis │
└────────────┘ └────────┘ └───────────┘
BFF (Backend for Frontend)¶
# Агрегатор для мобільного клієнта — збирає дані з кількох сервісів
@app.get("/mobile/dashboard/{user_id}")
async def mobile_dashboard(user_id: int):
async with aiohttp.ClientSession() as session:
# Паралельно запитуємо 3 мікросервіси
user_data, orders, balance = await asyncio.gather(
fetch_user(session, user_id),
fetch_orders(session, user_id),
fetch_balance(session, user_id),
)
return {"user": user_data, "orders": orders, "balance": balance}
10. Production Deployment¶
Стек розгортання¶
Internet
│
[ Traefik / Nginx ] ← TLS termination, load balancing, static files
│
[ uvicorn ] ← ASGI server, 1+ workers
│
[ FastAPI app ] ← application code
│
[ asyncpg pool ] ← 5–20 persistent connections
│
[ PostgreSQL ] ← database
docker-compose.yml (production-ready)¶
services:
api:
build: .
command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
environment:
- DATABASE_URL=postgresql://user:pass@db/mydb
depends_on:
db:
condition: service_healthy
deploy:
resources:
limits:
memory: 512M
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
Кількість uvicorn workers¶
# Правило: (2 × CPU cores) + 1
# Для I/O-bound FastAPI — часто достатньо 1–2 worker
# ProcessPoolExecutor у worker обробляє CPU-bound задачі
uvicorn main:app --workers 4 # 4 незалежних Event Loop
11. Mermaid Діаграми¶
Діаграма 1: FastAPI Request Lifecycle¶
flowchart TD
REQ([HTTP Request]) --> UVICORN[uvicorn<br>ASGI Server]
UVICORN --> MIDDLEWARE[Middleware Stack<br>CORS / Auth / Logging]
MIDDLEWARE --> ROUTER[FastAPI Router<br>маршрутизація за URL]
ROUTER --> DETECT{Тип<br>функції?}
DETECT -- "async def" --> DI_ASYNC[DI System<br>виконує Depends async]
DETECT -- "def sync" --> THREADPOOL[ThreadPoolExecutor<br>окремий потік]
DI_ASYNC --> DEPS_CHECK{Blocking<br>у залежностях?}
DEPS_CHECK -- "await (OK)" --> HANDLER_ASYNC[Async Handler<br>Event Loop вільний]
DEPS_CHECK -- "blocking (❌)" --> FREEZE[⛔ Event Loop<br>ЗАМОРОЖЕНИЙ]
HANDLER_ASYNC --> PYDANTIC[Pydantic<br>Серіалізація відповіді]
THREADPOOL --> PYDANTIC
PYDANTIC --> RESP([HTTP Response JSON])
FREEZE --> BLOCK_ALL[Всі клієнти чекають!]
classDef step fill:#eceff1,stroke:#546e7a,stroke-width:1px,color:#1a1a1a;
classDef decision fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#1a1a1a;
classDef success fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#1a1a1a;
classDef error fill:#ffebee,stroke:#c62828,stroke-width:3px,color:#1a1a1a;
classDef warning fill:#fff8e1,stroke:#e65100,stroke-width:2px,color:#1a1a1a;
class REQ,UVICORN,MIDDLEWARE,ROUTER,DI_ASYNC,PYDANTIC step
class DETECT,DEPS_CHECK decision
class HANDLER_ASYNC,RESP success
class THREADPOOL warning
class FREEZE,BLOCK_ALL error
Діаграма 2: Connection Pool Lifecycle¶
sequenceDiagram
participant A as Клієнт A
participant B as Клієнт B
participant C as Клієнт C
participant P as Connection Pool (max=2)
participant DB as PostgreSQL
A->>P: pool.acquire()
P-->>A: з'єднання #1 (миттєво!)
B->>P: pool.acquire()
P-->>B: з'єднання #2 (миттєво!)
C->>P: pool.acquire()
P-->>C: ⏳ Semaphore.acquire() — SUSPEND
A->>DB: await conn.fetchrow(SQL)
B->>DB: await conn.fetchrow(SQL)
Note over A,B: Обидва чекають відповідь БД<br/>Event Loop вільний!
DB-->>A: Result A
A->>P: conn.release() → з'єднання #1 повернуто
P-->>C: з'єднання #1 передано клієнту C!
C->>DB: await conn.fetchrow(SQL)
DB-->>B: Result B
B->>P: conn.release()
DB-->>C: Result C
C->>P: conn.release()
Діаграма 3: Repository + DI Pattern¶
flowchart LR
subgraph HTTP["HTTP Layer (FastAPI)"]
EP["@app.get<br>/users/{id}"]
end
subgraph DI["Dependency Injection"]
DEPS["Depends(get_user_repo)"]
end
subgraph REPO["Repository Layer"]
ABS["AbstractUserRepository<br>(abc.ABC)"]
ASYNCPG["AsyncpgUserRepository<br>(production)"]
FAKE["InMemoryUserRepository<br>(tests)"]
end
subgraph INFRA["Infrastructure"]
PG[(PostgreSQL<br>asyncpg)]
MEM[("dict in memory<br>(tests)")]
end
EP -->|"user_id: int<br>repo: AbstractUserRepository"| DEPS
DEPS -->|"prod"| ASYNCPG
DEPS -->|"test"| FAKE
ASYNCPG --> ABS
FAKE --> ABS
ASYNCPG --> PG
FAKE --> MEM
ABS -->|"await repo.get(user_id)"| EP
Діаграма 4: Unit of Work — Transaction Safety¶
stateDiagram-v2
direction LR
[*] --> IDLE : FastAPI отримує запит
IDLE --> CONNECTING : async with uow:<br>__aenter__()
CONNECTING --> ACTIVE : pool.acquire()<br>transaction.start()<br>BEGIN SQL
ACTIVE --> WORKING : Бізнес-логіка<br>repo.add() / update()
WORKING --> COMMITTED : await uow.commit()<br>COMMIT SQL ✅
WORKING --> ROLLEDBACK : Exception!<br>__aexit__(exc)<br>ROLLBACK SQL ⛔
COMMITTED --> RELEASE : pool.release(conn)
ROLLEDBACK --> RELEASE : pool.release(conn)
RELEASE --> [*] : HTTP Response
note right of ROLLEDBACK
Безпека за замовчуванням:
__aexit__ ЗАВЖДИ викликає rollback()
якщо commit() не був явним
end note
Діаграма 5: Microservices Architecture¶
flowchart TD
Browser([Browser SPA]) --> GW
Mobile([Mobile App]) --> BFF
GW[API Gateway<br>Traefik / Nginx<br>TLS + Rate Limiting]
BFF[BFF Service<br>FastAPI<br>Агрегатор для Mobile]
GW --> US[Users Service<br>FastAPI + asyncpg]
GW --> OS[Orders Service<br>FastAPI + MongoDB]
GW --> PS[Payments Service<br>FastAPI + Redis]
BFF --> US
BFF --> OS
BFF --> PS
US --> PG[(PostgreSQL)]
OS --> MONGO[(MongoDB)]
PS --> REDIS[(Redis)]
US -.->|"async events"| MQ[[Message Queue<br>RabbitMQ / Kafka]]
OS -.->|"async events"| MQ
PS -.->|"async events"| MQ
MQ -.-> NOTIFY[Notification Service<br>Email / Push]
Діаграма 6: WSGI vs ASGI під навантаженням¶
Числа на графіку — ілюстрація, не вимір
Це схема того, як поводяться три моделі, а не результат бенчмарку. Реальні виміри на fastapi_demo — в уроці 38.
xychart-beta
title "Throughput під навантаженням (req/s)"
x-axis [10, 50, 100, 500, 1000, 5000]
y-axis "req/s" 0 --> 2000
line "ASGI FastAPI (async)" [95, 490, 980, 1800, 1950, 1990]
line "WSGI Gunicorn (4 workers)" [95, 380, 600, 650, 640, 620]
line "async def + blocking" [5, 5, 5, 5, 5, 5]
Швидкий чеклист перед деплоєм FastAPI¶
□ Всі DB/HTTP виклики використовують await (asyncpg, aiohttp, httpx)
□ Жодного time.sleep() в async def
□ Connection pool ініціалізується в lifespan (не per-request)
□ DI через yield гарантує release з'єднань
□ CPU-bound задачі делеговані в ProcessPoolExecutor
□ Repository Pattern відокремлює SQL від бізнес-логіки
□ UoW Pattern гарантує атомарність транзакцій
□ Fake репозиторії покривають юніт-тести
□ uvicorn --workers = (2 × cpu_count) + 1
□ Pydantic response_model валідує вихідні дані