Flask-SocketIO 在 gunicorn 部署环境下使用 gevent 和 eventlet 哪个更好?

Updated on in Python with 254 views

先来说答案 —— eventlet

背景

Flask-SocketIO 应用部署和常规的 Flask 应用部署大同小异,开发时常规的应用使用 flask.run() 运行,而 Flask-SocketIO 应用使用 socketio.run() 运行。

部署时我们通常需要一个 Web 服务器,例如 gunicornuWSGI 等。而此时需要用到 Python 的协程网络库来支持请求响应的并发。而常用的协程网络库有 geventeventlet 等。

协程

协程是个很有趣的东西,第一次接触这玩意是在面试官的面试题里。进程和线程大部分开发者都了解过,但协程又是什么呢?

直观得来说 进程 > 线程 > 协程,协程是一种比线程粒度还小的调度单位。对于 Python 中的协程而言,它的诞生可能离不开一个叫做 GIL(全局解释性锁) 的东西(另一个面试题)。Python 中的多线程运行示例图如下图所示。

GIL 一直以来是被诟病的东西,简单来讲,GIL 的存在直接导致了多线程只能发挥单核的效用。在日常使用时,为了发挥多核并行的优势,我们甚至会使用多进程来替代多线程,但多进程的切换开销又过大了,有些得不偿失。

为了更好地解决并发问题,协程应运而生。它也被称作微线程,是比线程更小的执行单元,其切换由程序控制,而非操作系统。这就能够做到一个 CPU 处理上万个协程,非常适用于高并发、低成本、高拓展的场景。

协程框架

协程框架有哪些(又是一个经典的面试题)?没想到往下写能写出这么多 Python 经典面试题,主要是大部分都遇到过,所以一直记着。这里就介绍两种:geventeventlet

gevent

gevent 是一种基于协程的 Python 网络库,实现了 Python 标准库中大部分的阻塞式系统调用。但它没有原生的 websocket 支持,若要支持,还需安装 gevent-websocket 包。

使用 gunicorn + gevent 时,一般需要用到一个叫做“猴子补丁”的玩意 monkey.patch_all(),可以使得线程的方式无感知的使用协程。举例来说,它能自动将内置的阻塞 socket,替换为非阻塞的 socket,而无需侵入源码。

猴子补丁 —— 起源于 Zope 框架,在修正 Zope 的 bug 时经常在程序后面追加更新,这些更新被称作是“杂牌军补丁(guerillapatch)”,后来 guerilla 渐渐地写成了 gorllia(猩猩),再后来,就写了 monkey,猴子补丁的叫法由此而来。

# gconfig.py
import multiprocessing
from gevent import monkey
monkey.patch_all()
daemon = False
reload = True
debug = False
bind = '0.0.0.0:5000'
worker_class = 'gevent'
loglevel = 'info'
pidfile = 'log/gunicorn.pid'
accesslog = 'log/access.log'
errorlog = 'log/error.log'
workers =1
# 运行
gunicorn -c gconfig.py starter:app

原则上使用 gevent 也能够拉起 Flask-SocketIO,但在运行过程中还是遇到了问题,例如 Socket 断连、连接不稳定等,一部分是版本兼容性问题,另一部分则是 gevent 的 Socket 性能表现问题。

这里在关注一下 Flask-SocketIO的官方文档,其中优先考虑的是使用 eventlet 与 gunicorn 联合使用,而非 gevent。

eventlet

eventlet 是一个高性能的协程框架,支持长轮询、websocket,性能优于 gevent,而且不需要安装其他依赖。socket 通信优先选它,这也是本文标题的标准答案。

使用时只需要,修改上述 gconfig.py 文件的 worker_class 和导包即可。但是这里还是可能会出现问题,主要是由于各个依赖之间的版本差异导致的。

笔者在使用 eventlet 时至少遇到了 3-5 个不同的问题,例如:

  • TypeError: wrap_socket() got an unexpected keyword argument '_context'
  • [ERROR] Exception in worker process
  • OSError: [Errno 9] Bad file descriptor

这些问题的解决,其实取决于你的项目的环境,在此我提供一下相关依赖的主要版本。

[[source]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
verify_ssl = true
name = "pypi"

[packages]
flask = "==1.1.4"
flask-socketio = "==4.3.2"
python-socketio = "==4.6.1"
python-engineio = "==3.14.2"
gunicorn = "==20.1.0"
eventlet = "==0.29.1"
requests = "==2.23.0"

[dev-packages]

[requires]
python_version = "3.7"

我使用如上依赖版本时运行 Flask-SocketIO 可以完美使用,运行方式与文档一致,如下所示:

gunicorn -w 1 -b 0.0.0.0:5000 starter:app --worker-class eventlet --reload

上述问题是我在开发 july_server 这个项目时所遇到并解决的,欢迎大家支持该项目 https://github.com/YYJeffrey/july_server


标题:Flask-SocketIO 在 gunicorn 部署环境下使用 gevent 和 eventlet 哪个更好?
作者:Jeffrey

Responses
取消