Django服务压力测试异常:超4000请求后随机Socket超时求助
嘿,我来帮你一步步揪出这个问题的根源!从你描述的现象——单条请求正常,但批量跑到4000条左右就变慢甚至Socket超时——来看,大概率是资源泄漏或者请求堆积搞的鬼,咱们从代码到运行环境逐一排查:
先修复代码里的低级bug(别让小问题干扰排查)
先看你的服务端handle函数,有两个明显的错误,先改掉:
- 判断了
"vid" in request.GET,但后面却取了request.GET["id"],这明显是笔误,应该取vid; - 解码
title后又用原请求值覆盖了,会导致中文乱码(虽然这可能不是性能问题,但先修正避免其他干扰)。
修正后的代码:
def handle(request) : sys.stderr.write( "[info]request come in {} \n".format(sys._getframe().f_code.co_name)) ret_dict = dict() ret_str = "" if ("vid" in request.GET) and ("title" in request.GET): # 修正:取vid而非id vid = request.GET["vid"] # 保留解码后的title,不要覆盖 title = urllib.parse.unquote(request.GET["title"], encoding='utf-8') req_video = {"vid": vid, "title": title} tags = handle_function(req_video) ret_dict["vid"] = vid ret_dict["tags"] = tags ret_str = json.dumps(ret_dict) return HttpResponse(ret_str)
核心排查方向:资源泄漏
批量请求到一定量出问题,最常见的就是资源没释放,越积越多,导致服务崩溃:
- 数据库连接泄漏:如果
handle_function里用到了数据库,检查是否每次请求后都关闭了连接,或者连接池配置不合理(比如最大连接数不够)。可以用数据库自带的工具看连接数变化——比如PostgreSQL用ps aux | grep postgres,如果连接数随着请求持续上涨不回落,那就是泄漏了。 - 文件句柄泄漏:用
lsof -p <你的Django进程PID>查看进程打开的文件/网络句柄数量,每处理一批请求后看数值是不是一直涨。如果超过系统限制(用ulimit -n查看),会导致无法创建新Socket,直接触发超时。 - 外部请求未设超时:如果
handle_function调用了外部API,一定要加超时时间!比如用requests.get(url, timeout=5),不然外部服务偶尔响应慢,请求会一直挂着占用Django工作进程,导致后续请求排队,越拖越慢最后超时。
检查Django的运行模式(别用开发服务器跑批量请求!)
如果你是用python manage.py runserver启动的Django,这是开发专用服务器,默认单进程单线程(或低并发配置),批量请求很容易被阻塞导致请求堆积。生产/压测环境必须用Gunicorn、uWSGI这类WSGI服务器,配置足够的进程和线程:
比如用Gunicorn启动(根据服务器CPU核心数调整,一般workers设为CPU核心数*2+1):
gunicorn --workers 4 --threads 2 your_project.wsgi:application
监控请求处理细节,定位瓶颈
给handle函数加日志,记录每个请求的处理时间,看看从第4000条开始是不是处理时间突然飙升:
import time def handle(request) : start_time = time.time() sys.stderr.write( "[info]request come in {} \n".format(sys._getframe().f_code.co_name)) ret_dict = dict() ret_str = "" vid = "" # 提前定义避免日志报错 if ("vid" in request.GET) and ("title" in request.GET): vid = request.GET["vid"] title = urllib.parse.unquote(request.GET["title"], encoding='utf-8') req_video = {"vid": vid, "title": title} tags = handle_function(req_video) ret_dict["vid"] = vid ret_dict["tags"] = tags ret_str = json.dumps(ret_dict) # 记录单请求处理时间 process_time = time.time() - start_time sys.stderr.write(f"[info]vid:{vid} 处理耗时 {process_time:.2f}s\n") return HttpResponse(ret_str)
同时用top/htop监控Django进程的CPU、内存占用——如果处理到4000条后CPU跑满或内存飙升,那就是资源耗尽导致的超时。
排查客户端连接问题
客户端是顺序发送请求,检查get_rec_mingqiang函数里的连接是否正确关闭:
比如用requests的话,最好用Session复用连接,或者确保每次请求后释放连接:
def get_rec_mingqiang(server_ip_mq, server_port_mq, web_page_mq, page_params_mq): url = f"http://{server_ip_mq}:{server_port_mq}{web_page_mq}" # 用with语句自动关闭会话,避免连接泄漏 with requests.Session() as s: # 给客户端请求也加超时,避免一直等服务器响应 response = s.get(url, params=page_params_mq, timeout=10) response.raise_for_status() return response.json()
如果客户端没正确关闭连接,服务器端会有大量TIME_WAIT状态的连接,占满端口。可以用netstat -an | grep TIME_WAIT查看,如果数量很多,可调整系统TCP参数(比如echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse)。
单独压测handle_function
既然单条请求没问题,批量出问题,那handle_function可能有累积性性能问题(比如内存泄漏、缓存缺失)。单独写个脚本压测它:
import time import tracemalloc # 导入你的handle_function from your_module import handle_function tracemalloc.start() start_time = time.time() # 模拟批量调用 with open('test_data', 'r') as f: for line in f: line = line.rstrip('\n') fields = line.split("\t") vid = fields[0] title = fields[1] req_video = {"vid": vid, "title": title} tags = handle_function(req_video) end_time = time.time() current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"总耗时: {end_time - start_time:.2f}s") print(f"峰值内存: {peak / 1024 / 1024:.2f}MB")
如果这个测试里时间越来越长,或内存持续上涨,那问题就出在handle_function里——比如内部有全局变量累积数据、数据库查询没加索引、没释放临时资源等,针对性优化即可。
内容的提问来源于stack exchange,提问作者davidliu

