uWSGI和Gunicorn的对比小结

由于uWGSI使用的是GPL2开源协议,Gunicorn使用的是MIT协议,商业使用为了规避相关法律风险,业务上有打算把uWSGI替换成Gunicorn,小结下两种组件的使用及区别。

简介

相同点

perfork是一种服务端编程模型, Nginx, Gunicorn, uWSGI都是这种模型的实现, 简单的说perfok就是master进程启动注册一堆信号处理函数, 创建listen socket fd, fork出多个worker子进程, 子进程执行accept循环处理请求(这里简化模型, 当然也可以用select, epoll多路复用), master进程只负责监控worker进程状态, 通过pipeline通信来控制worker进程.

perfork模型使用master进程来监控worker进程状态, 避免了我们使用supervisor来监控进程, 还支持多种信号来控制worker的数量, 使得CPU能充分得到利用, 多个worker进程监听同一端口, 可以配置reuse_port参数在worker进程间负载均衡.

区别

Gunicorn

Gunicorn是使用Python实现的WSGI服务器, 直接提供了http服务, 并且在woker上提供了多种选择, gevent, eventlet这些都支持, 在多worker最大化里用CPU的同时, 还可以使用协程来提供并发支撑, 对于网络IO密集的服务比较有利.

同时Gunicorn也很容易就改造成一个TCP的服务, 比如doge重写worker类, 在针对长连接的服务时, 最好开启reuse_port, 避免worker进程负载不均。

uWSGI

不同于Gunicorn, uWSGI是使用C写的, 它的socket fd创建, worker进程的启动都是使用C语言系统接口来实现的, 在worker进程处理循环中, 解析了http请求后, 使用python的C接口生成environ对象, 再把这个对象作为参数塞到暴露出来的WSGI application函数中调用. 而这一切都是在C程序中进行, 只是在处理请求的时候交给python虚拟机调用application. 完全使用C语言实现的好处是性能会好一些.

除了支持http协议, uWSGI还实现了uwsgi协议, 一般我们会在uWSGI服务器前面使用Nginx作为负载均衡, 如果使用http协议, 请求在转发到uWSGI前已经在Nginx这里解析了一遍, 转发到uWSGI又会重新解析一遍. uWSGI为了追求性能, 设计了uwsgi协议, 在Nginx解析完以后直接把解析好的结果通过uwsgi协议转发到uWSGI服务器, uWSGI拿到请求按格式生成environ对象, 不需要重复解析请求. 如果用Nginx配合uWSGI, 最好使用uwsgi协议来转发请求.

除了是一个WSGI服务器, uWSGI还是一个开发框架, 它提供了缓存, 队列, rpc等等功能, 在github找找就会发现有人用它的缓存写了一个Django cache backend, 用它的队列实现异步任务这些东西, 但是用了这些东西技术栈也就跟uWSGI绑定在一起, 所以一般也只是把uWSGI当作WSGI服务器来用。

测试对比

组里其他同学测试简单测试如下:

avatar

http://www.zh-noone.cn/2019/11/gunicorn-uwsgi%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94/ 文章中测试结果大体一致。其中该文章中总结使用方式如下:

gunicorn小结

1
2
3
4
5
6
7
8
9
werkzeug:RPS:681.30/sec

sync:RPS:615.97/sec

gevent:RPS:1030.56/sec

eventlet(preload模式):RPS:616.65/sec

eventlet(非preload模式):1048.65/sec

uwsgi小结

1
2
3
4
5
6
7
打印日志:RPS:1605.11/sec

不打印日志(preload模式):RPS:1868.42/sec

不打印日志(非preload模式):RPS:1433.42/sec

gunicorn+nginx+eventlet:RPS:1030.56/sec

小结

1
2
3
4
5
gunicorn 最好使用eventlet(非preload模式)

uwsgi+nginx 最好使用不打印日志(preload模式)

gunicorn与uwsgi比较最好使用uwsgi

Gunicorn简介

Gunicorn,是一个针对Python的、在Unix系统上运行的、用来解析HTTP请求的网关服务。它的特点是:能和大多数的Python web框架兼容;使用简单;轻量级的资源消耗;高性能。

Sync Workers(同步workers)

avatar

默认的、最简单的worker模式,是同步模式。

每个worker进程,一次只处理一个请求;如果此时又有其他请求被分配到这个worker进程,那只好被阻塞了,要先等第一个请求完成 。并且,一个请求一个进程,并发时,显然很占CPU和内存。

因此,只适合在访问量不大、CPU密集而非I/O密集的情形。

不过也不是没有好处;好处是,即使一个worker进程crash了,也只会影响一个请求。

Async Workers(异步workers)

avatar

有Gevent和Eventlet两种,都是基于Greenlet实现的。

用了异步worker,就能同时处理不止一个请求,就不会出现上面同步worker那样,一个请求就把后续请求都block阻塞住的情况了。

Gevent是一个Python网络函数库,它通过Greenlet协程+libev快速事件循环,实现了异步模型。有了Gevent,切换Greenlet时就不再需要手动切换,而是当一个Greenlet遇到I/O时,Gevent能自动切换Greenlet,保证总有Greenlet在运行,而无需等待I/O。

异步worker是怎样实现并发,使得一个worker就能同时处理很多请求的呢?

以Gevent为例,每个请求的连接是一个Greenlet协程。Gevent虽然只有一个线程、同时只能处理一个请求,但是在这个请求的异步事件没准备好、进入IO等待时,能主动yield让出控制权、而不是阻塞其他请求的协程,而是先让其他协程执行,当自己的IO准备好时,事件循环会将它从yield让出控制权的地方,继续恢复执行。

这样,Gevent就能在不同请求间不断切换,从而实现并发,以充分利用CPU、减少IO等待。并且,因为切换的Greenlet是“微线程”,它操作的维度是函数,而不是线程/进程,所以来回切换的开销,就没有那么大。

就我个人理解,同步worker和Gevent异步worker,这两种worker类型是最常用的。一般来说,我们的web app多半属于外部IO密集型(总要访问db、访问第三方服务等等),所以用Gunicorn的Gevent异步worker,就非常合理。

而如果你的web app是CPU密集型,或者你希望请求之间不要互相影响,那么可以选择Gunicorn的同步worker。

Tornado Workers

用来配合Tornado使用。

Tornado是一个Python框架和网络库,可以提供异步IO非阻塞型模型,来处理长延时请求。

avatar

AsyncIO Workers

分成gthread和gaiohttp两种模式。

gaiohttp利用aiohttp库,在服务端和客户端执行异步IO操作。支持web socket。

gthread是一种全线程worker,worker与线程池保持连接,线程会等待接收请求,一个请求一个线程。在Gunicorn启动时,除了可配置worker进程数,还可以配每个进程里的thread线程数。

avatar

Gunicorn是怎样实现高并发的

Gunicorn启动时,就把worker进程预先fork出来了。当多个请求到来的时候,会轮流复用这些worker进程,从而能提高服务器的并发负载能力。至于worker数的配置,一般推荐2CPU数+1。这样推荐,背后的想法是,在任何时间,都有大概一半的worker是在做I/O,剩下一半才是需要CPU的。如果在开多进程的同时,也开多线程(也就是选择gthread类型的worker),那么,配置总的并发数(worker进程数线程数),仍然建议2*CPU数+1。

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

方案一:使用gevent做协程,从而解决高并发的问题,通过Gunicorn(with gevent)的形式对app进行包装,从而来启动服务

1
2
3
4
5
6
7
8
9
10
# 使用gevent做异步(默认worker是同步的)    多进程+协程
gunicorn -w 8 --bind 0.0.0.0:8000 -k 'gevent' 运行文件名称:Flask程序实例名

# 使用gunicorn命令启动flask项目
# -w 8
8个进程
# --bind 0.0.0.0:8000
ip + 端口
# -k 'gevent'
协程

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

使用gunicorn + gevent 开启高并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 多进程
import multiprocessing

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


# 预加载资源
preload_app = True
# 绑定 ip + 端口
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 = "gevent"

# 最大客户客户端并发数量,对使用线程和协程的worker的工作有影响
# 服务器配置设置的值 1200:中小型项目 上万并发: 中大型
# 服务器硬件:宽带+数据库+内存
# 服务器的架构:集群 主从
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'

执行 :

1
gunicorn -c gunicorn_config.py flask_server:app

协程 第三方封装库

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

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

eventlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

Gunicorn源码概览

gunicorn源码的入口文件为main.py

1
2
from gunicorn.app.wsgiapp import run
run()

在初始化加载相关配置文件后,调用 Arbiter(self).run()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def run(self):
"Main master loop."
self.start()
util._setproctitle("master [%s]" % self.proc_name)

try:
self.manage_workers()

while True:
self.maybe_promote_master()

sig = self.SIG_QUEUE.pop(0) if self.SIG_QUEUE else None
if sig is None:
self.sleep()
self.murder_workers()
self.manage_workers()
continue

if sig not in self.SIG_NAMES:
self.log.info("Ignoring unknown signal: %s", sig)
continue

signame = self.SIG_NAMES.get(sig)
handler = getattr(self, "handle_%s" % signame, None)
if not handler:
self.log.error("Unhandled signal: %s", signame)
continue
self.log.info("Handling signal: %s", signame)
handler()
self.wakeup()
except (StopIteration, KeyboardInterrupt):
self.halt()
except HaltServer as inst:
self.halt(reason=inst.reason, exit_status=inst.exit_status)
except SystemExit:
raise
except Exception:
self.log.info("Unhandled exception in main loop",
exc_info=True)
self.stop(False)
if self.pidfile is not None:
self.pidfile.unlink()
sys.exit(-1)

其中self.start()会根据配置文件加载相关运行信息,包括worker工作方式。在manage_workers代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def manage_workers(self):
"""\
Maintain the number of workers by spawning or killing
as required.
"""
if len(self.WORKERS) < self.num_workers:
self.spawn_workers()

workers = self.WORKERS.items()
workers = sorted(workers, key=lambda w: w[1].age)
while len(workers) > self.num_workers:
(pid, _) = workers.pop(0)
self.kill_worker(pid, signal.SIGTERM)

active_worker_count = len(workers)
if self._last_logged_active_worker_count != active_worker_count:
self._last_logged_active_worker_count = active_worker_count
self.log.debug("{0} workers".format(active_worker_count),
extra={"metric": "gunicorn.workers",
"value": active_worker_count,
"mtype": "gauge"})

在spawn_worker中有初始化进程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def spawn_worker(self):
self.worker_age += 1
worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS,
self.app, self.timeout / 2.0,
self.cfg, self.log)
self.cfg.pre_fork(self, worker)
pid = os.fork()
if pid != 0:
worker.pid = pid
self.WORKERS[pid] = worker
return pid

# Do not inherit the temporary files of other workers
for sibling in self.WORKERS.values():
sibling.tmp.close()

# Process Child
worker.pid = os.getpid()
try:
util._setproctitle("worker [%s]" % self.proc_name)
self.log.info("Booting worker with pid: %s", worker.pid)
self.cfg.post_fork(self, worker)
worker.init_process()
sys.exit(0)
except SystemExit:
raise
except AppImportError as e:
self.log.debug("Exception while loading the application",
exc_info=True)
print("%s" % e, file=sys.stderr)
sys.stderr.flush()
sys.exit(self.APP_LOAD_ERROR)
except Exception:
self.log.exception("Exception in worker process")
if not worker.booted:
sys.exit(self.WORKER_BOOT_ERROR)
sys.exit(-1)
finally:
self.log.info("Worker exiting (pid: %s)", worker.pid)
try:
worker.tmp.close()
self.cfg.worker_exit(self, worker)
except Exception:
self.log.warning("Exception during worker exit:\n%s",
traceback.format_exc())

在init_process中就能够选择不同类型的worker了。

参考文献

  1. https://pypi.org/project/gunicorn/
  2. https://www.cnblogs.com/xuegqcto/p/10411943.html
  3. http://www.zh-noone.cn/2019/11/gunicorn-uwsgi%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94/
  4. https://blog.csdn.net/dadajuan_plus/article/details/106571287
  5. https://blog.csdn.net/weixin_46072106/article/details/109708788