JavaEE鸿蒙应用开发HTML&JS+前端Python+大数据开发人工智能开发AI+设计软件测试新媒体+短视频直播运营产品经理集成电路应用开发(含嵌入式)Linux云计算+运维开发C/C++拍摄剪辑+短视频制作PMP项目管理认证电商运营Go语言与区块链大数据PHP工程师Android+物联网iOS.NET

flask+Gunicorn(gevent)+sqlalchemy 高并发的解决方法探究

来源:黑马程序员

浏览46804人

2019.08.30

1.使用Flask的做服务器框架,可以以python code.py的方式运行,但这种方式不能用于生产环境,不稳定,比如说:  有一定概率遇到连接超时无返回的情况

1,通过设置app.run()的参数,来达到多进程的效果。看一下app.run的具体参数:

1.jpg

注意: threaded与processes不能同时打开,如果同时设置的话,将会出现以下的错误:

2.jpg

2.使用gevent做协程,从而解决高并发的问题:

# 携程的第三方包-这里选择gevent, 当然你也可以选择eventlet

  pip install gevent

# 具体的代码如下:

from flask import Flask

from gevent.pywsgi import WSGIServer

from gevent import monkey

# 将python标准的io方法,都替换成gevent中同名的方法,遇到io阻塞gevent自动进行协程切换

monkey.patch_all()

# 1.创建项目应用对象app

app = Flask(__name__)

# 初始化服务器

WSGIServer(("127.0.0.1", 5000), app).serve_forever()

# 来启动服务---这样就是以协程的方式运行项目,提高并发能力

 python code.py

3.通过Gunicorn(with gevent)的形式对app进行包装,从而来启动服务【推荐

安装遵循了WSGI协议的gunicorn服务器--俗称:绿色独角兽

  pip install gunicorn

查看命令行选项: 安装gunicorn成功后,通过命令行的方式可以查看gunicorn的使用信息。

$gunicorn -h

1.jpg

指定进程和端口号: -w: 表示进程(worker) --bind:表示绑定ip地址和端口号(bind) —threads 多线程  -k  异步方案

# 使用gevent做异步(默认worker是同步的)

gunicorn -w 8 --bind 0.0.0.0:8000 -k 'gevent' 运行文件名称:Flask程序实例名

运行方案2: 将运行的信息加载到配置文件中

使用gunicorn + gevent 开启高并发

import multiprocessing

"""gunicorn+gevent 的配置文件"""

# 预加载资源

preload_app = True

# 绑定

bind = "0.0.0.0:5000"

# 进程数

workers = multiprocessing.cpu_count() * 2 + 1

# 线程数

threads = multiprocessing.cpu_count() * 2

# 等待队列最大长度,超过这个长度的链接将被拒绝连接

backlog = 2048

# 工作模式

# worker_class = "egg:meinheld#gunicorn_worker"

worker_class = "gevent"

# 最大客户客户端并发数量,对使用线程和协程的worker的工作有影响

worker_connections = 1200

# 进程名称

proc_name = 'gunicorn.pid'

# 进程pid记录文件

pidfile = 'app_run.log'

# 日志等级

loglevel = 'debug'

# 日志文件名

logfile = 'debug.log'

# 访问记录

accesslog = 'access.log'

# 访问记录格式

access_log_format = '%(h)s %(t)s %(U)s %(q)s'

# 运行方式 命令行

gunicorn -c gunicorn_config.py flask_server:app

使用 meinheld + gunicorn + flask 开启高并发神器

前提在虚拟环境中安装meinheld:

  pip install meinheld

import multiprocessing

"""gunicorn+meinheld 的配置文件"""

# 预加载资源

preload_app = True

# 绑定

bind = "0.0.0.0:5000"

# 进程数: cup数量 * 2 + 1

workers = multiprocessing.cpu_count() * 2 + 1

# 线程数 cup数量 * 2

threads = multiprocessing.cpu_count() * 2

# 等待队列最大长度,超过这个长度的链接将被拒绝连接

backlog = 2048

# 工作模式

worker_class = "egg:meinheld#gunicorn_worker"

# 最大客户客户端并发数量,对使用线程和协程的worker的工作有影响

worker_connections = 1200

# 进程名称

proc_name = 'gunicorn.pid'

# 进程pid记录文件

pidfile = 'app_run.log'

# 日志等级

loglevel = 'debug'

# 日志文件名

logfile = 'debug.log'

# 访问记录

accesslog = 'access.log'

# 访问记录格式

access_log_format = '%(h)s %(t)s %(U)s %(q)s'

# 运行方式 命令行

gunicorn -c gunicorn_config.py flask_server:app

拓展

概念:协程就是协同工作的程序,不是进程也不是线程 理解成--不带返回值的函数调用。

Coroutine:协程,又称微线程,纤程。 

协程的这种“挂起”和“唤醒”机制实质上是将一个过程切分成了若干个子过程,给了我们一种以扁平的方式来使用事件回调模型。优点:共享进程的上下文,一个进程可以创建百万,千万的coroutine。

python中的yield和第三方库greenlet,都可以实现协程。 

greenlet 提供了在协程中直接切换控制权的方式,比生成器(yield)更加灵活、简洁。


历史遗留问题—GIL锁

1.线程安全是在多线程的环境下,线程安全能够保证多个线程同时执行时程序依旧运行正确,而且要保证对于共享的数据,可以由多个线程存取,但是同一时刻只能有一个线程进行存取。每一个interpreter进程,只能同时仅有一个线程来执行,获得相关的锁,存取相关的资源。那么很容易就会发现,如果一个interpreter进程只能有一个线程来执行,多线程的并发则成为不可能,即使这几个线程之间不存在资源的竞争。

2.所以虽然 CPython的线程库直接封装操作系统的原生线程,但CPython进程做为一个整体同一时间只会有一个获得了GIL的线程在跑,其它的线程都处于等待状态等着 GIL的释放。所以只能使用cpu单核。这也是python多线程被人诟病的原因。

解决方案:python的高并发更加推荐多进程+协程

io多路复用

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。 

1. select(线程不安全):它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。 

2. poll(线程不安全):它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制 

3. epoll(线程安全):epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

python异步实现

多进程 + 协程 + callback(io多路复用做事件驱动)

协程 第三方封装库:

  • gevent  = greenlet + python.monkey(底层使用 libevent   时间复杂度: O(N * logN))

  • meinheld = greenlet + picoev    (时间复杂度: O(N) )

  • eventlet

picoev和libevent

meinheld和gevent都能实现异步,但是测评中meinheld比gevent的性能好很多,不过因为meinheld支持的比较少,一般都是配合gunicorn使用的。下面分析一下meinheld和gevent性能差距主要原因,分别使用的是picoev和lievent。

# libevent

主要实现:使用堆(优先队列)作为timer事件的算法(nlogn),IO和信号的实现均使用了双向队列(用链表实现)。

时间复杂度: O(N * logN)

# picoev

picoev:根据作者简介,主要优化有两点。 

1. 主要是考虑是fd(file descriptors)在unix中是用比较小的正整数表示的,那么把fd的相关信息,全部存储在一个array中,这样使得查找快速,在操作socket状态时会更加的快。 

2. 第二点是对于timer事件的算法优化,通过环形缓冲区(128)和bit vector实现查看部分源码可以看出,主要实现是每个时间点对应的是缓冲区的一个位置,每个缓存区使用bit vector 表示fd的数值,相当于一种hash映射所以时间复杂度为(o(n)),n为那个缓存区所存的fd数量。

时间复杂度: O(N)

性能: picoev > libevent

理解----协程&线程&进程


计算机.jpg

2.思考:协程之前切换的场景?

程序发送阻塞的时候切换

  • 读磁盘

  • 读写文件

  • 网络io操作

  • 收发http请求