曦远 (DealiAxy)一份私人的文字刊物

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中间件之实现Admin后台IP白名单

前言 Django自带的Admin管理后台很方便,但是实际生产环境真的会有挺多安全问题的,在admin的安全防护这方面,我之前就研究实现了给admin加上登录验证码和限流功能,可以参考这篇文章:给Django Admin添加验证码和多次登录尝试限制 不过就在内部使用的话就好很多,因此我们可以通过Django的中间件来实现IP过滤,所以本文中涉及到两个知识点,一个是Django的中间件,另外一个是P

2022-08-26 · Django · 约 1 分钟

Django-Import-Export插件关于外键的处理

前言 Django-Import-Export是一款很好用很方便的Django数据导出导入插件,可以和DjangoAdmin管理后台完美集成,只需要少量的代码配置即可方便实现你要的多种格式导出导入,关于这个插件的使用更多可以看我之前的文章:这里放文章链接 之前我在使用中都是专门做了一个原始数据的表来存导入的数据,然后再对原始数据表做一些数据处理,把数据存到其他表才能真正使用这些数据。(不是很好的做

2022-04-02 · Django · 约 1 分钟

讨论

共 0 条

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

加载评论中…