返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Django

Performance Research on Different Django Servers and Deployment Methods

WSGI is top-ranked? uWSGI is the real deal. ASGI adds complexity and performs significantly worse. Tencent Cloud servers even suffer from downtime? Enough said.

曦远 (DealiAxy)约 34 分钟
目录19 个章节

Preface

Recently, I've spent a lot of time tinkering with DjangoStarter.

Last time, I mentioned switching the server to Granian, developed in Rust.

Although this allowed me to remove the Nginx container, making things more convenient, the performance was still not entirely satisfactory.

So, I decided to try out most of the available servers one by one while optimizing the code.

After these past few days, investing a significant amount of time in environment configuration, code modifications, and performance testing, I've finally summarized the performance of the DjangoStarter framework with different servers.

PS: The current popularity of AI has rendered most introductory technical articles less meaningful. Nowadays, I mostly write articles as notes and try to avoid writing overly basic introductions, as they don't add much value. 😅

The less appealing parts for most readers, like code and detailed data, are placed below.

Test Environment

The project for this test was deployed on a Tencent Cloud server with 2 cores and 2GB RAM (a small, low-performance server). It feels like the server's performance seriously drags down the application's performance, haha 🤣

I also noticed something: Tencent Cloud seems to secretly reduce server performance during peak hours (like afternoons). The results from afternoon tests differed significantly from those during early morning hours.

The WSGI/ASGI servers participating in this test include daphne, granian, gunicorn, uwsgi, uvicorn, and hypercorn, which basically cover most of the servers in the Python ecosystem.

This time, wrk was used for performance testing. All interfaces were tested with the parameters -t4 -c200 -d30s.

Performance Test Conclusions

RPS Rankings

RankServerReq/secInterface
🥇 1uWSGI + WSGI1206WSGI
🥈 2Gunicorn + WSGI740WSGI
🥉 3Granian + ASGI220ASGI
4Daphne + ASGI190ASGI
5Uvicorn + ASGI181ASGI
6Hypercorn + ASGI168ASGI

Conclusion:

  • WSGI is significantly faster overall than ASGI

    This is an expected result, as Django's internal architecture is natively WSGI, and synchronous routing does not require additional async conversion.

  • uWSGI once again proves itself to be the king of WSGI performance

    uWSGI's C implementation, mature worker management, and optimized memory utilization make it unbeatable in pure WSGI scenarios.

Memory Usage Comparison

RankserverMemory
🥇 1uWSGI58M (extremely low)
2Granian170M
3Daphne175M
4Hypercorn300M
5Gunicorn+WSGI430M
6Uvicorn500M (highest)

Conclusion:

  • uWSGI is not only the fastest but also the most memory-efficient.
  • Uvicorn has the highest memory usage (the overhead of a single Python worker is significant).
  • It is normal for ASGI servers to generally have higher memory usage.

Performance Test Data

Below are the detailed performance test data.

daphne + asgi

Memory peak usage around 175M during testing.

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.03s   134.59ms   1.99s    89.71%
    Req/Sec    69.17     64.53   313.00     80.49%
  5737 requests in 30.04s, 2.14MB read
  Socket errors: connect 0, read 0, write 0, timeout 4
Requests/sec:    190.99
Transfer/sec:     72.93KB

granian + asgi

During testing, the peak memory usage is approximately 170M.

$ curl http://127.0.0wrk -t4 -c200 -d30s875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   886.48ms   79.27ms   1.26s    85.40%
    Req/Sec    86.07    112.58   490.00     84.05%
  6637 requests in 30.04s, 2.72MB read
Requests/sec:    220.93
Transfer/sec:     92.56KB

gunicorn + wsgi

Peak memory usage during testing was around 430MB.

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   268.67ms   31.82ms 425.23ms   86.13%
    Req/Sec   187.47    119.54   494.00     56.66%
  22244 requests in 30.03s, 9.52MB read
Requests/sec:    740.60
Transfer/sec:    324.74KB

uwsgi + wsgi

During testing, peak memory usage was approximately 58MB.

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   199.52ms  277.85ms   1.97s    87.56%
    Req/Sec   302.89     88.84   575.00     67.33%
  36210 requests in 30.02s, 12.95MB read
  Socket errors: connect 0, read 36216, write 0, timeout 96
Requests/sec:   1206.08
Transfer/sec:    441.68KB

uvicorn + asgi

During testing, peak memory usage was around 500M.

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.00s   537.23ms   2.00s    59.81%
    Req/Sec    62.04     66.05   460.00     83.03%
  5440 requests in 30.04s, 2.23MB read
  Socket errors: connect 0, read 0, write 0, timeout 369
Requests/sec:    181.07
Transfer/sec:     76.05KB

hypercorn + asgi

During testing, the peak memory usage was around 300MB.

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/django-starter/monitoring/health
Running 30s test @ http://127.0.0.1:9875/api/django-starter/monitoring/health
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   734.64ms  423.05ms   2.00s    51.70%
    Req/Sec    55.53     52.50   360.00     83.49%
  5064 requests in 30.06s, 2.09MB read
  Socket errors: connect 0, read 0, write 0, timeout 1124
Requests/sec:    168.45
Transfer/sec:     71.24KB

More Refined Performance Test

Accessing an interface with database query: http://127.0.0.1:9878/api/demo/movie/movie

Direct uwsgi

$ wrk -t4 -c200 -d30s http://127.0.0.1:9875/api/demo/movie/movie
Running 30s test @ http://127.0.0.1:9875/api/demo/movie/movie
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   501.93ms  260.82ms   1.99s    94.20%
    Req/Sec    61.02     39.11   191.00     62.02%
  6726 requests in 30.04s, 15.84MB read
  Socket errors: connect 0, read 6758, write 0, timeout 45
Requests/sec:    223.89
Transfer/sec:    539.82KB

After caddy reverse proxy

$ wrk -t4 -c200 -d30s http://127.0.0.1:9878/api/demo/movie/movie
Running 30s test @ http://127.0.0.1:9878/api/demo/movie/movie
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   562.46ms  166.56ms   1.65s    88.77%
    Req/Sec    52.66     21.07   150.00     65.63%
  6299 requests in 30.04s, 15.06MB read
  Socket errors: connect 0, read 0, write 0, timeout 208
  Non-2xx or 3xx responses: 40
Requests/sec:    209.69
Transfer/sec:    513.30KB

Code

Let's introduce the code section.

The main items used for performance testing this time are the health check and a simple database access API. All APIs are developed using django-ninja.

Database Access Interface

This is the interface involved in database operations.

Query data return with pagination functionality, very straightforward.

@router.get('/movie', response=List[MovieOut], url_name='demo/movie/list')
@paginate
def list_items(request):
    qs = Movie.objects.all()
    return qs

Health Check Interface

Import these packages

import asyncio
from ninja import Router
from django.db import connections
from django.db.utils import OperationalError
from redis import Redis
from redis.exceptions import RedisError
from redis import asyncio as aioredis
import os
import time
import platform
import subprocess
import anyio

The original interface was purely synchronous, and it also called a system process to obtain uptime, which severely slowed down the entire interface. This time, I have refactored it into both synchronous and asynchronous interfaces.

First is the simplest version, which does nothing and simply returns JSON.

@router.get('health')
def simple_health_check(request):
    """Health check endpoint for container health checks and monitoring"""
    response_data = {
        'status': 'healthy',
        'status_code': 200,
    }

    return response_data

Asynchronous Interface

Let's start with the asynchronous interface.

Actually, I rarely use Python's asynchronous features, which might be related to Python's relatively weak support for asynchronicity.

First, let's write two asynchronous methods to check database and Redis connections.

async def check_db_async():
    """Database check (synchronous ORM → run in thread pool)"""
    try:
        def _check():
            for conn in connections.all():
                conn.cursor()
            return True

        return await anyio.to_thread.run_sync(_check)
    except OperationalError:
        return False


async def check_redis_async():
    """Asynchronous Redis check (Note: Ensure client is closed to release connection pool)"""
    try:
        redis_client = aioredis.Redis(
            host="redis",
            port=6379,
            socket_connect_timeout=1,
            decode_responses=True,
        )
        try:
            ok = await redis_client.ping()
            return bool(ok)
        finally:
            # Close client and connection pool to avoid connection leaks (important in short-lived containers/requests)
            try:
                await redis_client.close()
            except Exception:
                pass
            try:
                await redis_client.connection_pool.disconnect()
            except Exception:
                pass
    except Exception:
        return False

Django ORM does not support asynchronous operations, so here we use the anyio package to simplify things by wrapping synchronous operations to run asynchronously. You could also do this with the asyncio library, but anyio is more convenient.

When using aioredis, there's a pitfall I encountered. I initially installed the aioredis package, only to find it conflicted with the existing redis package.

After checking the documentation, I realized that from redis import asyncio as aioredis is all you need—the redis package already includes built-in asynchronous support.

Now, let's implement the asynchronous interface.

@router.get('health/async')
async def health_check_async(request):
    """Async mode health check (ASGI optimized version)"""
    # Use asyncio.gather to concurrently run DB and Redis checks
    db_ok, redis_ok = await asyncio.gather(
        check_db_async(),
        check_redis_async(),
    )

    status = "healthy" if db_ok and redis_ok else "unhealthy"
    status_code = 200 if status == "healthy" else 503

    return {
        "status": status,
        "status_code": status_code,
        "checks": {
            "database": "ok" if db_ok else "error",
            "redis": "ok" if redis_ok else "error",
        },
        "system": get_system_info(),
    }

Synchronous Interface

It's simpler with synchronous

@router.get('health/sync')
def health_check_sync(request):
    """Synchronous version of the health check interface"""

    # --- Check Database & Redis ---
    db_ok = check_db_sync()
    redis_ok = check_redis_sync()

    # --- System Information ---
    system_info = {
        "timestamp": time.time(),
        "uptime": get_uptime(),
        "hostname": os.environ.get("HOSTNAME", ""),
        "environment": os.environ.get("ENVIRONMENT", "development"),
        "os": platform.system(),
    }

    # --- Status Code ---
    status = "healthy" if db_ok and redis_ok else "unhealthy"
    status_code = 200 if status == "healthy" else 503

    # --- Return Data ---
    return {
        "status": status,
        "status_code": status_code,
        "checks": {
            "database": "ok" if db_ok else "error",
            "redis": "ok" if redis_ok else "error",
        },
        "system": system_info,
    }

Summary

For traditional applications, uWSGI is the straightforward choice.

For those needing asynchronous capabilities, you can split out the async parts and run them using granian or daphne.

Of course, this also increases complexity, but it's the optimal trade-off for performance optimization.

That's it for now. Actually, this performance investigation taught me a lot, and I'll continue analyzing my takeaways in the next article.

_EOF_

Performance Research on Different Django Servers and Deployment Methods

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

Django中「强行」使用MVVM设计模式

扫盲 首先带大家了解一下什么是MVVM模式: 什么是MVVM?MVVM是Model-View-ViewModel的缩写。 MVVM是MVC的增强版,实质上和MVC没有本质区别,只是代码的位置变动而已 从名字上看,MVVM比MVC架构中多了一个ViewModel,没错,就是这个ViewModel,他是MVVM相对于MVC改进的核心思想。在开发过程中,由于需求的变更或添加,项目的复杂度越来越高,代码量

2021-12-18 · Django · 约 1 分钟

Django应用部署:Docker容器化实践

在此之前,我一直用uwsgi+virtualenv+nginx方式进行应用部署,操作起来比较麻烦,而且依赖于服务器上的Python版本,服务的管理方面单纯uwsgi + pid算不上特别麻烦但总没有docker compose方便,无奈原本的服务器是32位系统的用不上docker,现在又增加了一个新服务器,所以赶紧把docker安排上~ 不得不说真的太简单太方便了,全程傻瓜式操作,也基本没像之前u

2021-12-18 · Django · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…