先确认是否真泄漏:Django进程内存持续上涨、CPU长期高于70%且无请求峰值,才是排查起点;用psutil每5秒采样RSS和cpu_percent(interval=1),监控各worker,关注QuerySet缓存、循环引用及py-spy抓帧定位。

用 psutil 实时监控 Django 进程的内存与 CPU 变化
不靠猜,先确认是不是真泄漏——Django 进程的内存持续上涨、CPU 长期高于 70% 且无明显请求峰值,才是排查起点。直接在生产环境跑 top 或 htop 容易漏掉瞬时毛刺,建议用 psutil 写个轻量脚本挂后台采样:
示例(每 5 秒记录一次):
import psutil
import time
p = psutil.Process(<code>os.getpid()</code>)
while True:
mem = p.memory_info().rss / 1024 / 1024 # MB
cpu = p.cpu_percent(interval=1)
print(f"{time.strftime('%H:%M:%S')} | MEM: {mem:.1f}MB | CPU: {cpu:.1f}%")
time.sleep(5)关键点:
- 必须用
p.cpu_percent(interval=1),传interval才能获取真实增量;不传或传 0 会返回自进程启动以来的平均值,完全失真 -
rss是实际物理内存占用,比vms更反映真实压力;注意单位换算,避免误判为“几百 GB”这种假警报 - 如果用 Gunicorn/Uvicorn,要对每个 worker 单独监控,主进程 CPU 低不代表没泄漏
查 django.db.models.query.QuerySet 缓存导致的内存堆积
Django 的 QuerySet 是惰性求值,但一旦被多次遍历、转成 list、或参与 len()/bool() 判断,就会缓存结果集。缓存不会自动释放,尤其在长生命周期的 Celery task 或管理命令中极易堆积。
立即学习“Python免费学习笔记(深入)”;
典型症状:内存曲线随请求次数阶梯式上升,重启后归零,且 gc.get_objects() 中大量出现 QuerySet 实例。
实操建议:
- 禁用不需要的缓存:在 QuerySet 后链式调用
.iterator(chunk_size=2000),强制流式读取,不缓存 - 显式清空:对已缓存的 QuerySet,调用
qs._result_cache = None(虽是私有属性,但稳定可用) - 避免在循环里重复构造相同 QuerySet:把
Model.objects.filter(...)提到循环外,或用values_list('id', flat=True)减少对象实例化开销
识别第三方库引发的循环引用(gc.garbage 不为空)
Python 的 GC 能处理大多数引用环,但遇到带 __del__ 方法的对象(如某些数据库连接池、异步库封装类),或 C 扩展中未正确管理引用计数时,gc.collect() 会失败,残留对象进入 gc.garbage ——这是内存泄漏的铁证。
快速验证方法:
import gc gc.set_debug(gc.DEBUG_SAVEALL) # 开启调试模式 gc.collect() print(len(gc.garbage)) # >0 表示存在无法回收的环
常见诱因:
-
celery+redis连接未关闭:任务函数内手动调用redis_client.connection_pool.disconnect() -
requestsSession 复用不当:全局 Session 被闭包捕获,导致 Response 对象无法释放 - 自定义中间件中持有 request/response 引用并存入全局 dict
定位技巧:打印 gc.garbage[0].__class__ 和 gc.get_referrers(gc.garbage[0]),顺藤摸瓜找谁在强引用它。
用 py-spy 抓取运行中 Django 的 CPU 热点与内存分配栈
cProfile 需要重启服务且影响性能,py-spy 是真正的无侵入方案,尤其适合线上紧急诊断。
安装与基本用法:
pip install py-spy # 查看实时火焰图(需浏览器打开) py-spy record -p <code>PID</code> -o profile.svg # 查看当前调用栈(文本版,最快) py-spy top -p <code>PID</code>
重点关注:
- 火焰图中长时间占据顶部的函数,尤其是
json.dumps、django.template.base.Template.render、或某次数据库查询的execute调用 —— 往往对应 N+1 查询或模板过度渲染 -
py-spy dump -p <code>PID输出所有线程栈,检查是否有线程卡在select.select(I/O 阻塞)、或反复执行同一段序列化逻辑 - 如果发现大量
__new__调用集中在某个模型类,大概率是该模型被高频实例化且未及时释放(比如日志中间件里无节制创建 Model 实例)
真正棘手的是那些不常触发、但每次触发就吃掉几百 MB 的场景——比如某次导出接口加载了全部用户数据再做内存排序。这类问题不会在常规监控里暴露,只能靠 py-spy 在问题发生瞬间抓帧。


















