前言
最近我花了很多时间折腾 DjangoStarter。
上次说到把服务器换成了 Rust 开发的 Granian
虽然是把 Nginx 容器去掉了,方便了,但性能还是不太满意。
索性对大部分能用的服务器来拿来挨个尝试,同时对代码进行优化。
经过这几天,花费大量时间进行环境配置、代码修改、性能测试,最后我总结出 DjangoStarter 框架搭配不同的服务器的性能表现。
PS:现在 AI 的流行让大部分介绍基础的技术文章失去了意义,我现在写文章也基本是当成笔记在用了,而且也尽量避免写很水的基础介绍,意义不大😅
大家不爱看的代码和详细数据部分我放在底下。
测试环境
本次测试的项目部署在腾讯云2C2G的小水管服务器上,感觉服务器性能严重拖累了应用性能哈哈🤣
而且我还发现一个事情,腾讯云似乎偷偷摸摸在高峰期(如下午)把服务器性能降低,下午和凌晨测试的结果有很大差异。
本次参与测试的wsgi/asgi服务器有 daphne, granian, gunicorn, uwsgi, uvicorn, hypercorn 基本涵盖了 python 生态的大部分服务器了~
本次使用 wrk 作为性能测试功能,所有接口都使用 -t4 -c200 -d30s 的测试参数。
性能测试结论
RPS 排名
| 排名 | server | req/sec | 接口 |
|---|---|---|---|
| 🥇 1 | uWSGI + WSGI | 1206 | WSGI |
| 🥈 2 | Gunicorn + WSGI | 740 | WSGI |
| 🥉 3 | Granian + ASGI | 220 | ASGI |
| 4 | Daphne + ASGI | 190 | ASGI |
| 5 | Uvicorn + ASGI | 181 | ASGI |
| 6 | Hypercorn + ASGI | 168 | ASGI |
结论:
WSGI 整体远快于 ASGI
这是预期结果,Django 的内部原生就是 WSGI,同步路由不需要额外的 async 转换。
uWSGI 再次证明自己是 WSGI 性能之王
uWSGI 的 C 实现、成熟的 worker 管理、内存利用优化都让它在纯 WSGI 场景无敌。
内存占用对比
| 排名 | server | 内存 |
|---|---|---|
| 🥇 1 | uWSGI | 58M(极低) |
| 2 | Granian | 170M |
| 3 | Daphne | 175M |
| 4 | Hypercorn | 300M |
| 5 | Gunicorn+WSGI | 430M |
| 6 | Uvicorn | 500M(最高) |
结论:
- uWSGI 不仅最快,还最省内存
- Uvicorn 内存最高(Python 单 worker overhead 明显)
- ASGI 服务器普遍内存偏高是正常现象
性能测试数据
以下是详细的性能测试数据。
daphne + asgi
测试时内存峰值占用175M左右
$ 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
测试时内存峰值占用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
测试时内存峰值占用430M左右
$ 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
测试时内存峰值占用58M左右
$ 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
测试时内存峰值占用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
测试时内存峰值占用300M左右
$ 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
更细化的性能测试
访问一个有数据库查询的接口 http://127.0.0.1:9878/api/demo/movie/movie
直接 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
经过 caddy 反代
$ 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
代码
来介绍下代码的部分。
这次拿来进行性能测试的主要是健康检查和一个简单的数据库访问接口。所有接口都是用 django-ninja 开发的。
数据库访问接口
涉及到数据库的就是这个接口
查询数据返回+分页功能,非常简单
@router.get('/movie', response=List[MovieOut], url_name='demo/movie/list')
@paginate
def list_items(request):
qs = Movie.objects.all()
return qs
健康检查接口
引入这些package
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
原版的接口是纯同步的,还要调用一个系统进程去获取 uptime,这个操作严重拖慢了整个接口的速度,我这次重构成同步和异步两种接口。
首先是最简单的版本,这里面就啥也没有,单纯返回 JSON。
@router.get('health')
def simple_health_check(request):
"""健康检查端点,用于容器健康检查和监控"""
response_data = {
'status': 'healthy',
'status_code': 200,
}
return response_data
异步接口
先来异步接口。
其实我很少用到 python 的异步功能,可能这也和 python 对异步的支持比较差有关系。
先写两个异步方法,用来检查数据库和Redis连接。
async def check_db_async():
"""数据库检查(同步 ORM → 放线程池)"""
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():
"""Redis 异步检查(注意:确保关闭客户端以释放连接池)"""
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:
# 关闭客户端和连接池,避免连接泄露(在短生命周期的容器/请求里很重要)
try:
await redis_client.close()
except Exception:
pass
try:
await redis_client.connection_pool.disconnect()
except Exception:
pass
except Exception:
return False
Django ORM 不支持异步,所以这里用 anyio 这个包来简化一下操作,把同步操作包装一下假装成异步运行。实际上用 asyncio 这个库也能做,不过 anyio 方便一点。
在用 aioredis 时,这里有个坑,我一开始安装了 aioredis 这个包,结果发现和原有的 redis 包冲突了。
查看文档才发现 from redis import asyncio as aioredis 就完事儿了,redis 包里已经自带了异步支持。
接着实现异步接口
@router.get('health/async')
async def health_check_async(request):
"""Async 模式健康检查(ASGI 优化版)"""
# 使用 asyncio.gather 并发运行 DB 和 Redis 检查
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(),
}
同步接口
同步的就简单了
@router.get('health/sync')
def health_check_sync(request):
"""同步版本的健康检查接口"""
# --- 检查数据库 & Redis ---
db_ok = check_db_sync()
redis_ok = check_redis_sync()
# --- 系统信息 ---
system_info = {
"timestamp": time.time(),
"uptime": get_uptime(),
"hostname": os.environ.get("HOSTNAME", ""),
"environment": os.environ.get("ENVIRONMENT", "development"),
"os": platform.system(),
}
# --- 状态码 ---
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": system_info,
}
小结
传统应用无脑选 uwsgi 就对了。
需要异步的可以把异步那部分切割出来用 granian 或者 daphne 跑。
当然这也增加了复杂度,但这是对性能优化的最佳权衡。
先这样吧,其实这次性能调研还让我学到很多,下一篇文章继续分析这次的收获。
程序设计实验室
微信公众号