Django 应用 OOM(Out of Memory)故障的定位思路和排查方法

二、定位思路总览

markdown 复制代码
1. 确认现象 → 2. 内存分析 → 3. 代码审查 → 4. 复现验证 → 5. 修复优化
    ↑___________________________________________________________|

三、详细排查步骤

第一步:确认内存使用趋势

1.1 系统层面监控

bash 复制代码
# 查看进程内存(RSS:实际物理内存,VSZ:虚拟内存)
ps aux --sort=-%mem | head -20

# 实时观察
watch -n 1 'ps -p <PID> -o pid,rss,vsz,comm'

# 查看系统 OOM 日志
dmesg -T | grep -i "killed process"
grep "Out of memory" /var/log/syslog

1.2 Django/Gunicorn 层面

bash 复制代码
# Gunicorn worker 内存
pgrep -f gunicorn | xargs -I {} ps -p {} -o pid,rss,vsz,cmd

# 查看 worker 重启次数(内存过高会被 master 重启)
grep "Worker" /var/log/gunicorn/error.log

1.3 监控图表分析

  • 内存持续增长不释放 → 内存泄漏
  • 内存周期性波动 → 正常,但峰值过高
  • 突然飙升后 OOM → 大对象分配/批量操作

第二步:Python 内存分析工具

2.1 基础工具:tracemalloc(Python 3.4+)

python 复制代码
# settings.py 中启用
import tracemalloc

tracemalloc.start(25)  # 保留25个栈帧

# 在需要查看的地方(如中间件、信号)
import tracemalloc

def log_memory():
    snapshot = tracemalloc.take_snapshot()
    top_stats = snapshot.statistics('lineno')[:10]
    
    for stat in top_stats:
        print(f"{stat.size / 1024 / 1024:.1f} MB")
        print(stat.traceback.format()[-3:])

2.2 进阶工具:memory_profiler

bash 复制代码
pip install memory_profiler
python 复制代码
from memory_profiler import profile

@profile
def heavy_view(request):
    # 逐行显示内存变化
    users = User.objects.all()  # 危险!
    data = list(users)          # 内存爆炸点
    return JsonResponse(data)

运行:

bash 复制代码
python -m memory_profiler manage.py runscript test_script

2.3 对象级分析:pympler + objgraph

python 复制代码
from pympler import tracker, muppy, summary
import objgraph

# 跟踪对象增长
tr = tracker.SummaryTracker()
tr.print_diff()  # 显示对象数量变化

# 查看最占内存的对象
all_objects = muppy.get_objects()
sum1 = summary.summarize(all_objects)
summary.print_(sum1)

# 查找循环引用(Django ORM 常见)
objgraph.show_most_common_types(limit=20)
objgraph.show_backrefs([some_obj], max_depth=10)

2.4 生产环境安全工具:Django-Prometheus + 自定义指标

python 复制代码
# 暴露内存指标给 Prometheus
import os
import psutil

process = psutil.Process(os.getpid())

def memory_usage_mb():
    return process.memory_info().rss / 1024 / 1024

# 在关键路径记录
logger.info(f"After query: {memory_usage_mb():.1f} MB")

第三步:Django 特有内存陷阱排查

3.1 ORM 查询优化(最常见!)

问题代码 内存影响 解决方案
Model.objects.all() 全表加载 iterator() / values_list()
len(queryset) 强制求值 .count()
list(queryset) 全部载入内存 分页处理
select_related() 滥用 JOIN 过大 只选必要字段
N+1 查询 多次 DB 往返 prefetch_related()

危险代码示例:

python 复制代码
# ❌ 内存爆炸:100万用户 × 1KB = 1GB
def bad_export(request):
    users = User.objects.all()
    data = []
    for user in users:  # 这里才触发查询,全部载入内存
        data.append({
            'name': user.name,
            'orders': list(user.orders.all())  # N+1!
        })
    return JsonResponse(data)

# ✅ 流式处理 + 预加载
def good_export(request):
    users = User.objects.prefetch_related('orders').iterator(chunk_size=1000)
    response = StreamingHttpResponse(
        (json.dumps({'name': u.name}) for u in users),
        content_type='application/json'
    )
    return response

3.2 检查 QuerySet 缓存

python 复制代码
# Django Debug Toolbar 或手动检查
from django.db import connection

def show_queries():
    print(f"Query count: {len(connection.queries)}")
    for q in connection.queries[-5:]:
        print(q['sql'][:100], f"{q['time']}s")

3.3 中间件/信号泄漏

python 复制代码
# 检查是否有全局变量累积数据
_BAD_CACHE = []  # 危险!进程级全局变量

class BadMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    
    def __call__(self, request):
        _BAD_CACHE.append(request.user)  # 只增不减!
        return self.get_response(request)

3.4 文件上传处理

python 复制代码
# ❌ 大文件直接读入内存
def bad_upload(request):
    content = request.FILES['file'].read()  # 100MB 文件 = 100MB 内存
    
# ✅ 流式处理
def good_upload(request):
    for chunk in request.FILES['file'].chunks(chunk_size=8192):
        process_chunk(chunk)

第四步:Gunicorn/Uvicorn 配置优化

python 复制代码
# gunicorn.conf.py
import multiprocessing

# Worker 类型
worker_class = "sync"  # 或 "gevent" / "uvicorn.workers.UvicornWorker"

# 关键:限制 worker 数量,防止内存耗尽
workers = multiprocessing.cpu_count() * 2 + 1
max_requests = 1000       # 处理1000请求后重启 worker(防泄漏)
max_requests_jitter = 50  # 随机抖动,避免同时重启
timeout = 30

# 内存限制(需要 systemd/cgroups 配合)
# 或使用 --worker-tmp-dir /dev/shm

K8s 配置:

yaml 复制代码
resources:
  limits:
    memory: "512Mi"
  requests:
    memory: "256Mi"

第五步:诊断流程图

markdown 复制代码
发现 OOM
   │
   ├─► 查看监控:是持续增长还是突然飙升?
   │       │
   │       ├─ 持续增长 ──► 内存泄漏嫌疑
   │       │              ├─ 用 tracemalloc 对比快照
   │       │              └─ 检查全局变量、单例、缓存
   │       │
   │       └─ 突然飙升 ──► 大对象分配嫌疑
   │                      ├─ 检查批量查询/导出功能
   │                      ├─ 检查文件上传处理
   │                      └─ 检查大数据量序列化
   │
   └─► 复现问题
           │
           ├─ 本地用 memory_profiler 定位具体行
           │
           └─ 生产用日志打点 + 采样分析

四、快速检查清单

bash 复制代码
# 1. 立即查看当前内存大户
ps aux --sort=-%mem | head -10

# 2. 查看 Django 进程
pgrep -f "gunicorn\|python" | xargs ps -o pid,rss,vsz,cmd

# 3. 检查是否有明显泄漏模式(RSS 持续增长)
for i in {1..10}; do 
    ps -p <PID> -o rss= >> memory.log
    sleep 5
done

# 4. 分析 Python 对象(如果还能进入 shell)
python -c "
import sys, gc
print(f'GC objects: {len(gc.get_objects())}')
big = [o for o in gc.get_objects() if sys.getsizeof(o) > 1000000]
print(f'Objects > 1MB: {len(big)}')
for o in big[:5]:
    print(type(o), sys.getsizeof(o))
"

五、修复优先级

优先级 问题 修复方式
P0 无分页全表查询 .iterator() 或分页
P0 全局变量累积 改为 Redis/Memcached
P1 N+1 查询 select_related/prefetch_related
P1 大文件内存处理 改为流式/chunk 处理
P2 Worker 不重启 配置 max_requests
P2 序列化大对象 values_list 减少字段

相关推荐
甄心爱学习2 小时前
【python】获取所有长度为 k 的二进制字符串
python·算法
tuotali20263 小时前
氢气压缩机技术规范亲测案例分享
人工智能·python
嫂子的姐夫3 小时前
030-扣代码:湖北图书馆登录
爬虫·python·逆向
a1117763 小时前
EasyVtuber(或其衍生/增强版本)的虚拟主播(Vtuber)面部动画生成与直播解决方案
python·虚拟主播
lintax3 小时前
计算pi值-积分法
python·算法·计算π·积分法
小凯123454 小时前
pytest框架-详解(学习pytest框架这一篇就够了)
python·学习·pytest
逻极4 小时前
pytest 入门指南:Python 测试框架从零到一(2025 实战版)
开发语言·python·pytest