Haruskah Anda menggunakan Django Asynchronous Support?

Nov 28 2022
Seperti saya, Anda mungkin telah mempertimbangkan untuk menggunakan Django jika Anda menginginkan kerangka kerja berfitur lengkap untuk pengembangan web yang cepat. Sayangnya, Django tidak dirayakan untuk kinerja — mungkin karena layanan Sinkron (Sinkron) bawaannya.
Seekor python menangani banyak buah persik pada saat yang bersamaan! (sumber: https://labs.openai.com/)

Seperti saya, Anda mungkin telah mempertimbangkan untuk menggunakan Django jika Anda menginginkan kerangka kerja berfitur lengkap untuk pengembangan web yang cepat.

Sayangnya, Django tidak dirayakan untuk kinerja — mungkin karena layanan Sinkron (Sinkron) bawaannya. Inilah pengantar berjalan melalui (https://docs.djangoproject.com/en/4.1/intro/tutorial01/).

Sinkronisasi berkinerja buruk karena utas server hanya dapat melayani satu permintaan dalam satu waktu. Yang penting, permintaan I/O memblokir utas. Throughput dapat ditingkatkan dengan menambahkan utas, tetapi utas server Python mengambil jumlah RAM yang hampir sama dengan proses server Python. Ini membuatnya tidak layak untuk meningkatkan throughput dengan menambahkan utas.

Desain asinkron (Async) telah ditambahkan di Django 3.0 (https://docs.djangoproject.com/en/3.0/topics/async/), dan tidak disebutkan dalam pendahuluan. Disebutkan sebagai topik terakhir dalam menggunakan Django (https://docs.djangoproject.com/en/4.1/topics/).

Async dapat mengungguli Sinkronisasi karena satu utas server Async dapat melayani banyak permintaan sekaligus. Selama permintaan I/O yang ditunggu, kontrol hasil tunggu kembali ke utas dan mengizinkan permintaan lain untuk dilanjutkan. Ini mirip dengan loop acara Javascript, tetapi tidak cukup (https://stackoverflow.com/questions/68139555/difference-between-async-await-in-python-vs-javascript).

Sehingga menimbulkan pertanyaan:

  • Berapa banyak perolehan kinerja yang dapat diperoleh Django dengan menggunakan penyiapan Asinkron?
  • Apakah penting apa yang dilakukan rute/titik akhir (berat-CPU vs berat-IO)?
  • Bagaimana jika latensi rendah (server dekat) atau latensi tinggi (server jauh)?

Jadi saya menjalankan percobaan uji beban. Anda dapat menemukan kode dan detailnya di sinihttps://github.com/nalkhish/Asynchronize-Django. Secara singkat, inilah yang saya selidiki:

For each setup (sync, async limited, async unlimited):
    For each request route (high cpu, high io create, high io read):
        For each added latency (200, 100, 0):
            Emulate load and generate report

  • Untuk Sinkronisasi, Gunicorn digunakan untuk memutar 1 pekerja Sinkronisasi (https://docs.gunicorn.org/en/stable/design.html).
  • Untuk Async Limited, Gunicorn digunakan untuk memutar 1 pekerja Uvicorn (https://www.uvicorn.org/deployment/)
  • Untuk Async Unlimited, Gunicorn digunakan untuk memutar 1 pekerja Uvicorn (https://www.uvicorn.org/deployment/), tetapi postgres diatur untuk memungkinkan 1000 koneksi simultan (lebih dari 100 default).

3 rute digunakan:

  • cpu — menunjukkan bahwa rutenya berat cpu
  • def cpu(request: HttpRequest):
        """High cpu route
        CPU: High
        I/O: low
        """
        ct = 0
        for i in range(10**7):
            ct += i * i
        return JsonResponse({"ct": ct})
    

    # In sync
    def io_create(request: HttpRequest):
        """Create 1 post
        CPU: low
        I/O: High
        """
        new_post = Post.objects.create(
            title=request.POST["title"],
            content=request.POST["content"],
        )
        return JsonResponse(new_post.to_dict())
    
    # In async
    async def io_create(request: HttpRequest):
        """Create 1 post
        CPU: low
        I/O: High
        """
        new_post = await Post.objects.acreate(
            title=request.POST["title"],
            content=request.POST["content"],
        )
        return JsonResponse(new_post.to_dict())
    

    # In sync
    def io_read(request: HttpRequest):
        """Get first 25 posts
        CPU: low
        I/O: High
        """
        posts = Post.objects.all()[:25]
        ct = Post.objects.count()
        return JsonResponse(
            {
                "ct": ct,
                "posts": [post.to_dict() for post in posts],
            }
        )
    
    # In Async
    async def io_read(request: HttpRequest):
        """Get first 25 posts
        CPU: low
        I/O: High
        """
        posts = []
        async for p in Post.objects.all()[:25]:
            posts.append(p)
        ct = await Post.objects.acount()
        return JsonResponse(
            {
                "ct": ct,
                "posts": [post.to_dict() for post in posts],
            }
        )
    

docker exec <container_name> tc qdisc add dev eth0 root netem delay 100ms

Di mana <container_name> adalah nama wadah

Muat Emulasi

Muatan ditiru menggunakan alat sumber terbuka Locust (https://locust.io/). Belalang membombardir pelabuhan dengan permintaan. Segera setelah 'pengguna' menyelesaikan tugasnya, itu dimulai lagi.

Beban total berlangsung 3 menit:

  • Dimulai dengan 0 pengguna.
  • Untuk 100 detik pertama, tingkatkan dengan kecepatan 5 pengguna per detik untuk membatasi 500 pengguna bersamaan.
  • Selama 80 detik terakhir, pengguna dipertahankan pada 500.

Temuan:

Definisi:

  • Max sRPS: Permintaan berhasil maksimum per detik diperoleh dengan mengambil perbedaan maksimum antara keseluruhan permintaan dan permintaan gagal untuk seluruh grafik laporan (lihat angka tambahan di repo ).
  • Keberhasilan total: Permintaan yang berhasil secara keseluruhan diperoleh dengan mengurangkan #permintaan gagal dari #total permintaan (lihat angka tambahan di repo ).

Tabel 1: Terikat CPU. Max sRPS & Keberhasilan keseluruhan untuk kombinasi penyiapan (Sinkronisasi/Async Terbatas/Async tidak terbatas) dan Latensi (0 md, 100 md, 200 md). Async sebenarnya bekerja dengan buruk.

Async terbatas vs Async tidak terbatas:

  • Mengapa Async unlimited lebih baik daripada Async limited? Async unlimited memungkinkan lebih banyak koneksi database, tetapi rute CPU dan pengaturan middleware tidak menggunakan database. Ini perlu diselidiki lebih lanjut.
  • Bagaimanapun, kedua pengaturan Async memiliki dinamika yang tidak dapat diprediksi (lihat gambar tambahan di repohttps://github.com/nalkhish/Asynchronize-Django).
  • Sinkronisasi memiliki sRPS maks yang lebih rendah daripada Async tanpa batas. Ini mungkin karena server async dapat menangani banyak permintaan sekaligus sehingga beberapa permintaan kebetulan selesai pada waktu yang bersamaan. Ini mengejutkan karena asyncio seharusnya tidak mengganti konteks kecuali jika mengenai pernyataan menunggu, yang tidak ada di rute cpu. Ini perlu diselidiki lebih lanjut.
  • Sinkronisasi memiliki dinamika yang dapat diprediksi dan keberhasilan keseluruhan yang lebih tinggi daripada Async. Ini cukup untuk menjamin penggunaan Sync untuk layanan terikat cpu.

Tabel 2: Pembacaan DB yang terikat I/O. Max sRPS & Keberhasilan keseluruhan untuk kombinasi Server (Async/Sync) dan Latensi (0ms, 100ms, 200ms). Ada throughput yang lebih tinggi pada latensi tinggi saat menggunakan Async daripada Sinkronisasi.

Tabel 3: Pembuatan DB yang terikat I/O. Max sRPS & Keberhasilan keseluruhan untuk kombinasi Server (Async/Sync) dan Latensi (0ms, 100ms, 200ms). Ada throughput yang lebih tinggi pada latensi tinggi saat menggunakan Async.

Async terbatas vs Async tidak terbatas: Async tidak terbatas memiliki sRPS maks yang lebih tinggi dan kesuksesan keseluruhan daripada Async terbatas.

  • Untuk rute baca yang terikat IO, ini kemungkinan dapat dikaitkan dengan database yang menjadi hambatan karena gagal.
  • Untuk rute pembuatan terikat IO, ini perlu diselidiki lebih lanjut karena database tidak gagal untuk Async terbatas (lihat gambar tambahan)
  • Untuk rute io_read dan io_create, Sync memiliki kinerja yang jauh lebih rendah daripada Async (untuk latensi tambahan 200 md, perbedaan dalam throughput keseluruhan adalah 40 kali lipat untuk io_read dan 230 kali lipat untuk io_create).
  • Ini mungkin karena thread pekerja server sedang menunggu permintaan database selesai sebelum dapat menangani permintaan berikutnya. Teori ini didukung oleh hubungan terbalik antara latensi dan sRPS maks dan kesuksesan keseluruhan untuk Sinkronisasi.

Keterbatasan dieksplorasi lebih lanjut di repo readmehttps://github.com/nalkhish/Asynchronize-Django(sedikit informasi yang dibahas di sini, tidak adanya logika coba lagi, dan tidak adanya latensi masuk), tetapi penelitian ini cukup mengeksplorasi kemampuan layanan Django asinkron dan sinkron.

Jika Anda menggunakan Django dan memiliki muatan terikat CPU, gunakan Sync. Jika tidak, jika beban terikat I/O, gunakan Async. Kemungkinan akan lebih dari 10x throughput pekerja Anda.