Перейти до змісту

09 — Real-World Use Cases: коли async Django справді потрібен

Навіщо це потрібно

Ти пройшов весь шлях: asyncio, ASGI, async views, async ORM, sync_to_async, httpx, benchmarking.

Останнє питання: коли все це справді варто використовувати?

Async — це інструмент. Як молоток: ідеальний для цвяхів і марний для гвинтів. Цей документ — карта: де цвяхи, де гвинти.


🧠 Ментальна модель

Уяви диспетчера екстреної служби.

Синхронний диспетчер: бере один дзвінок, повністю вирішує ситуацію (відправляє машину, чекає підтвердження, закриває справу), потім бере наступний.

Асинхронний диспетчер: приймає 10 дзвінків одночасно. Кожному говорить "зрозумів, машина їде" і тримає в курсі. Не чекає поки кожна машина доїде — просто координує.

Для складних справ що потребують повної уваги (CPU-bound) — кращий синхронний диспетчер. Для координації багатьох одночасних з'єднань — асинхронний.


Async Suitability Matrix

Тип задачі Приклад Архітектура Причина
Concurrent I/O Агрегація даних з 5 API ASGI + Async View + httpx await звільняє event loop між запитами
Persistent connections Чат, Live dashboard, WebSockets Django Channels + ASGI Один event loop тримає тисячі відкритих з'єднань
High-volume webhooks Stripe, GitHub webhooks Async View + ASGI Миттєво приймає й підтверджує без блокування
Streaming відповіді CSV/JSON streaming, SSE Async + StreamingHttpResponse Надсилає частинами без блокування сервера
Стандартний CRUD Список статей, профіль користувача Sync View (WSGI або ASGI) ORM достатньо швидкий, async додає складність
Image/Video processing Resize аватару, конвертація відео Celery Worker CPU-bound: блокує event loop, краще у фоні
ML inference Класифікація тексту, рекомендації Celery + GPU Worker Тяжкий CPU/GPU, не для event loop
PDF/report generation Звіти, рахунки-фактури Celery Worker CPU-bound + I/O, довго виконується

Коли async справді корисний

1. Агрегація з кількох зовнішніх API

Класичний use case: дашборд, що збирає дані з кількох джерел.

sequenceDiagram
    participant B as Браузер
    participant V as Django Async View
    participant A1 as Billing API (0.8s)
    participant A2 as Inventory API (1.2s)
    participant A3 as Shipping API (0.6s)

    B->>V: GET /dashboard/
    Note over V: Запускає всі три паралельно
    V->>A1: await httpx.get()
    V->>A2: await httpx.get()
    V->>A3: await httpx.get()
    Note over V: Event loop вільний!
    A3-->>V: 0.6s — відповідь
    A1-->>V: 0.8s — відповідь
    A2-->>V: 1.2s — відповідь
    Note over V: Загальний час: ~1.2s (не 2.6s!)
    V-->>B: Агрегована відповідь
import asyncio
import httpx
from django.http import JsonResponse

async def dashboard_view(request):
    async with httpx.AsyncClient(timeout=10.0) as client:
        billing, inventory, shipping = await asyncio.gather(
            client.get("https://billing.internal/summary"),
            client.get("https://inventory.internal/status"),
            client.get("https://shipping.internal/active"),
            return_exceptions=True
        )

    return JsonResponse({
        "billing": billing.json() if not isinstance(billing, Exception) else None,
        "inventory": inventory.json() if not isinstance(inventory, Exception) else None,
        "shipping": shipping.json() if not isinstance(shipping, Exception) else None,
    })

2. WebSockets і чат-системи

Django Channels з ASGI дозволяє один event loop тримати тисячі відкритих WebSocket-з'єднань:

# consumers.py (Django Channels)
from channels.generic.websocket import AsyncWebsocketConsumer
import json

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        await self.channel_layer.group_add(self.room_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.room_name, self.channel_name)

    async def receive(self, text_data):
        data = json.loads(text_data)
        # Розсилаємо повідомлення всій групі
        await self.channel_layer.group_send(
            self.room_name,
            {"type": "chat_message", "message": data["message"]}
        )

    async def chat_message(self, event):
        await self.send(text_data=json.dumps({"message": event["message"]}))

3. High-volume webhooks

Endpoints що приймають тисячі webhook-запитів (Stripe, GitHub):

from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from asgiref.sync import sync_to_async

@csrf_exempt
async def stripe_webhook_view(request):
    if request.method != "POST":
        return JsonResponse({"error": "Method not allowed"}, status=405)

    payload = request.body

    # Швидко підтверджуємо отримання (Stripe очікує < 30s)
    # Реальна обробка — в Celery task
    await sync_to_async(enqueue_webhook_processing)(payload)

    return JsonResponse({"status": "received"})

4. Server-Sent Events (Live updates)

import asyncio
from django.http import StreamingHttpResponse

async def live_updates(request):
    async def event_stream():
        count = 0
        while True:
            count += 1
            yield f"data: {{\"update\": {count}}}\n\n"
            await asyncio.sleep(2)  # Оновлення кожні 2 секунди

    return StreamingHttpResponse(
        event_stream(),
        content_type="text/event-stream"
    )

Коли async НЕ потрібен

CPU-bound задачі: Event loop заморожується

# ❌ НЕ роби так — блокує весь сервер
async def generate_report_view(request):
    # PDF генерація — CPU-bound!
    pdf_bytes = generate_pdf_report(request.user.id)  # 10 секунд
    return HttpResponse(pdf_bytes, content_type="application/pdf")

Поки генерується PDF — сервер не відповідає нікому.

Правильна архітектура: Celery Worker

graph TD
    A[Браузер] -->|POST /generate-report/| B[Django View\nSync або Async]
    B -->|Celery task| C[Redis / RabbitMQ]
    B -->|202 Accepted + task_id| A
    C --> D[Celery Worker\nGenerates PDF]
    D --> E[Зберігає файл\nNotifies user]
    A -->|GET /report/status/task_id| B
    B --> F[{"status": "done", "url": "..."}]

    style D fill:#fff3e0,stroke:#e65100
    style C fill:#e3f2fd,stroke:#1565c0
# tasks.py (Celery)
from celery import shared_task

@shared_task
def generate_pdf_report(user_id):
    # CPU-bound — безпечно у Celery worker
    user = User.objects.get(id=user_id)
    pdf = create_pdf(user)  # Займає скільки завгодно часу
    save_to_storage(pdf, user_id)
    notify_user(user_id)

# views.py
from django.http import JsonResponse
from .tasks import generate_pdf_report

def request_report_view(request):
    # Запускаємо задачу і одразу повертаємо відповідь
    task = generate_pdf_report.delay(request.user.id)
    return JsonResponse({"task_id": str(task.id), "status": "processing"}, status=202)

Decision Tree: коли обирати async

graph TD
    A[Нова задача] --> B{Потрібне постійне\nз'єднання?\nWebSocket, SSE}
    B -->|Так| C[Django Channels + ASGI ✅]
    B -->|Ні| D{CPU-bound?\nОбробка зображень,\nML, PDF}

    D -->|Так| E[Sync View + Celery Worker ✅]
    D -->|Ні| F{Кілька повільних\nзовнішніх API-запитів?}

    F -->|Так| G[Async View + httpx + gather ✅]
    F -->|Ні| H{Велика кількість\nодночасних запитів?}

    H -->|Так| I[Async View + ASGI ✅]
    H -->|Ні| J[Standard Sync View ✅\nПростіший і достатній]

    style C fill:#e8f5e9,stroke:#2e7d32
    style E fill:#fff3e0,stroke:#e65100
    style G fill:#e8f5e9,stroke:#2e7d32
    style I fill:#e8f5e9,stroke:#2e7d32
    style J fill:#e3f2fd,stroke:#1565c0

Архітектурна діаграма: async дашборд

graph TD
    Browser -->|GET /dashboard/| ASGI[ASGI Server\nUvicorn]
    ASGI --> View[Async View]
    View -->|await gather| H1[httpx → Billing API]
    View -->|await gather| H2[httpx → Inventory API]
    View -->|await gather| H3[httpx → Analytics API]
    View -->|await aget| DB[(PostgreSQL)]
    H1 --> View
    H2 --> View
    H3 --> View
    DB --> View
    View --> ASGI
    ASGI --> Browser

    style View fill:#e8f5e9,stroke:#2e7d32
    style ASGI fill:#e3f2fd,stroke:#1565c0

Архітектурна діаграма: Celery для CPU-bound

graph TD
    Browser -->|POST /process/| WebServer[Django Sync View]
    WebServer -->|task.delay| Broker[(Redis / RabbitMQ)]
    WebServer -->|202 task_id| Browser
    Broker --> Worker[Celery Worker\nCPU-bound Processing]
    Worker -->|Результат| Storage[S3 / DB]
    Worker -->|Notification| Notification[Email / WebSocket]
    Browser -->|GET /status/task_id| WebServer
    WebServer -->|task.result| Browser

    style Worker fill:#fff3e0,stroke:#e65100
    style Broker fill:#e3f2fd,stroke:#1565c0

Типова помилка початківця

❌ Async скрізь без причини

# У тебе звичайний CRUD view — навіщо async?
async def article_list_view(request):
    articles = [a async for a in Article.objects.all()[:20]]
    return JsonResponse({"articles": [...]})

Якщо view просто читає зі своєї БД і повертає JSON — sync view простіший, зрозуміліший і не вимагає ASGI, async ORM і всього стеку.

✅ Async тільки там, де є реальна потреба

# Async виправданий: три зовнішні API одночасно
async def dashboard_view(request):
    weather, news, stocks = await asyncio.gather(
        fetch_weather(), fetch_news(), fetch_stocks()
    )
    return JsonResponse({...})

Практичне завдання

Завдання 1

Переглянь свій останній або поточний Django-проект. Визнач: - Які endpoints зараз sync — і чи варто їх переводити в async? - Які задачі зараз виконуються у view і потребують Celery? - Чи є будь-які зовнішні API-виклики, що могли б бути паралельними?

Завдання 2

Напиши async view /api/summary/, який: 1. Через httpx.AsyncClient + gather робить два паралельних запити до mock endpoints 2. Через await User.objects.acount() рахує кількість користувачів у БД 3. Повертає все в одному JsonResponse

Завдання 3

Намалюй (або опиши) архітектуру Django-додатку для обробки завантажених відео: - User завантажує відео - Сервер приймає файл - Задача конвертації йде в Celery - User отримує посилання на статус завдання - Після завершення — посилання на готовий файл

Самоперевірка

  • [ ] Я знаю, коли async views справді корисні
  • [ ] Я знаю, чому CPU-bound задачі не підходять для async
  • [ ] Я розумію архітектуру Celery для важких операцій
  • [ ] Я можу прийняти рішення: async view чи sync + Celery?
  • [ ] Я не буду додавати async "просто тому що можу"

Фінальний підсумок усього уроку

Ти пройшов повний шлях від основ до практики:

01 — Sync vs Async: Асинхронність існує через величезну різницю в швидкості між CPU і зовнішніми операціями. Async — це concurrency (перемикання), не parallelism (одночасне виконання).

02 — asyncio: async def повертає coroutine object. await — точка призупинення. Event loop керує всіма задачами. gather() — паралельне виконання кількох coroutines.

03 — ASGI: WSGI — один потік на запит. ASGI — event loop обслуговує тисячі. Uvicorn, Daphne, Hypercorn — ASGI-сервери для Django.

04 — Async Views: async def view під ASGI — нативне виконання. Sync middleware додає context switch penalties. Async view не означає автоматично кращу продуктивність.

05 — Async ORM: filter() — lazy, безпечний. get(), for — блокуючий, підніме SynchronousOnlyOperation. Використовуй aget(), acount(), async for.

06 — sync_to_async: Міст між sync і async світами. Sync-код виконується у виділеному потоці, event loop залишається вільним. Мінімізуй кількість перетинів межі.

07 — Async HTTP Clients: requests блокує event loop. httpx.AsyncClient + gather — правильна пара для паралельних API-запитів.

08 — Benchmarking: Async переваги видно тільки при concurrent навантаженні. Тестуй RPS, а не latency одного запиту. Використовуй локальний mock API.

09 — Use Cases: Async для I/O-bound concurrent задач. Celery для CPU-bound. Sync view для стандартного CRUD.


Головне правило: async — це інструмент для конкретних проблем. Якщо проблеми немає — інструмент не потрібен. Почни зі звичайного sync Django, і додавай async тільки там, де є реальний bottleneck.

← 08_benchmarking.md | INDEX.md ↑