Paradigma Concurrency: Event Loop (ASGI) vs Pre-fork Worker (WSGI)
Memilih runtime Django antara WSGI dan ASGI berakar pada model penanganan I/O di tingkat kernel sistem operasi. WSGI standar (seperti Gunicorn dengan sync worker) menggunakan model pre-fork process per-request. Setiap proses worker hanya memproses satu request HTTP secara eksklusif hingga response selesai dikirim.
Ketika aplikasi melakukan blocking I/O (misalnya query database lambat atau HTTP call eksternal), worker tersebut tertahan (blocked) dan tidak dapat melayani koneksi lain. Opsi WSGI gthread memperbaiki masalah ini dengan membagi worker menjadi thread pool:
gunicorn config.wsgi:application \
--workers 4 \
--worker-class gthread \
--threads 8 \
--bind 0.0.0.0:8000Model gthread memanfaatkan kernel OS scheduler untuk beralih konteks (context switching) saat sebuah thread mengalami blocking I/O. Namun, thread kernel memiliki memory overhead (alokasi stack memori per thread) dan batasan skalabilitas konkurensi (ribuan thread akan mendegradasi performa CPU akibat context switching).
Sebaliknya, ASGI (Asynchronous Server Gateway Interface) dijalankan di atas event loop berbasis asyncio (sering kali dipercepat dengan C-extension seperti uvloop pada Uvicorn):
gunicorn config.asgi:application \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000Model ini menggunakan non-blocking sockets dengan multiplexing (epoll/kqueue). Satu worker process dapat mempertahankan puluhan ribu koneksi terbuka (WebSockets, Server-Sent Events, streaming I/O) dengan footprint memori sangat rendah, selama eksekusi di dalam loop bersifat non-blocking secara kooperatif (menggunakan await).
Dilema Django ORM: sync_to_async dan Overhead Thread Pool
Django secara bertahap mengadopsi interface asynchronous (seperti Model.objects.aget(), QuerySet.aiter()). Namun, mesin internal database adapter Python (seperti psycopg2 standar, konektor MySQL) dan arsitektur inti ORM Django dibangun di atas operasi sinkron dan thread-local storage.
Untuk menjaga safety dan menghindari SynchronousOnlyOperation error, Django membungkus pemanggilan sinkron ke dalam thread terpisah menggunakan utilitas asgiref.sync.sync_to_async. Implementasi ini mendelegasikan eksekusi query ke ThreadPoolExecutor:
from django.http import JsonResponse
from asgiref.sync import sync_to_async
from .models import Invoice
# Anti-pattern: Blocking di dalam async view
async def bad_async_view(request):
# Ini akan melempar SynchronousOnlyOperation
invoices = Invoice.objects.filter(status='pending')[:10]
return JsonResponse({'count': len(invoices)})
# Pola yang benar pada ASGI
async def good_async_view(request):
# Native async iteration atau eksekusi via sync_to_async
invoices = []
async for item in Invoice.objects.filter(status='pending')[:10]:
invoices.append(item.id)
return JsonResponse({'invoice_ids': invoices})Ketika Anda menjalankan async view yang melakukan pemanggilan ORM, runtime melakukan context handoff: thread event loop utama menyerahkan task ke thread pool, menunggu thread pool menyelesaikan pembacaan I/O soket database, lalu mengembalikan kontrol ke event loop.
Peringatan Teknis: Jika endpoint Anda didominasi oleh query database relasional tradisional (CRUD), beralih ke ASGI sering kali justru meningkatkan latensi (P95/P99) akibat overhead context switching antara asyncio event loop dan thread pool asgiref.Efisiensi Infrastruktur Cloud dan Sizing Server
Perbedaan model runtime secara langsung mempengaruhi kebutuhan kapasitas node (vCPU dan RAM) pada infrastructure provider:
- Footprint Memori (RAM): Model Gunicorn sync worker memakan 100MB-250MB RAM per worker process tergantung ukuran dependensi aplikasi. Jika server memiliki 16 worker, baseline RAM yang terkonsumsi berkisar antara 1.6GB hingga 4GB hanya untuk worker idle. Gunicorn
gthreadmemangkas alokasi ini secara signifikan karena thread berbagi memory space dari process parent yang sama. ASGI worker memiliki footprint memori per-koneksi yang paling rendah; ribuan idle connections hanya menambah konsumsi memori seukuran struktur data state event loop. - Karakteristik vCPU: Pada WSGI, CPU terpakai efisien untuk beban sinkron CPU-bound, tetapi vCPU terbuang menunggu database response pada I/O blocking. Pada ASGI, satu thread event loop dapat memanfaatkan 100% dari 1 vCPU secara optimal untuk I/O orchestration, namun satu operasi CPU-bound yang intensif (seperti pemrosesan gambar atau JSON parsing raksasa) akan mem-blokir seluruh event loop untuk semua koneksi lain di worker tersebut.
Maintainability: Async Poisoning, Package Third-Party, dan Debugging
Dampak arsitektural terbesar beralih ke ASGI bukan pada performa puncak, melainkan pada cognitive load tim rekayasa perangkat lunak:
- Async Poisoning (Function Coloring): Sekali Anda mendeklarasikan fungsi sebagai
async, semua caller di atasnya harus menangani fungsi tersebut denganawaitatau membungkusnya secara eksplisit denganasync_to_sync. Hal ini merambat ke middleware, sinyal (Django signals), dan custom validation logic. - Kompatibilitas Package Ekosistem: Banyak package populer di ekosistem Django (seperti client APM tertentu, library payment gateway yang memakai
requests, atau caching backend) masih menggunakan blocking I/O. Jika dependensi ini dipanggil di dalam async view tanpa dibungkussync_to_async(thread_sensitive=True), library tersebut akan membekukan (freeze) event loop utama secara senyap tanpa menghasilkan exception log yang jelas. - Traceback dan Debugging: Traceback pada ASGI melintasi stack internal asyncio scheduler dan asgiref context wrappers, menyulitkan identifikasi root-cause pada production monitoring (seperti Sentry) dibandingkan dengan clean linear traceback dari WSGI call stack.
Matriks Keputusan Arsitektur
Gunakan matriks perbandingan berikut untuk menentukan runtime yang sesuai dengan karakteristik beban kerja sistem:
| Kriteria | WSGI Sync | WSGI (gthread) | ASGI (Uvicorn) |
|---|---|---|---|
| Protokol Web | HTTP/1.1 saja | HTTP/1.1 saja | HTTP/1.1, WebSockets, SSE, HTTP/2 |
| Profil Beban Kerja | CRUD Database standar, CPU-bound | I/O moderat, DB queries, API calls | High-concurrency long-lived connections, SSE, external microservice orchestration |
| Toleransi Blocking Code | Tinggi (hanya mengisolasi 1 process) | Moderat (hanya mengisolasi 1 thread) | Nol (mematikan seluruh event loop pada worker) |
| Traceback Complexity | Rendah (Linear) | Rendah (Thread stack) | Tinggi (Async task chaining) |
| Overhead Django ORM | Nol (Native path) | Nol (Native path) | Ada context handoff via thread pool |
Panduan Implementasi: Kapan Bertahan vs Kapan Migrasi
Bertahan di WSGI (Gunicorn + gthread):
- Aplikasi adalah sistem monolitik tradisional berbasis relational database (PostgreSQL, MySQL).
- Interaksi antar-komponen dominan berupa blocking SQL queries yang dioptimasi.
- Tim belum terbiasa dengan prinsip dasar cooperative multitasking, coroutine cancellation, dan thread safety.
Migrasi ke ASGI (Gunicorn + UvicornWorker):
- Aplikasi memerlukan WebSockets real-time (misalnya via Django Channels) atau streaming response langsung dari server ke browser.
- View didominasi oleh non-database I/O, seperti agregasi response dari puluhan microservice via
httpx.AsyncClientsecara paralel. - Aplikasi bertindak sebagai webhook receiver dengan concurrency sangat tinggi dan latensi processing rendah.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!