flask中使用协程,通过gunicorn或uwsgi搭建web服务时遇到的坑

3,167 阅读4分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

问题描述

我需要通过flask搭一个深度学习的接口,由于深度学习耗时很长,flask的http服务是单向被动通讯,(客户端请求,服务器端响应)。客户端发来一个get请求,http服务在等待深度学习模型响应,如果数据量很大,连接一定会超时。

需求就变成,我需要写一个flask接口,返回response的同时,还需要运行深度学习模型,这里使用了多线程,其中我还用协程调一个内部的查询api,部署时用docker+uwsgi。变成了docker+uwsgi+flask+threading+gevent。

问题就出在了gevent和uwsgi上,昨天坑了我一天并且我还没有找到原因。

今天我就改成docker+gunicorn+flask+threading+gevent。成功部署之后,我就开始排查发现是gevent+uwsgi出的问题。下面做了点实验验证我的想法,如果实验部分不想看,建议拉到最后看结论部分。

PS . uwsgi 和 gunicorn 在windows安装基本不成功,建议使用docker或者linux系统,不然,努力半天就像什么都没干一样。

我本机是windows使用的是docker。结构如下

image.png

# main.py
import gevent
from gevent import monkey;monkey.patch_all()
from gevent.pool import Pool
import threading
from flask import Flask, request, jsonify
import time
from time import perf_counter

app = Flask(__name__)

# 模拟请求
def get_url(num):
    time.sleep(num)
    print(f"cost:{num}s")

def gevent_get(msg):
    print(msg)
    s=perf_counter()
    pool=Pool(5)
    threads=[pool.spawn(get_url,num) for num in [1,2,3,2,1,2,3,4,4,5]]
    gevent.joinall(threads

@app.route("/test", methods=["post","get"])
def test():
    t = threading.Thread(target=gevent_get, args=("开始",))
    t.start()
    return jsonify({"code": 500,"msg": "success"})


if __name__ == '__main__':
    app.run(debug=True)
# test.py
import requests
URL="http://192.168.160.237:8000/test"
response=requests.get(URL).json()
print(response)
# uwsgi.ini
[uwsgi]

# app是run.py里面的Flask对象 
module = main:app
# 对外提供 http 服务的端口
http = :8000
#指定工作进程
processes    = 4
#主进程
master     = true
#每个工作进程有2个线程
threads = 2
#指的后台启动 日志输出的地方
daemonize    = uwsgi.log
#保存主进程的进程号
pidfile = uwsgi.pid

docker build创建之后,通过uwsgi启动

uwsgi uwsgi.ini

如果按正常逻辑,输出应该是下面的结果

开始
[pid: 632|app: 0|req: 1/1] 172.17.0.1 () {32 vars in 386 bytes} [Tue Feb 15 05:28:25 2022] GET /test => generated 29 bytes in 4 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 0)
sleep:1s
sleep:1s
sleep:2s
sleep:2s
sleep:3s
sleep:2s
sleep:3s
sleep:4s
sleep:4s
sleep:5s
花费时间8.007732099999885

但是实际输出的结果,到“开始”就卡住了。也就是gevent没有用。

开始
[pid: 632|app: 0|req: 1/1] 172.17.0.1 () {32 vars in 386 bytes} [Tue Feb 15 05:28:25 2022] GET /test => generated 29 bytes in 4 msecs (HTTP/1.1 200) 2 headers in 71 bytes (3 switches on core 0)

我昨天弄了一天也没解决,因为上面的代码是我今天想通之后简化的,开发代码逻辑要复杂更多。后面我就生气了,我就改成gunicorn。结果还是出问题了。

# 我就改成gunicorn.py
# 绑定的ip与端口
bind = "0.0.0.0:8000"
# 进程数  
workers = 4
# 指定每个进程开启的线程数
# threads = 2
# 工作模式
worker_class = 'gthread'
# 处理请求的工作线程数,使用指定数量的线程运行每个worker。为正整数,默认为1。
# worker_connections = 2000
# 设置pid文件的文件名,如果不设置将不会创建pid文件
pidfile = './gunicorn.pid'
# 要写入错误日志的文件目录。
errorlog = './log/gunicorn.error.log' 
# 要写入的访问日志目录
accesslog = './log/gunicorn.access.log' 

正常输出应该是这样:

gunicorn -c gunicorn.py main:app
开始
sleep:1s
sleep:1s
sleep:2s
sleep:2s
sleep:3s
sleep:2s
sleep:3s
sleep:4s
sleep:4s
sleep:5s

但这里通要还是会卡住

gunicorn -c gunicorn.py main:app
开始

经过我多轮百度,翻看博客,发现gunicorn worker_class = 'gthread'有玄机。

我修改成worker_class = 'gevent'之后就一切正常了。默认的"sync"也是正常的

于是,我灵机一动想着是不是uwsgi也是和gevent有冲突。百度了一下,果然在uwsgi中使用协程要加一句话。

uwsgi uwsgi.ini --gevent 100 --gevent-early-monkey-patch

期间我还做了一下,gunicorn worker_class和uwsgi会不会对多线程有影响,结果是没有什么影响。

# main.py
import threading
from flask import Flask, request, jsonify
import time


app = Flask(__name__)

def get_url(num):
    for i in range(num):
        time.sleep(0.5)
        print(i)

@app.route("/test", methods=["post","get"])
def test():
        num=10
        t = threading.Thread(target=get_url, args=(num,))
        t.start()
        
        return jsonify({"code": 500,"msg": "success"})


if __name__ == '__main__':
    app.run(debug=True)

补充

后续还会研究一下,把上面的gevent替换成thread在做相同实验。

结论

在使用gevent+flask+uwsgi,配置启动

uwsgi uwsgi.ini --gevent 100 --gevent-early-monkey-patch

hi在使用gevent+flask+gunicorn,配置中模式worker_class = 'gevent'

gunicorn -c gunicorn.py main:app