先回答核心问题
但 Gunicorn 使用了协程(通过 gevent/eventlet worker)来实现高并发,这背后的原理非常值得深挖。
Gunicorn 处理的是
HTTP 请求 → 启动 Worker 进程/协程 → 调用 WSGI 应用处理请求 → 返回响应
不需要"连接池"——每个 HTTP 请求都是独立无状态的
连接池处理的是
数据库/Redis 连接 → 预建立一批长连接 → 复用这些昂贵的连接 → 避免频繁握手
TCP 握手 + DB 认证成本高,所以需要池化复用
Gunicorn 整体架构
Gunicorn 采用 Master-Worker 模型,Master 进程负责管理,Worker 进程负责处理请求。
监控 + 信号处理 + 管理 Worker
处理请求
处理请求
处理请求
处理请求
绑定在 0.0.0.0:8000
转发 HTTP 请求
SO_REUSEPORT 或 pre-fork),竞争 accept() 新连接,谁先 accept 谁处理。
四种 Worker 类型对比
sync — 同步阻塞 Worker 默认模式
一个 Worker 进程同一时刻只处理一个请求。如果请求在等 DB 查询,整个进程就阻塞在那里。
# sync worker 的行为模型(伪代码)
while True:
conn = socket.accept() # 阻塞等待连接
request = conn.recv() # 阻塞读取
response = wsgi_app(request) # 阻塞调用应用(含 DB IO)
conn.send(response) # 阻塞发送
conn.close()
不适合:大量数据库查询、HTTP 外调、文件 IO 等 IO 密集型场景。
gthread — 多线程 Worker IO 中等场景
每个 Worker 进程内启动多个线程,由于 Python GIL 存在,同一时刻只有一个线程执行 Python 字节码,但 IO 等待时可以切换线程。
# gthread 核心逻辑
# --threads N 控制每个 Worker 的线程数
class ThreadWorker:
def __init__(self, threads=10):
self.pool = ThreadPoolExecutor(max_workers=threads)
def handle(self, conn):
self.pool.submit(self._process, conn)
def _process(self, conn):
request = conn.recv()
response = wsgi_app(request) # 线程阻塞,但其他线程可运行
conn.send(response)
gevent / eventlet — 协程 Worker IO 密集型首选
这是协程模型的核心。gevent 通过 Monkey Patching 将标准库的阻塞 IO 替换为非阻塞版本,配合 Greenlet(微线程)实现协程调度。
Monkey Patching 原理
import gevent.monkey
gevent.monkey.patch_all() # 关键!在导入其他模块前调用
# patch 后,以下标准库函数被替换为非阻塞版本:
# socket.recv() → gevent 版本(遇到阻塞会切换协程)
# time.sleep() → gevent 版本(让出控制权给事件循环)
# threading.Lock → gevent 版本(避免死锁)
协程切换原理
from gevent import spawn, sleep
from gevent.pool import Pool
pool = Pool(1000) # 最多 1000 个并发协程
def handle_request(conn):
data = conn.recv(4096) # ← 此处 IO 阻塞时,自动切换到其他协程!
result = db.query(sql) # ← 数据库等待时,再次切换
conn.send(result)
while True:
conn = server.accept()
pool.spawn(handle_request, conn) # 创建一个轻量级协程
| Worker 类型 | 并发机制 | 单 Worker 并发数 | 适用场景 | 内存开销 |
|---|---|---|---|---|
sync |
无(阻塞) | 1 | CPU 密集、无 IO | 最低 |
gthread |
线程池 | = threads 参数 | IO 中等场景 | 中 |
gevent |
协程(Greenlet + libev) | 理论无上限(实际 ~1000) | IO 密集型 | 低 |
eventlet |
协程(Greenlet + libevent) | 理论无上限 | IO 密集型 | 低 |
协程核心原理:从阻塞到切换
传统阻塞模型 vs 协程模型
传统阻塞 IO(sync worker)
协程模型(gevent worker)
协程调度的完整生命周期
请求到达,事件循环创建 Greenlet
gevent 的 Hub(事件循环)通过 libev 的 epoll/kqueue 监听文件描述符,新连接到来时创建一个轻量级协程(Greenlet,几 KB 栈空间)
协程执行,直到遇到 IO 阻塞点
Greenlet 运行 WSGI 应用代码,执行到 socket.recv() / db.query() 等被 Monkey Patch 替换的 IO 调用时,gevent 检测到"这个 IO 不会立即完成"
IO 等待注册到事件循环,协程挂起
将 "fd 可读/可写" 事件注册到 epoll,保存当前协程的栈帧(调用栈、局部变量)到内存,主动让出 CPU 给 Hub
Hub 调度下一个可运行的协程
Hub 通过 epoll_wait() 等待任意事件就绪,从等待队列中取出可运行的协程,切换栈帧恢复执行——这整个过程发生在 同一个线程/进程内,无需线程切换,无需内核态切换
IO 完成,挂起的协程恢复运行
epoll 通知 IO 事件就绪,Hub 将对应协程放回运行队列,下次调度时从挂起点继续执行,就像什么都没发生一样
连接池是什么,以及为何和 Gunicorn 是两个层
连接池状态示意(10 个连接槽)
绿色=空闲可复用,红色=已被某请求借用,灰色=请求排队等待
# SQLAlchemy 连接池配置示例
from sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@localhost/db",
pool_size=10, # 池中维持的连接数
max_overflow=5, # 超出 pool_size 时最多再建 5 个
pool_timeout=30, # 等待连接超时时间(秒)
pool_recycle=3600, # 连接超过 1 小时自动重建(防 DB 断连)
)
层次关系图:Gunicorn 与连接池各在哪一层
反向代理 / 负载均衡
HTTP Server,管理 Worker 进程/协程
WSGI 应用代码
连接池管理层 ★
连接池管理层 ★
★ 连接池存在于应用层,不在 Gunicorn 层
gevent 与连接池配合时的关键注意点
错误做法
- gevent.monkey.patch_all() 调用太晚(在导入 psycopg2 之后)
- 使用不支持协程的连接池(如 pgbouncer 的 session 模式混用)
- 连接池 pool_size 设置过小,大量协程排队等待连接
- 忘记配置 pool_recycle,DB 空闲断连后报错
正确做法
- 在程序入口最开始调用
gevent.monkey.patch_all() - SQLAlchemy 自动感知 gevent,使用协程安全队列
- pool_size ≈ Worker 数 × 协程并发数 / DB 连接上限
- 推荐使用 NullPool(每次请求建连)或 gevent 专属连接池
# Gunicorn + gevent + SQLAlchemy 最佳实践
# gunicorn.conf.py
worker_class = "gevent"
workers = 4 # CPU 核数
worker_connections = 1000 # 每个 worker 最大并发协程数
# app.py - 必须在最开头
from gevent import monkey
monkey.patch_all() # ← 第一行!否则协程无法接管 socket IO
from sqlalchemy import create_engine
engine = create_engine(
DATABASE_URL,
pool_size=20, # 4 workers × 5 并发 DB 操作
max_overflow=10,
pool_pre_ping=True, # 借用前检测连接是否存活
)
补充:HTTP 层的"连接复用"(Keep-Alive)
HTTP/1.1 的 Keep-Alive 特性允许同一个 TCP 连接复用发送多个请求,这在一定程度上类似连接池的思想,但机制不同:
HTTP Keep-Alive
客户端复用已建立的 TCP 连接发送多个请求,避免 TCP 三次握手开销。Gunicorn 通过 --keepalive 参数配置超时时间(默认 2 秒)。
本质:客户端-服务端之间的 TCP 连接复用
数据库连接池
服务端(应用)预先与数据库建立一批持久连接,请求进来时从池中借用一个,用完放回,避免每次请求都要 TCP 握手 + DB 认证。
本质:应用-数据库之间的 TCP 连接复用
总结:清晰的层次划分
| 技术 | 所在层次 | 解决的问题 | 核心机制 |
|---|---|---|---|
| Gunicorn sync | HTTP 服务器层 | 请求并发数(1个Worker=1请求) | 多进程 fork |
| Gunicorn gevent | HTTP 服务器层 | IO 密集型高并发(1个Worker=N请求) | Greenlet + epoll + Monkey Patch |
| HTTP Keep-Alive | HTTP 协议层 | 客户端 → 服务端 TCP 连接复用 | Connection: keep-alive 头 |
| SQLAlchemy 连接池 | 应用 ORM 层 | 服务端 → 数据库 TCP 连接复用 | 预建连接 + 借用/归还队列 |
| pgbouncer | 数据库代理层 | 大量应用实例共享少量 DB 连接 | 中间代理 + 事务级连接池 |