Widoki asynchroniczne Django i ASGI w 2026: wydajność i pytania rekrutacyjne

Szczegółowe omówienie widoków asynchronicznych Django i ASGI w 2026 roku: jak działają od środka, który serwer wdrożyć, asynchroniczny ORM i pułapka SynchronousOnlyOperation oraz pytania rekrutacyjne.

Architektura widoków asynchronicznych Django i ASGI obsługująca równoczesne żądania

Widoki asynchroniczne Django pozwalają jednemu procesowi worker obsłużyć setki równoczesnych żądań ograniczonych operacjami I/O bez przydzielania osobnego wątku każdemu połączeniu, ale tylko wtedy, gdy ścieżka kodu pozostaje asynchroniczna od widoku aż po każde wywołanie sieciowe. Odkąd Django 3.1 wprowadziło widoki async def, a Django 4.1 dodało asynchroniczny interfejs ORM, framework stał się wiarygodną platformą ASGI, jednak większość wdrożeń produkcyjnych nadal działa pod WSGI i pozostawia tę współbieżność niewykorzystaną. To szczegółowe omówienie wyjaśnia, jak widoki asynchroniczne zachowują się pod ASGI, które serwery uruchamiać w 2026 roku, jakie pułapki ORM prowadzą do wyjątku SynchronousOnlyOperation oraz jakie pytania rekrutacyjne odróżniają inżynierów, którzy wdrażali asynchroniczne Django, od tych, którzy jedynie o nim czytali.

Kiedy async naprawdę pomaga

Widoki asynchroniczne opłacają się, gdy żądanie spędza większość czasu na oczekiwaniu na operacje I/O, nad którymi nie ma kontroli: zewnętrzne API HTTP, wolne usługi zależne czy długotrwałe odpowiedzi strumieniowe. W przypadku pracy ograniczonej procesorem lub szybkich zapytań do lokalnej bazy danych widok synchroniczny wsparty większą liczbą procesów worker Gunicorna jest zwykle szybszy i znacznie łatwiejszy do analizy.

Jak działają widoki asynchroniczne Django pod ASGI

WSGI jest z założenia synchroniczne: każde przetwarzane żądanie zajmuje jeden wątek lub proces worker od początku do końca, więc 50 wolnych żądań wymaga 50 procesów worker. ASGI zastępuje ten model event loopem, który przeplata wiele żądań na jednym procesie worker, wstrzymując korutynę za każdym razem, gdy oczekuje ona (await) na I/O, i wznawiając ją, gdy dane nadejdą. Oficjalna dokumentacja Django dotycząca async opisuje dwa adaptery, które umożliwiają to współistnienie: pod ASGI Django uruchamia widoki async def natywnie na pętli, a widoki synchroniczne przenosi do threadpoola za pomocą sync_to_async; pod WSGI opakowuje widoki asynchroniczne w async_to_sync i wykonuje je do końca, tracąc wszelkie korzyści ze współbieżności.

Praktyczna konsekwencja jest bezwzględna: widok async def wdrożony na serwerze WSGI zachowuje się jak wolniejszy widok synchroniczny. Współbieżność pojawia się dopiero na serwerze ASGI. Minimalny widok asynchroniczny wywołujący zewnętrzne API wygląda niemal identycznie jak jego synchroniczny odpowiednik, a różnica koncentruje się w await.

python
# views.py
import httpx
from django.http import JsonResponse

async def dashboard(request):
    # Pod ASGI ta korutyna działa na event loopie, więc proces worker może
    # obsługiwać inne żądania, gdy httpx oczekuje na rundę po sieci.
    async with httpx.AsyncClient(timeout=5.0) as client:
        response = await client.get("https://api.example.com/metrics")
    return JsonResponse(response.json())

Zysk nie polega tutaj na tym, że pojedyncze żądanie kończy się szybciej; zajmuje ono tyle samo czasu rzeczywistego. Zysk polega na tym, że proces worker nie jest zablokowany podczas await, więc przepustowość przy obciążeniu współbieżnym rośnie, a liczba procesów pozostaje bez zmian.

Ten sam model odblokowuje wzorce odpowiedzi, które pod WSGI są niewygodne. Od Django 4.2 StreamingHttpResponse przyjmuje asynchroniczny generator, więc widok może zwracać kolejne fragmenty w miarę, jak wytwarza je źródło nadrzędne, co sprawdza się w przypadku Server-Sent Events oraz proxowanych strumieni tokenów LLM, gdzie połączenie pozostaje otwarte przez kilka sekund. Trwałe protokoły dwukierunkowe, takie jak WebSockets, nadal przechodzą przez Django Channels, a nie przez zwykłe widoki HTTP, ale obie technologie opierają się na tym samym fundamencie ASGI, więc projekt, który wdraża widoki asynchroniczne, poniósł już koszt infrastruktury pod przyszłe funkcje czasu rzeczywistego.

Wybór serwera ASGI dla Django w 2026 roku

Django dostarcza obiekt aplikacji ASGI w pliku asgi.py, ale sam się nie uruchamia; pętlą zdarzeń steruje dedykowany serwer ASGI. Specyfikacja ASGI definiuje interfejs, który implementuje każdy z tych serwerów, dlatego na poziomie protokołu są one wymienne i różnią się głównie wydajnością oraz zakresem obsługiwanych protokołów.

| Serwer | Zaimplementowany w | Najlepsze zastosowanie | |--------|----------------|----------| | Uvicorn | Python (uvloop) | Ogólny ASGI, HTTP/1.1, typowy domyślny wybór | | Daphne | Python | Django Channels, WebSockets, HTTP/2 | | Granian | Rust | Najwyższa surowa przepustowość, HTTP/1 i HTTP/2 | | Hypercorn | Python | HTTP/2 i HTTP/3 (QUIC) |

W większości wdrożeń w 2026 roku niezawodnym wyborem pozostaje Uvicorn uruchamiany jako klasa workera Gunicorna: Gunicorn nadzoruje cykl życia procesów, restartuje procesy worker po awarii i obsługuje sygnały, a Uvicorn steruje event loopem wewnątrz każdego z nich. Jeden szczegół migracji utrudnia aktualizacje: od wersji Uvicorn 0.30 klasy workera Gunicorna zostały wydzielone z rdzenia Uvicorna do osobnego pakietu uvicorn-worker, więc zmieniła się ścieżka importu.

bash
# Najpierw zainstaluj pakiet workera: pip install uvicorn-worker
gunicorn myproject.asgi:application \
    --workers 4 \
    --worker-class uvicorn_worker.UvicornWorker \
    --bind 0.0.0.0:8000

Zespoły dążące do maksymalnej przepustowości coraz częściej sięgają po Granian, serwer oparty na Ruście, który eliminuje narzut parsowania HTTP po stronie Pythona. Niezależnie od wyboru, to właśnie sposób wdrożenia sprawia, że async pojawia się na produkcji tylko wtedy, gdy zostanie świadomie skonfigurowany; konfiguracja specyficzna dla ASGI odzwierciedla dbałość operacyjną omówioną w module pytania rekrutacyjne o wdrażanie Django.

Asynchroniczny ORM i pułapka SynchronousOnlyOperation

Django 4.1 dodało asynchroniczne metody zapytań w całym ORM: aget, acreate, asave, adelete, acount, aexists, aaggregate oraz iterację za pomocą async for. Zapewniają one przyjazną dla async powierzchnię, ale w 2026 roku wciąż obowiązuje kluczowe zastrzeżenie: leżąca poniżej warstwa bazy danych nie jest natywnie asynchroniczna. Nawet z psycopg3 Django wykonuje zapytania w threadpoolu i udostępnia je przez asynchroniczne wrappery, więc asynchroniczny ORM poprawia ergonomię i unika blokowania pętli, nie zapewniając prawdziwie asynchronicznego I/O bazy danych od początku do końca.

Najczęstszym błędem jest bezpośrednie wywołanie synchronicznej metody ORM wewnątrz widoku asynchronicznego. Django wykrywa kontekst asynchroniczny i odmawia, zgłaszając SynchronousOnlyOperation, aby zapobiec zamrożeniu event loopa przez blokujące wywołanie dla wszystkich pozostałych żądań współdzielących ten proces worker.

SynchronousOnlyOperation to funkcja, nie błąd

Ten wyjątek to zabezpieczenie, a nie błąd. Sięgnięcie po DJANGO_ALLOW_ASYNC_UNSAFE=true, aby go wyciszyć, przywraca dokładnie to blokujące zachowanie, które async miał wyeliminować. Poprawnym rozwiązaniem jest asynchroniczna metoda ORM (aget zamiast get) lub jawne opakowanie w sync_to_async kodu, który nie ma asynchronicznego odpowiednika.

Widoki asynchroniczne czyta się przejrzyście, gdy asynchroniczne metody zapytań zastąpią swoje synchroniczne odpowiedniki. Poniższy wzorzec zlicza wiersze i pobiera pierwsze dziesięć rekordów, ani razu nie opuszczając event loopa.

python
# views.py
from django.http import JsonResponse
from .models import Article

async def latest_articles(request):
    # acount() i iteracja async to bezpieczne dla async wrappery wokół ORM.
    count = await Article.objects.acount()
    titles = []
    async for article in Article.objects.order_by("-created_at")[:10]:
        titles.append(article.title)
    return JsonResponse({"count": count, "titles": titles})

Część kodu nie ma asynchronicznego odpowiednika: zewnętrzny SDK oferujący wyłącznie wywołania synchroniczne albo gęsty łańcuch przejść ORM, którego nie opłaca się przepisywać. Opakowanie go w sync_to_async z biblioteki asgiref przenosi pracę do wątku i utrzymuje responsywność pętli. Argument thread_sensitive=True to bezpieczna wartość domyślna, ponieważ kieruje każde opakowane wywołanie do jednego współdzielonego wątku wykonawczego, zachowując stan połączenia z bazą danych i transakcji, który zostałby uszkodzony, gdyby wywołania rozproszyły się po dowolnych wątkach.

python
# views.py
from asgiref.sync import sync_to_async
from .services import build_report  # funkcja synchroniczna

async def report_view(request):
    # thread_sensitive=True utrzymuje opakowane wywołania w jednym wspólnym wątku,
    # zachowując stan połączenia i transakcji przy przekraczaniu granicy.
    report = await sync_to_async(build_report, thread_sensitive=True)(request.user.id)
    return JsonResponse(report)

Wydajność async w Django: kiedy współbieżność wygrywa

Wydajność async w Django to historia o nakładaniu się operacji I/O, a nie o surowej szybkości. Najwyraźniejszy zysk daje rozgałęzienie niezależnych wywołań sieciowych za pomocą asyncio.gather: trzy zewnętrzne żądania po 200 ms każde kosztują sekwencyjnie około 600 ms, ale wykonane współbieżnie kończą się w około 200 ms, ponieważ operacje await nakładają się na tej samej pętli.

python
# views.py
import asyncio
import httpx
from django.http import JsonResponse

async def aggregate(request):
    async with httpx.AsyncClient(timeout=5.0) as client:
        # Trzy niezależne wywołania działają współbieżnie zamiast jedno po drugim.
        weather, prices, news = await asyncio.gather(
            client.get("https://api.example.com/weather"),
            client.get("https://api.example.com/prices"),
            client.get("https://api.example.com/news"),
        )
    return JsonResponse({
        "weather": weather.json(),
        "prices": prices.json(),
        "news": news.json(),
    })

Odwrotny przypadek jest równie istotny. Widok wykonujący pojedyncze szybkie lokalne zapytanie nic nie zyskuje na async, a często traci, ponieważ każde żądanie płaci teraz za planowanie w event loopie oraz przeskok do threadpoola z synchronicznym sterownikiem bazy danych. Async nakłada też subtelniejszy podatek na każdej granicy sync/async: mieszanie synchronicznego i asynchronicznego middleware zmusza Django do przełączania się między pętlą a wątkiem przy każdym przejściu, a żądanie przekraczające tę granicę kilkukrotnie na odpowiedź kumuluje realne opóźnienie. Utrzymanie stosu middleware w całości asynchronicznego lub w całości synchronicznego eliminuje te przełączenia.

Decyzja sprowadza się do charakteru obciążenia, a nie do upodobania do nowoczesnej składni.

| Rodzaj obciążenia | Lepszy domyślny wybór | |----------|----------------| | Wiele wolnych wywołań zewnętrznego API na żądanie | Widok asynchroniczny z asyncio.gather | | Długotrwałe strumieniowanie lub Server-Sent Events | Widok asynchroniczny z asynchronicznym generatorem | | Pojedyncze szybkie zapytanie do bazy, niskie użycie CPU | Widok synchroniczny, więcej procesów worker Gunicorna | | Renderowanie lub obliczenia ograniczone CPU | Widok synchroniczny, przeniesienie do kolejki zadań |

W przypadku naprawdę długotrwałych zadań widoki asynchroniczne są całkowicie niewłaściwym narzędziem; utrzymywanie otwartego żądania przez wiele minut pracy marnuje połączenie niezależnie od modelu współbieżności. Taka praca należy do procesu worker działającego w tle, co jest kompromisem omówionym w przewodniku zadania asynchroniczne Celery w Django.

Wdrożenie powinno wynikać z pomiarów, a nie z intuicji. Uczciwym sposobem uzasadnienia async jest przeprowadzenie testów obciążeniowych konkretnego endpointu narzędziem takim jak Locust czy k6 przy realistycznej współbieżności, porównując wersję synchroniczną na N procesach worker z wersją asynchroniczną na tym samym sprzęcie. Endpointy zdominowane przez pojedyncze wywołanie zewnętrzne o wysokim opóźnieniu wykazują dużą przewagę na korzyść async; endpointy zdominowane przez szybkie zapytania często wypadają nieco gorzej w wersji asynchronicznej, gdy uwzględni się przeskok do threadpoola. Wcześniejsze profilowanie żądania ujawnia też, czy wąskim gardłem w ogóle jest I/O, ponieważ async nic nie da widokowi, który jest wolny z powodu niezaindeksowanych zapytań, co jest problemem poruszonym w przewodniku optymalizacja zapytań ORM w Django.

Gotowy na rozmowy o Django?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Pytania rekrutacyjne o async w Django na 2026 rok

Rozmowa rekrutacyjna o async w Django sprawdza, czy kandydat rozumie model, czy jedynie rozpoznaje słowa kluczowe. Poniższe pytania odzwierciedlają to, o co faktycznie pytają rozmowy na stanowiska senior w 2026 roku, a każde z nich zawiera odpowiedź, jakiej udziela doświadczony inżynier.

Co zmienia się, gdy widok asynchroniczny działa pod WSGI zamiast pod ASGI? Pod WSGI Django opakowuje korutynę w async_to_sync i wykonuje ją do końca na wątku żądania, więc zachowuje się jak wolniejszy widok synchroniczny bez żadnej korzyści ze współbieżności. Dopiero serwer ASGI z działającym event loopem pozwala procesowi worker obsługiwać inne żądania podczas await.

Dlaczego Article.objects.get(pk=1) zgłasza SynchronousOnlyOperation wewnątrz widoku asynchronicznego? Synchroniczne wywołanie ORM zablokowałoby event loop, wstrzymując wszystkie pozostałe żądania na tym procesie worker. Django wykrywa kontekst asynchroniczny i odmawia. Rozwiązaniem jest metoda asynchroniczna await Article.objects.aget(pk=1) lub sync_to_async dla kodu bez asynchronicznego odpowiednika.

Czy asynchroniczny ORM Django jest naprawdę nieblokujący? Nie. Metody asynchroniczne to ergonomiczne wrappery; leżące poniżej zapytania nadal wykonują się w threadpoolu, ponieważ sterowniki bazy danych nie są zintegrowane do natywnego asynchronicznego wykonania od początku do końca. Korzyścią jest utrzymanie wolnej pętli, a nie równoległe I/O bazy danych na poziomie sterownika.

Kiedy należy wybrać widoki asynchroniczne zamiast kolejki zadań takiej jak Celery? Widoki asynchroniczne pasują do współbieżności I/O w zakresie żądania, która musi zakończyć się przed wysłaniem odpowiedzi, na przykład agregowania kilku API. Praca, która wykracza poza żądanie, wymaga ponawiania lub trwa wiele minut, należy do Celery lub zadań w tle Django, ponieważ utrzymywanie otwartego połączenia HTTP na jej potrzeby marnuje zasoby serwera.

Jaki jest koszt mieszania synchronicznego i asynchronicznego middleware? Każde przejście między komponentem synchronicznym a asynchronicznym zmusza Django do przeskoku między event loopem a wątkiem za pomocą sync_to_async lub async_to_sync. Żądanie wielokrotnie przekraczające tę granicę płaci kumulujący się narzut, więc jednolity stos middleware działa lepiej niż przeplatany.

Podsumowanie

  • Widoki asynchroniczne zapewniają współbieżność tylko na serwerze ASGI; ten sam widok pod WSGI działa synchronicznie przez async_to_sync.
  • Wdrażaj Uvicorn przez pakiet uvicorn-worker pod Gunicornem lub Granian dla maksymalnej przepustowości, a konfigurację ASGI traktuj jako świadomy krok wdrożenia.
  • Sięgaj po widoki asynchroniczne, gdy żądanie czeka na wiele wolnych zewnętrznych wywołań I/O; endpointy z szybkimi zapytaniami i ograniczone CPU pozostaw synchroniczne, z większą liczbą procesów worker.
  • Używaj aget, acreate i async for wewnątrz widoków asynchronicznych i pamiętaj, że ORM nadal wykonuje zapytania w threadpoolu, a nie natywnie asynchronicznie.
  • Traktuj SynchronousOnlyOperation jako zabezpieczenie: naprawiaj je metodami asynchronicznymi lub sync_to_async(thread_sensitive=True), nigdy przez wyłączenie kontroli.
  • Utrzymuj stos middleware w całości synchroniczny lub asynchroniczny, aby uniknąć kary za przełączanie wątków przy każdym przekroczeniu granicy.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#django
#async
#asgi
#python
#performance

Udostępnij

Powiązane artykuły