站内链接:

Introduction

介绍

gunicode(独角兽)是一个高效的 python WSGI Server,该项目是由 Ruby 的 Unicorn 项目移植而来, 继续了大部分的设计思想并扩展和巩固了其中一些概念. 一般使用 unicorn 来运行 wsgi application/wsgi framework, 类似 java 中的tomcat, 在大部分的服务器架构中, 其通常位于反向代理/负载均衡(nginx)和 web 应用之间(flask/django).

gunicorn 实现了一个 UNIX 的预分发web 服务器端, 其中的一些专业术语类似 nginx:

  • 一个 master,多个 worker
  • master 管理配置和热升级
  • worker 接收链接和处理链接

其中 worker 可选实现如下:

  • sync worker # 每一个进程当且处理一个请求
  • thread worker # 通过在一个进程里面开启多个线程来处理多请求, 对应 thread.py
  • greenlet worker # 没有使用 python 原生线程而是使用 eventlet 或是 gevent 协程来响应多个请求
  • tornado worker # tornao 提供的 asyncio worker, 对应 gtornado.py
  • aiohttp # aiohttp 提供的 asyncio worker, 对应 gaiohttp.py

下面章节会介绍不同的配置方式以及各个配置方式的使用场景.

1.2 pre-fork

在介绍pre-fork之前, 先回顾一下大学介绍编写服务器的一些知识点: 迭代服务器, 并发服务器:

  • 迭代服务器: 依次处理客户顿连接, 只要上一次连接未处理完成, 服务器资源就会被一直占用, 这是最简单的 socket 服务器
  • 并发服务器: 对每一个客户端连接请求, fork 自身去处理该请求, master 自身一直处于listen状态不会导致阻塞

pre-fork服务器类似fork服务器, 其也是一个较为古老且稳定的服务器模式, 通过一个单独的 worker 进程来处理每条请求, 但是其会预派生一些子进程等待客户端连接, 从而减少了频繁创建和销毁进程的开销, 并且默认情况下每一个进程仅仅包含一个线程. 在高峰负载介绍, 由于预派生子进程不足, 仍然需要消耗一定的时间/空间来处理新的请求, 从而造成一定的响应延时.

另外, 其他服务器模式优化方案:

  • Worker: 多进程 + 多线程 + pre-fork, 此配置在大部分的中小服务中已经足够.
  • Event: 多进程 + 多线程 + epoll, 例如 nginx 服务, 基于 IO 多路复用技术

服务器模式

gunicorn 基于pre-fork模块进行框架设计: 一个 master, 一组 worker 进程, 下面简单的描述这些 worker 的功能以及分类

  1. master 主控进程

master 一个无限循环, 不断的 listen 不同进程(子)信号从而进行不同的 action, master 通过信号来管理正在运行中的 worker 进程, 这里简单的列举一些常见的信号以及功能:

  • TTIN: 增加一个 worker 进程
  • TTOU: 减少一个 worker 进程
  • CHLD: 一个子进程中止后, 主控 master 重启这个失效的进程
  • HUP: 重启所有的配置, 重启所有的 worker 进程
  • QUIT: 正常关闭, 等待所有 worker 关闭之后正常退出
  • INT/TERM: 强制关闭
  • WINCH: 正常关闭 worker, 保持主控 master 运行
  1. 同步/异步

大部分情况下 worker 都采用sync的方式运行, 即一次仅仅处理一个请求, 但是明显效率非常低效.async worker则基于 greenlets 软件包(eventle, genvent)实现的异步请求处理, 该方式大大提升了批量请求处理的效率和上限, 其中 greenlet 就基于 Python 实现的协程方式.

注意, 在部分操作系统上可能不支持协程处理机制, 之前就碰到客户的红帽系统无法支持协程或者协程的工作效率极为底下, 最后只能改为多进程/多线程的工作模式.

为了达到最大效率, gunicorn 的 worker 数量应该设置为多少? 一般而言4-12个 workers 就可以做到每秒钟处理几百甚至上千的请求了, 一般推荐的 worker 数量: 2 * num_cores) + 1.

Configure

安装

异步库:

1
2
3
pip install greenlet
pip install eventlet
pip install gevent

gunicorn:

1
pip install gunicorn

命令行

gunicorn 命令的基本格式: gunicorn [OPTIONS] APP_MODULE, 以下是一些常用的命令选项及其说明:

  • -b, --bind <ADDRESS>: 指定绑定的地址和端口。例如:-b 0.0.0.0:8080
  • -w, --workers <COUNT>: 设置工作进程的数量。默认为 1。例如:-w 4
  • -k, --worker-class <CLASS>: 指定工作进程的类型或类。默认为sync。常用的选项包括 synceventletgevent等。
  • -t, --timeout <SECONDS>: 设置超时时间,单位为秒。默认为 30 秒。例如:-t 60
  • -p, --pid <FILE>: 指定保存主进程 PID 的文件路径。例如:-p gunicorn.pid
  • -n, --name <NAME>: 设置进程的名称。默认为gunicorn。例如:-n myapp
  • -D, --daemon: 启动守护进程模式。后台运行 Gunicorn 服务器。
  • -c Config: 指定配置文件启动
  • --access-logfile <FILE>: 指定访问日志文件路径。例如:--access-logfile access.log
  • --error-logfile <FILE>: 指定错误日志文件路径。例如:--error-logfile error.log
  • --reload: 在代码文件变化时自动重载应用程序。
  • --workers-class <MODULE>:<CLASS>: 自定义工作进程的类型或类。例如:--workers-class mymodule.MyWorker

这些选项可以根据需要进行组合和调整,以满足具体应用程序的需求。使用gunicorn --help命令可以查看更多可用选项和其详细说明。另外, 命令gunicorn -c gunicorn.conf.py app:app指定的配置文件示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# gunicorn.conf.py
# 绑定的地址和端口
bind = '0.0.0.0:8000'

# 工作进程数
workers = 4
# 工作进程类型
worker_class = 'gevent'
# 超时时间
timeout = 30

# 访问日志文件路径
accesslog = '/var/log/gunicorn/access.log'
# 错误日志文件路径
errorlog = '/var/log/gunicorn/error.log'

进程类型

--worker-class选项用于指定 Gunicorn 工作进程的类型或类。不同的工作进程类型在处理并发请求时采用不同的策略和机制。该选项可接受以下可选值:

  • sync:同步工作进程,采用阻塞模式处理请求。
  • eventlet:使用 Eventlet 库的工作进程,适合处理 I/O 密集型任务。
  • gevent:使用 Gevent 库的工作进程,适合处理 I/O 密集型任务。
  • meinheld:使用 Meinheld 库的工作进程,提供较低的内存使用和更好的性能。
  • tornado:使用 Tornado 库的工作进程,适合处理高并发请求。
  • gthread:使用标准库 threading 实现的多线程工作进程,适合处理 I/O 密集型任务。

默认日志

1
2
3
# 运行阶段的gunicorn日志, 用于记录程序的错误, 并非gunicorn本身的信息
less logs/gunicorn-supervisor.log
less logs/gunicorn-error.log

工作模式

默认

每一个 worker 都是一个加载 Python 应用程序的 UNIX 进程, 进程之间不存在共享内容, 下面为模式下的实际测试例子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
""" flask 基本服务 """
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
return "Hello, World!"

@app.route("/sleep")
def sleeptest():
time.sleep(0.2)
return "Block Test"

if __name__ == '__main__':
app.run(host='0.0.0.0')

gunicorn 启动命令, 设置启动进程数量为 5 = 2 * CPU + 1:

1
2
# 其中app_module: wsgi文件, application实例(遵循 WSGI 协议)
gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app

此时通过pstree命令查看所有子进程, 输出如下:

1
2
3
4
5
6
7
\-+= 53933 bamboounuse -zsh
\-+= 60055 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app
|--- 60058 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app
|--- 60059 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app
|--- 60060 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app
|--- 60061 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app
\--- 60062 bamboounuse python gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app

使用 ab 命令测试: ulimit && ab -n 13000 -c 130 -r "http://127.0.0.1:5000/"

执行结果:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 24.126 seconds
Complete requests: 13000
Failed requests: 0
Total transferred: 2249000 bytes
HTML transferred: 169000 bytes
Requests per second: 538.84 [#/sec] (mean)
Time per request: 241.259 [ms] (mean)
Time per request: 1.856 [ms] (mean, across all concurrent requests)
Transfer rate: 91.03 [Kbytes/sec] received

由结果可知, LR(loadderRunner) 中的每秒事务数为538.84, LR 中的平均事务响应时间(ART, 运行每一个事务执行所用的时间, 用于分析系统运行期间性能)为241.259ms, 注意响应时间和运行时间的差别, 更加详细的信息见 AB 命令.

多线程

通过设置 thread, 允许一个 worker 进程中拥有多个线程, 其中线程数目一般也和 CPU 核数相关, 建议数量也是5 = 2 * CPU + 1, 但是这起始不是适用于所有环境, 比如对于存在大量长时间处理请求, 可以加大进程的数量以能够在同一个时间点接收更多的请求.

同上面一节, gthread 模式下的 gunicorn 命令如下:

1
gunicorn -b 0.0.0.0:5000 --workers=5 --log-level DEBUG --worker-class gthread --threads=5 main:app

ab 命令ab -n 13000 -c 130 -r "http://127.0.0.1:5000/"的输出如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 16.031 seconds
Complete requests: 13000
Failed requests: 0
Total transferred: 2249000 bytes
HTML transferred: 169000 bytes
Requests per second: 810.94 [#/sec] (mean)
Time per request: 160.307 [ms] (mean)
Time per request: 1.233 [ms] (mean, across all concurrent requests)
Transfer rate: 137.00 [Kbytes/sec] received

对比 3.1 节的多进程同步模式, 性能还是有一点点的提升, 其中每秒事务数538.84提升为810.94, 平均事务响应事件从241.59ms降为160.307ms.

那么多线程的优势在哪里呢? 在 IO 密集型请求中, 多线程的优势就会有一个明显的提升, 具体见下面章节的说明.

协程

通过设置gevent, 基于 Python 的高并发协程库, 实现更加高效的响应处理.

1
gunicorn -b 0.0.0.0:5000 --workers=5 --log-level DEBUG --worker-class gevent main:app

ab 命令ab -n 13000 -c 130 -r "http://127.0.0.1:5000/"的输出如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 16.905 seconds
Complete requests: 13000
Failed requests: 0
Total transferred: 2249000 bytes
HTML transferred: 169000 bytes
Requests per second: 769.01 [#/sec] (mean)
Time per request: 169.048 [ms] (mean)
Time per request: 1.300 [ms] (mean, across all concurrent requests)
Transfer rate: 129.92 [Kbytes/sec] received

可以看出, 在 CPU 密集型 API 请求(每一个请求内部没有大的 IO 事件阻塞发生)情况下, 协程和多线程的性能是类似的, 但都稍微比多进程更加快捷.

IO 密集

3.1 ~ 3.3分别讲解了 worker 三种工作模式下的启动命令, ab 测试结果, 下面我们需要再测试一下这三种情况在 IO 密集请求中的处理效率有什么区别.

首先, 让我们测试多进程同步模式, gunicorn 服务启动命令同上:

1
gunicorn -b 0.0.0.0:5000 --workers=5 --worker-class sync main:app

但是, ab 命令发生了些许变化: ab -n 600 -c 130 -r "http://127.0.0.1:5000/sleep", 此时测试结果如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 24.762 seconds
Complete requests: 600
Failed requests: 0
Total transferred: 102000 bytes
HTML transferred: 6000 bytes
Requests per second: 24.23 [#/sec] (mean)
Time per request: 5365.135 [ms] (mean)
Time per request: 41.270 [ms] (mean, across all concurrent requests)
Transfer rate: 4.02 [Kbytes/sec] received

其次, 多线程模式下, gunicorn 服务器启动命令同 3.2 节:

1
gunicorn -b 0.0.0.0:5000 --workers=5 --log-level DEBUG --worker-class gthread --threads=5 main:app

此时 ab 命令: ab -n 600 -c 130 -r "http://127.0.0.1:5000/sleep"的测试结果如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 7.110 seconds
Complete requests: 600
Failed requests: 0
Total transferred: 102000 bytes
HTML transferred: 6000 bytes
Requests per second: 84.38 [#/sec] (mean)
Time per request: 1540.589 [ms] (mean)
Time per request: 11.851 [ms] (mean, across all concurrent requests)
Transfer rate: 14.01 [Kbytes/sec] received

最后, 协程模式下, gunicorn 服务器启动命令如同 3.3 节:

1
gunicorn -b 0.0.0.0:5000 --workers=5 --log-level DEBUG --worker-class gevent main:app

此时 ab 命令:ab -n 900 -c 130 -r "http://127.0.0.1:5000/sleep"的测试结果如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 1.862 seconds
Complete requests: 900
Failed requests: 0
Total transferred: 153000 bytes
HTML transferred: 9000 bytes
Requests per second: 483.29 [#/sec] (mean)
Time per request: 268.988 [ms] (mean)
Time per request: 2.069 [ms] (mean, across all concurrent requests)
Transfer rate: 80.23 [Kbytes/sec] received

注意, 此时 ab 压测总请求为 900, 但是其总测试时间仅仅只有 600 请求的多进程同步的 1/10, 只有 600 请求的多线程的1/4, 对比三者就可以明显的体现这三者的优缺点.

最后的最后, 如果在 IO 密集型请求中, 我如果无限加大单个 worker 的线程并发数, 会不会提升其请求的处理效率呢?

1
gunicorn -b 0.0.0.0:5000 --workers=5 --log-level DEBUG --worker-class gthread --threads=50 main:app

此时 ab 命令:ab -n 900 -c 130 -r "http://127.0.0.1:5000/sleep"的测试结果如下:

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      130
Time taken for tests: 2.024 seconds
Complete requests: 900
Failed requests: 0
Total transferred: 153000 bytes
HTML transferred: 9000 bytes
Requests per second: 444.72 [#/sec] (mean)
Time per request: 292.318 [ms] (mean)
Time per request: 2.249 [ms] (mean, across all concurrent requests)
Transfer rate: 73.83 [Kbytes/sec] received

从输出结果可以看出来, 加大线程的数量极大的提升了请求的处理效率, 性能接近于使用协程方式, 只不过前者可能消耗的资源更多.

参考