二、定位思路总览
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 减少字段 |