Portál AbcLinuxu, 24. října 2025 02:59

Zlý vtip menom async v djangu (pythone)

včera 15:36 | Přečteno: 300× | Programovanie | poslední úprava: včera 15:36

Práce na asynchronnom Djangu začali okolo roku 2020. Je rok 2025. Čo tak sa pozrieť, čo sme za tú dobu získali?

Päť rokov je v oblasti IT veľmi dlhá doba aby async prestal ignorovať aj taký technologický konzervatívec a spiatočník ako ja. Po všetkých tých fantastických blogoch a benchamrkoch som nasadol na vlnu asyncu.

Nie až tak dávno som začal nový projekt v asynchrónnom frameworku FastAPI. Nebudem rozoberať, prečo som sa rozhodol práve pre Django v úlohe ORM. Akonáhle som sa začal trocha hrabať vo vnútornostiach, šokovalo ma ako zle všetko funguje. Tento blog bude o čistom djangu.

Výkon

Blogy sľubujú výkon. Tak moje konzervatívne skostnatené ja si spustí zastaralý uWSGI a oproti tomu postavím uvicorn. Oba s jedným workerom. Môj naivný view vyzerá ako väčšina dnešných benchmarkov. Veď prečo sa pozerať na komplexnú aplikáciu keď môžeme merať nič?

from django.http.response import JsonResponse


def naive_sync(request):
    return JsonResponse({"status": "ok"})


async def naive_async(request):
    return JsonResponse({"status": "ok"})

S týmto viewom si spustím benchmark pre 10 simultánnych požiadaviek a 1000 celkovo:

ab -n 1000 -c 10 'http://127.0.0.1:8000/naive/sync/'

Výsledný graf zobrazuje synchrónne volanie v uWSGI, potom synchrónne uvicorn a asynchrónne uvicorn. Vyššie číslo udáva vyššiu priepustnosť.

Naivná implementácia

Obrázok 1: Naivná implementácia

Čo sa stalo? No jednoducho v tomto príklade nemala ako vyniknúť asynchrónnosť. Okrem toho uWSGI je napísaný v C, ale oproti python implementácii je to rozdiel len 2ms na pižiadavku. Nie je to nič, čo by mi žily trhalo v reálnej aplikácii. Tento benchmark je nanič a som si toho vedomý.

Chceme ešte jeden nanič benchmark? Samozrejme! Tak teda to isté so 16 workermi.

Naivná implementácia so 16 workermi

Obrázok 2: Naivná implementácia so 16 workermi

Trocha realistickejší príklad

Väčšina aplikácií hrabe do databázy a tak si vytvorme pár tabuliek:

from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)


class Category(models.Model):
    name = models.CharField(max_length=100)


class Document(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author, related_name='documents')
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='documents', null=True)

Po naplnení databázy som ešte napísal jeden synchrónny a jeden asynchrónny view.

from asgiref.sync import sync_to_async
from django.http.response import JsonResponse
from .models import Document


def db_sync(request):
    data = []
    for document in Document.objects.order_by('pk'):
        authors = []
        data.append(
            {
                "name": document.name,
                "category": document.category.name,
                "authors": authors,
            }
        )
        for author in document.authors.all():
            authors.append({"name": author.name})
    return JsonResponse({"data": data})


async def db_async(request):
    data = []
    async for document in Document.objects.order_by('pk'):
        authors = []
        data.append(
            {
                "name": document.name,
                "category": (await sync_to_async(getattr)(document, 'category')).name,
                "authors": authors,
            }
        )
        async for author in document.authors.all():
            authors.append({"name": author.name})
    return JsonResponse({"data": data})

Vytvoril som dve prakticky rovnaké funkcie líšiace sa len v dvoch detailoch. Prvým je volanie generátora for. V jednom prípade je synchrónny (for) a v druhom prípade asynchrónny (async for). No a potom je tu táto šialenosť:

(await sync_to_async(getattr)(document, 'category')).name

Python neumožňuje kombinovať synchrónne a asynchrónne funkcie. Napíšete jedinú funkciu asynchrónne a musíte prepísať všetky funkcie, ktoré ju volajú. V postate tým infikujete celý kód. Ak ste tvorcom knižnice, môžete buď napísať knižnicu syncrhónne, alebo asynchrónne, alebo oboma spôsobmi pričom každú funkciu napíšete 2x a bude sa v 99% prípadov líšiť v tomto:

# async
async def afunkcia():
   ...
   await ainafunkcia()
   ...

# sync
def funkcia():
   ...
   inafunkcia()
   ...

Django začala ako synchrónna knižnica a postupne sa duplikuje kód. Niektoré „drobnosti“ nie sú doteraz podporované ako napríklad transakcie. No a potom sú tu ešte veci, ktoré sa nedajú prepísať ako napríklad property, kde .category potrebuje zavolať SQL dotaz, ale propery nie je polymorfná a tak volá len syncrhónny select, ktorý sa nedá zavolať z asynchrónneho kontextu. Zabalíme to teda do sync_to_async

Prístup do databázy

Obrázok 3: Prístup do databázy

To nie je možné!?! Dajme tam 16 workerov. Nech sa ukáže asyncrhṕnnosť.

Prístup do databázy so 16 workermi

Obrázok 4: Prístup do databázy so 16 workermi

Ešte väčšia katastrofa, čo? Rozmeňme si to na drobné. Databázový driver, ktorý django používa je synchrónny. Aj keby nebol, tak celá implementácia Djanga je hračkárska a vyzerá takto:

async def aget(self, *args, **kwargs):
    return await sync_to_async(self.get)(*args, **kwargs)

V tomto momente dochádza k prepnutiu kontextu, čo môže trvať rádovo okolo 1ms. Nie je dostatok vývojárov, aby implementovali a udržiavali Django so skoro každou duplikovanou funkciou. Preto sa len hráme na akože asynchrónnosť. Mimochodom viete, že veľa vývojárov vo svojich knižniciach overriduje save, aby tam pridali napríklad nejakú logiku, ja neviem date_created = now? Teraz to funguje pretože asave vyzerá takto: sync_to_async(self.save). Teraz si predstavte ako sa django knižnice začnú rozpadávať až sa začne reálne implementovať async. Celý ekosystém, desaťtisíce knižníc sa musia prepísať.

Nakoniec ešte doplním úpravu vďaka ktorej sa spustia len 2 dotazy namiesto 300:

def db_opt_sync(request):
    data = []
    for document in Document.objects.order_by('pk').prefetch_related('authors').select_related('category'):
        authors = []
        data.append(
            {
                "name": document.name,
                "category": document.category.name,
                "authors": authors,
            }
        )
        for author in document.authors.all():
            authors.append({"name": author.name})
    return JsonResponse({"data": data})


async def db_opt_async(request):
    data = []
    async for document in Document.objects.order_by('pk').prefetch_related('authors').select_related('category'):
        authors = []
        data.append(
            {
                "name": document.name,
                "category": (await sync_to_async(getattr)(document, 'category')).name,
                "authors": authors,
            }
        )
        async for author in document.authors.all():
            authors.append({"name": author.name})
    return JsonResponse({"data": data})
Prístup do databázy so 16 workermi po optimalizácii

Obrázok 5: Prístup do databázy so 16 workermi po optimalizácii

Záver

Čo som vlastne chcel povedať? Neverte všetkým sladkým rečiam v blogoch. Python má svoju filozofiu „explicit is better“ a nej podriadil aj implementáciu async. Autori knižníc sa teraz musia rozhodnúť, či budú písať synchrónne, asynchrónne, alebo budú svoj kód duplikovať, budú mať 2x viac práce a 2x viac chýb. Pritom v dynamickom jazyku s tak neskorou adaptáciou async / await nebolo farbenie vôbec nevyhnutné. Škoda. Z môjho pohľadu premárnená príležitosť urobiť lepší jazyk.

       

Hodnocení: 50 %

        špatnédobré        

Obrázky

Zlý vtip menom async v djangu (pythone), obrázek 1 Zlý vtip menom async v djangu (pythone), obrázek 2 Zlý vtip menom async v djangu (pythone), obrázek 3 Zlý vtip menom async v djangu (pythone), obrázek 4 Zlý vtip menom async v djangu (pythone), obrázek 5

Tiskni Sdílej: Linkuj Jaggni to Vybrali.sme.sk Google Del.icio.us Facebook

Komentáře

Nástroje: Začni sledovat (0) ?Zašle upozornění na váš email při vložení nového komentáře. , Tisk

Vložit další komentář

mirec avatar včera 15:39 mirec | skóre: 32 | blog: mirecove_dristy | Poprad
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Odpovědět | Sbalit | Link | Blokovat | Admin
Příloha:

Keďže sa k blogu nedá priložiť súbor prikladám tu. Veľa sa tu na abclinuxu od mojej poslednej návštevy nezmenilo. Akurát ja som o dosť starší, šedivejší a bývam s 10 mačkami v dome.

LinuxOS.sk | USE="-fotak -zbytocnosti -farebne_lcd +vydrz +odolnost +java" emerge telefon
včera 18:02 Want
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)

Veľa sa tu na abclinuxu od mojej poslednej návštevy nezmenilo. Akurát ja som o dosť starší, šedivejší a bývam s 10 mačkami v dome.

To je teda ale smutný příběh.

mirec avatar včera 18:17 mirec | skóre: 32 | blog: mirecove_dristy | Poprad
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)

Smutný ani nie, ale 10 mačiek je dobrý začiatok konverzácie :P Sám nie som, mám partnerku, ktorá má rada mačky, veľa cestujem, mám catsittera kým som preč, veľa koníčkov, aktivít. Škoda akurát, že dni nemajú viac hodín.

LinuxOS.sk | USE="-fotak -zbytocnosti -farebne_lcd +vydrz +odolnost +java" emerge telefon
včera 20:26 _
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
nemackej macky nemaj to rady
vlk avatar včera 20:15 vlk | skóre: 23 | blog: u_vlka
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Odpovědět | Sbalit | Link | Blokovat | Admin
Ja som vobec neprisiel na chut async-u. Davam prednost prehladnosti, jednoduchosti a dobremu navrhu pred nejakymi sialenstvami. Ak potrebujem nieco naozaj paralelne (ale akoze naozaj - ze inak sa neda) tak mam stale v zalohe multiprocessing.Process a vynimocne este Thread. A obcas mi padne do vyuzitia yield a tam to konci. Ale mozno ta nechut do async asi bude aj tym sedivenim (a to nemam ziadne macky!)
You don't exist, Go away !
včera 20:21 RealJ | skóre: 8
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Zajimave, ja to mam presne naopak.
včera 20:20 RealJ | skóre: 8
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Odpovědět | Sbalit | Link | Blokovat | Admin
Ja ti nevim, kdyz neco pisu (jako amater), tak to vzdy pisu pro async. Psat v dobe 100+ core CPU a ruznych clustru neco synchronne mi prijde jako nevyuziti dostupneho vykonu. Delam hodne s daty v ruznych formatech (jako amater) a nezpracovavat je asynchronne by me zabilo. K tomu blogpostu - nevim co mas za db ale mozna narazis na db limits.
včera 20:23 mimi.vx | skóre: 37 | blog: Mimi.VX | Praha
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)

az na to ze async nema s vyuzitim cpu cores moc spolecneho ...

 

na tyto strandy jsou multiprocess a multithread moduly + concurrent.

USE="-gnome -kde";turris
včera 20:33 RealJ | skóre: 8
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Takze async neresi “overlaping overlaping waits on single thread”? A jak chces bez async vytizit 100gbit sitovku nebo rychly storage? Ja chapu, ze async resi IO ale to je jaksi provazane s multicpu systemy.
vlk avatar včera 20:52 vlk | skóre: 23 | blog: u_vlka
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
spracovanie dat zo 100gbit siete nebudem riesit v pythone
You don't exist, Go away !
mirec avatar včera 20:37 mirec | skóre: 32 | blog: mirecove_dristy | Poprad
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)

Async kód využíva kooperatívny multitasking. Niečo, čo vzniklo v dobách jednoprocesorových strojov. Spoliehať sa len na async znamená využívať jediné jadro. Ak sa bavíme o pythone tak správnou cestou je multiprocessing, alebo subinterpretery alebo novy no-GIL. Inak sa bude striedavo využívať jediné jadro.

Správnym využitím async je v prípadoch, keď sa čaká na IO, ale aj vtedy môže byť rozumnejšie kombinovať multiprocessing s asyncom a nemať jeden veľký loop pri veľkom množstve spojení s väčšou priepustnosťou.

Sám by som rád písal čistý async kód, ale knižnice, ktoré používam nie sú prepísané do async a nik ich do async celkom neche prepísať, lebo by bolo potrebné prepísať komplet kód, ktorý ich využíva a vlastne musel by sa infikovať celý ekosystém.

Na limity databázy nenarážam. Používam PostgreSQL 17 s nastavenými max 100 connections.

LinuxOS.sk | USE="-fotak -zbytocnosti -farebne_lcd +vydrz +odolnost +java" emerge telefon
vlk avatar včera 20:49 vlk | skóre: 23 | blog: u_vlka
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
zabudni na 100+ core, async bezi na jedinom vlakne, navyse to ma aj nejaky overhead ked prepina medzi ulohami ktore spracovava asynchronne. s obycajnym selectom dosiahnes lepsie vysledky..
You don't exist, Go away !
včera 23:00 jbv
Rozbalit Rozbalit vše Re: Zlý vtip menom async v djangu (pythone)
Odpovědět | Sbalit | Link | Blokovat | Admin
Krasny doklad toho, ze stejne jako AI ani blbci neumej programovat :-)

Založit nové vláknoNahoru

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.