很多用 Python 做后端服务的朋友,应该都对 Gunicorn 不陌生。它是比 uWSGI 更轻量、更容易上手的 WSGI 服务器,很多框架默认教程里都会带上它。不过一提到高并发,Gunicorn 往往被吐槽“很菜”,其实大多数时候不是它天生不行,而是我们没搞懂它的工作模式,也没做针对性调整。今天我就从日常踩坑的角度,聊聊它到底卡在哪里,以及怎么改才能让它跑得更顺畅。
一、Gunicorn 到底是一个怎样的角色
可以把 Gunicorn 想象成一家餐厅里的传菜员。你写的 Flask 或 Django 应用是后厨,负责真正处理各类“食材”(也就是数据),但外面成千上万的客人(用户请求)不可能直接冲进后厨。Gunicorn 就站在门口,把客人的菜单一张一张递进去,再把做好的菜端出来。如果传菜员只有一个人,而且一次只能端一道菜,那客人多了自然就堵在门口。
Gunicorn 默认采用的是一种“同步 Worker”模式。每个 Worker 进程在任何一个时刻,只能处理一个请求。处理完一个,才能去接下一个。这种模式简单又可靠,但在现代 Web 服务里,一个请求往往要经历很多 I/O 操作,比如查询数据库、读文件、调用第三方接口等,整个过程有一大半时间是在“等”,等的时候 Worker 就闲着,非常浪费。
二、高并发下 Gunicorn 通常卡在哪里
2.1 同步 Worker 的“一个人一座椅”
想象银行柜台只有一个窗口,而且窗口工作人员只服务当前客户,客户办理业务需要 10 分钟,后面的所有人就只能等着。Gunicorn 的同步 Worker 就是这种模型。哪怕你的机器有 32 个 CPU 核心,如果你只开了 2 个 Worker,那么同一时间也就只能处理 2 个请求,其余请求全部排队。
为了演示,我们写一个最简单的 Flask 接口。这个接口没有真实业务,只是模拟一次耗时 100 毫秒的数据库查询。
# 文件名:app.py
# 技术栈:Python 3 + Flask + Gunicorn
from flask import Flask
import time
app = Flask(__name__)
@app.route("/")
def index():
# 模拟数据库查询需要100毫秒才能返回
time.sleep(0.1)
return "OK"
如果用默认同步模式启动 4 个 Worker,然后同时来 100 个请求,每个请求都得轮流进到某一位 Worker 手里。假设每个请求处理时间是 0.1 秒,那么第 100 个请求的等待时间可能接近 2.5 秒,用户自然感觉到慢。
2.2 Worker 一多,内存先顶不住
有人会说,既然一个 Worker 同时只能处理一个请求,那就多开几个 Worker 呗。理论上没错,可每个 Worker 都是一个独立的进程,都需要加载一份完整的 Python 解释器、应用代码和依赖库。一个包含 Flask、ORM、Redis 客户端的应用,每个 Worker 轻松吃掉一两百兆内存。如果为了撑住高并发开到 32 个 Worker,光内存就是好几个 GB,还没开始处理请求,系统就已经开始用 swap 了,速度反而更慢。
2.3 系统内核的“隐形天花板”
就算你内存够大,Worker 数量也合理,操作系统还会给你使绊子。每个网络连接都需要一个文件描述符,而 Linux 默认对单进程可打开的文件数限制通常只有 1024。也就是说,同时建立的 TCP 连接一旦超过这个数,新的连接可能就被拒绝。除此之外,TCP 的 backlog 队列长度、可用端口范围,都会变成高并发下的瓶颈。
三、针对性的优化方案
3.1 方案一:把 Worker 类型从同步改成 gevent
前面说的瓶颈,大部分都可以通过切换 Worker 类型来解决。Gunicorn 本身支持很多 Worker 类型,其中最常用的两个是 gthread 和 gevent。这里我重点说 gevent,因为它的收益最明显。
gevent 是一个基于协程的 Python 库。协程是一种比线程更轻量的并发单元,切换开销非常小。Gunicorn 的 gevent Worker 会让每个进程同时维护大量协程。当一个请求在等待 I/O 时,协程会自动让出 CPU,去处理另一个请求。等 I/O 完成,再回来继续之前的处理。这样一来,一个 Worker 就能同时支撑成百上千个请求了。
你可以把它想象成一个咖啡店店员,同时为 10 位顾客下单,A 点完单,还不等钱拿出来,店员已经记下 B 的要求,再回头接 A 的钱。虽然店员还是同一个,但效率高了很多。
gevent 需要单独安装,然后在配置里指定。
# 技术栈:Shell 命令
# 安装 gevent 库
pip install gevent
然后创建一个配置文件,让 Gunicorn 使用 gevent。
# 文件名:gunicorn.conf.py
# 技术栈:Gunicorn 配置文件(Python 语法)
import multiprocessing
# 监听端口,通常部署时由 Nginx 转发到这里
bind = "0.0.0.0:8000"
# Worker 进程数。gevent 模式不需要太多,因为单进程并发能力强。
# 这里取 CPU 核心数的两倍,你也可以根据实测调小或调大。
workers = multiprocessing.cpu_count() * 2
# 使用 gevent 异步 Worker
worker_class = "gevent"
# 每个 Worker 最多同时处理的连接数,也就是同时能扛多少请求
worker_connections = 1000
# 一个请求超过 30 秒没处理完,就会被强制结束
timeout = 30
接着启动:
# 技术栈:Shell 命令
# 启动 Gunicorn,使用上面的配置文件
gunicorn -c gunicorn.conf.py app:app
需要注意的是,gevent 依赖 monkey patch 来把标准库里那些阻塞式的 I/O 操作替换成协程版本。所以最好在应用入口文件里做一次全局补丁。
# 文件名:wsgi.py
# 技术栈:Python 3 + Gunicorn + gevent
from gevent import monkey
monkey.patch_all()
# 导入真正的 Flask 应用
from app import app
这样之后,原来那些 socket、time.sleep、requests 等操作就都变成非阻塞的了。
3.2 方案二:如果你不想用协程,可以试试 gthread
gevent 虽然好,但某些场景下单线程内跑很多协程,容易出现奇怪的兼容问题。如果你不想引入额外的依赖,也可以使用 Gunicorn 自带的 gthread Worker。gthread 使用线程池来并发处理请求,同样能降低同步阻塞的浪费。
配置例子如下:
# 文件名:gthread.conf.py
# 技术栈:Gunicorn 配置文件(Python 语法)
bind = "0.0.0.0:8000"
# 4个进程,每个进程开8个线程,理论并发能力是32
workers = 4
threads = 8
# 使用 gthread Worker,也就是线程模式
worker_class = "gthread"
注意,Python 的全局解释锁(GIL)会让线程无法真正并行执行 CPU 计算,所以 gthread 最适合 I/O 密集型的接口。如果你的接口大量消耗 CPU,gthread 提升有限,反而进程多开一些更管用。
3.3 方案三:进程数怎么定才合理
很多人优化 Gunicorn 的第一反应就是把 workers 调到很大,其实这不是万能的。对于同步 Worker,业界常说一个公式:两倍 CPU 核心数再加一。比如 4 核机器,设置为 9 个 Worker 比较好。对于 gevent,Worker 没必要那么多,每个 Worker 能处理上千个连接,2 到 4 个通常就够。如果 Worker 太多,进程间频繁切换会带来额外开销,内存也压不住。
3.4 方案四:前面加一个 Nginx 做负载均衡
Gunicorn 毕竟是一个应用服务器,不适合直接暴露公网。更常见的架构是 Nginx 当大门,Gunicorn 躲在后面。Nginx 可以分发请求到多个 Gunicorn 实例,还能缓存静态资源、处理 HTTPS、配置超时和重试规则。
比如你想让两个 Gunicorn 实例来分担压力:
# 技术栈:Shell 命令
# 在 8001 和 8002 端口分别启动一个 Gunicorn 实例
gunicorn -b 127.0.0.1:8001 -w 4 -k gevent app:app &
gunicorn -b 127.0.0.1:8002 -w 4 -k gevent app:app &
然后在 Nginx 里配置一个 upstream 组,把请求均衡地分给这两个实例:
# 文件名:myapp.conf
# 技术栈:Nginx 配置文件
upstream myapp {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
}
server {
listen 80;
server_name example.com;
location / {
# 把请求转发给 Gunicorn 实例组
proxy_pass http://myapp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这样一来,单台机器的处理能力就被横向放大了。如果以后流量继续涨,只需要在 upstream 里多加几行 server 127.0.0.1:8003;,然后把新 Gunicorn 实例启动就好。
3.5 方案五:系统参数也要跟着调
有时候不是 Gunicorn 不给力,而是操作系统默认参数太保守。我们可以把文件描述符上限调高,让进程能同时维护更多连接。
# 技术栈:Shell 命令
# 临时把文件描述符限制改成 102400
ulimit -n 102400
还可以调整 TCP 有关的系统参数:
# 技术栈:Shell 命令
# 增大内核允许的最大文件数
sysctl -w fs.file-max=100000
# 端口不足时,尝试扩大本地端口范围
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# 增大 TCP 三次握手等待队列长度
sysctl -w net.core.somaxconn=4096
这些参数在不同环境里会有差异,改的时候要小心,毕竟系统级参数会影响整台机器上的所有服务。
四、优化效果实测
说了那么多,不如直接跑一次压测。还是用刚才那个 sleep 0.1 秒的接口。先启动同步模式:
# 技术栈:Shell 命令
# 4个同步 Worker 启动
gunicorn -b 127.0.0.1:8000 -w 4 app:app &
然后用 ab 工具模拟 500 个请求,并发数 50:
# 技术栈:Shell 命令
# 压测命令:500个请求,50并发
ab -n 500 -c 50 http://127.0.0.1:8000/
你会看到吞吐量大概在几十到上百 QPS,平均响应时间也很高。这时候把 Worker 切到 gevent,启动命令换成:
# 技术栈:Shell 命令
# 4个 gevent Worker,每个允许 500 个并发连接
gunicorn -b 127.0.0.1:8000 -w 4 -k gevent --worker-connections 500 app:app &
再跑一遍同样的 ab 命令,你会发现吞吐量可能翻了几番,平均响应时间大幅下降。这就是从“一个柜台一个客户”变成了“一个柜台服务几百个客户”的差别。
五、应用场景、优缺点与注意事项
5.1 最适用的场景
Gunicorn 最擅长的是中小型 Python Web 服务,尤其是那种大量时间花在等待 I/O 上的接口。比如访问数据库、读写 Redis、调用外部 API 等。通过 gevent 或 gthread,可以让单个 Worker 在等待期间处理其他请求,把 CPU 的浪费降到最低。
5.2 Gunicorn 的优点和缺点
先说明一下,Gunicorn 的优点在于部署简单、配置灵活,和 Flask、Django 配合得非常好。它有多种 Worker 模式,可以适配不同业务。缺点也很明显:它本身不擅长处理静态文件,不适合作为最外层的 Web 服务器。对于 CPU 密集型任务,受制于 GIL,提升空间不大。另外,gevent 这种异步模型会对某些依赖原生 C 扩展的库产生兼容性问题,比如某些旧版数据库驱动,需要在选型时多测试。
5.3 使用中的几个关键注意事项
使用 gevent 时,monkey.patch_all() 必须在任何网络库导入之前调用,否则可能补丁不完整,导致某些阻塞没有被替换。Worker 数量不是越多越好,要根据内存和 CPU 负载动态调整。如果 Gunicorn 后面挂了数据库,别忘了同步调大数据库连接池上限,否则 Gunicorn 能并发处理了,数据库反而支撑不住。最后一定要加监控,把请求延迟、活跃连接数、Worker 状态和系统指标都记录下来,遇到问题才能快速定位。
六、总结
Gunicorn 在高并发下表现不佳,往往不是因为软件本身有多差,而是我们忽略了它的同步模型、内存开销和系统限制。通过切换 Worker 类型到 gevent 或 gthread,合理设置进程和连接数,在前端部署 Nginx 做负载均衡,再调整内核参数,大部分并发问题都能得到肉眼可见的改善。优化没有银弹,还是要结合自己的业务场景多压测、多观察。希望这篇文章能帮你让 Gunicorn 跑得更快、更稳。
评论
围绕“Gunicorn在高并发场景下的性能瓶颈分析及针对性的优化方案探讨”参与讨论