当多人同时请求一个flask接口时,并发是一件需要考虑的事。
1. postman
postman可以用来测试接口,但是postman是串行的,当一个请求完成后才会发送下一个请求,可以测试连续执行,但不能测试并发。
【补充】:postman以json方式提交post请求,首先在headers中添加属性,接着修改body—>raw,设置为json
2. jMeter
双击jmeter.bat,jemter需要java环境,即安装jdk,见【https://www.cnblogs.com/hupilan521/p/12460580.html】
配置线程组、HTTP请求后,即可进行并发测试,这里的信息头要添加第1节中【补充】内容。
查看测试日志,发现flask接口能够接收到请求,但是同时接收的多个请求堵塞在一起返回。
3. WSGI
- 考虑到flask接口采用app.run()的开发模式运行,因此修改为WSGI启动,但是WSGI会强行将并发测试请求转为串行方式执行。
from gevent import pywsgi
server = pywsgi.WSGIServer(('0.0.0.0', 13579), app)
server.serve_forever()
4. 线程、进程、并发
详细了解下多线程、多进程、并发的含义,并使用python进行实践测试。
- 进程:是正在进行的程序
- 并发:指的是多个事件同时发生,本质是任务间的切换给人的感觉是同时在进行,称之为伪并行, 例如洗衣服和做饭,同时发生了
- 并行:指的是多个事件同时进行中,例如一个人在写代码另一个人在写书,这两件事件是同时在进行的,一个人是无法真正的并行执行任务
from multiprocessing import Process
from threading import Thread
os.getpid() # 获取进程号
发现flask接口开启后,一个api同时接收多个请求时,各个请求的进程号一样,当同时发送5次请求时,耗时从一次请求时的0.5s增加为1.3s,且5个请求的耗时相当。
因此,修改api接收到参数后的处理方式,采用multiprocessing.Process()调用函数,可以发现,发送同时发送20次请求时,耗时与一次请求相当。但是当发送25个请求时,耗时开始增加到0.8s。发送30个请求时,耗时增加到1s,推测是多个进程的阻塞。
5. 尝试降低单次请求计算耗时
- 采用grequests查询题目,一次请求耗时从0.5s降为0.2s
- 采用es客户端查询,一次请求耗时从0.5s降为0.2s,与grequests查询的效率相当
- 将查询id拼接后,采用OR语句查询,一次请求降为0.15s
- 将数据导入mysql数据库,采用pymysql查询,一次请求降为0.06s
【补充】:mysql 存储过程,检查执行耗时发现和采用sql执行耗时相当
DROP PROCEDURE select_more_topic;
delimiter ;;
CREATE PROCEDURE select_more_topic(in param1 varchar(10000))
BEGIN
set @id = param1;
set @sel = concat('select * from anpei_practise_info where _id in (', param1, ')');
prepare stmt from @sel;
execute stmt;
deallocate prepare stmt;
END;;
delimiter ;
call select_more_topic('"3886785006424822201","3886785006252855617"');
6. 处理flask、es的多次请求堵塞问题
python的多线程、多进程感觉效率提升的用处不大,最终采取的方案是:采用分布式缓存Redis存储数据,从Redis中一次请求的耗时约为0.01,并采用ngix进行负载均衡,将请求轮询发送到3个服务器节点上。
这个做法就是增加了数据库存储的开销,因为目前场景下这种出现多并发查询的情况是很少的,其实不怎么划算。而且Redis数据库的维护还要考虑。
- 采用进程池分发请求
pool = multiprocessing.Pool(4)
for i in range(10):
pool.apply_async(main)
pool.close()
pool.join()
- 采用多线程处理
class ThreadReturn(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
self.result = self.func(*self.args)
def get_result(self):
try:
return self.result
except Exception as e:
print(e)
return None
for i in range(1):
p1 = ThreadReturn(search_mysql, (i,))
existing_list.append(p1)
for i in existing_list:
i.start()
for sub in existing_list:
sub.join()