Flask默认多线程在高并发I/O场景下易退化为“假并发”,真正高并发需用gevent协程;monkey.patch_all()必须置于文件首行(早于所有import),否则patch失效;Gunicorn部署须配--worker-class gevent、--worker-connections、--timeout等参数,并与数据库连接池合理匹配。

Flask默认的多线程模式在真实并发场景下会迅速退化成“假并发”——线程数上去了,但I/O一卡全卡,CPU空转,错误率飙升。真正能扛住几百上千并发的,不是靠堆线程,而是用gevent把阻塞调用切走,让单个worker同时跑上千个协程。
为什么monkey.patch_all()必须放在最顶上?
这不是仪式感,是执行顺序硬约束。一旦任何模块(比如requests、urllib、甚至sqlalchemy)提前触发了标准库的socket或ssl导入,patch就失效了——后续所有协程调度都会掉回同步阻塞状态。
- 错误写法:
import requests写在monkey.patch_all()前面 → 协程被requests.get()彻底卡死 - 正确位置:在
app.py第一行(比from flask import Flask还早)写from gevent import monkey; monkey.patch_all() - 验证是否生效:启动后看日志里有没有
Monkey patched字样;压力测试时观察CPU是否长期低于30%而QPS却很高(说明没卡在I/O上)
Gunicorn + --worker-class gevent启动时的关键参数
只加--worker-class gevent远远不够,漏配--worker-connections或--timeout会导致协程池撑爆、请求直接502。
-
--worker-class gevent:必须显式指定,不能依赖默认值 -
--worker-connections 1000:每个gevent worker最多并发处理1000个协程,4核机器设800–1200较稳;设太高会OOM(协程虽轻,但每个仍占几KB栈) -
--timeout 30:防止某个协程因未patch库卡死,拖垮整个worker;建议设为业务最长I/O耗时的1.5倍 -
--workers $((2*$(nproc)+1)):进程数按CPU核心数算,别盲目堆worker——gevent本意是减少进程数,靠协程提并发
哪些操作会让gevent瞬间“失灵”?
协程调度依赖所有I/O路径都被patch。只要一个地方漏掉,整个worker就挂起,表现就是QPS断崖下跌、日志里大量Timeout但CPU很低。
立即学习“Python免费学习笔记(深入)”;
-
time.sleep(1):必须换成gevent.sleep(1),原生sleep会阻塞整个greenlet栈 - 原生
subprocess.Popen:未patch时会同步等待,改用gevent.subprocess或封装成异步调用 - 未patch的数据库驱动:如PyMySQL默认不兼容,要加
autocommit=True并确认已patch_all();SQLAlchemy需配pool_pre_ping=True防连接失效 - 第三方SDK(如阿里云OSS SDK):若内部用了原生
threading或socket,必须查文档确认是否支持gevent,否则得换SDK或加gevent.monkey.patch_thread(False)隔离
本地调试和线上部署的配置差异
很多人把本地WSGIServer那一套直接搬上线,结果Gunicorn压根不认spawn参数,或者日志错乱到无法排查。
- 本地开发:用
gevent.pywsgi.WSGIServer+spawn=100快速验证逻辑,但别压测——它没做生产级连接管理 - 线上部署:必须用
gunicorn,且禁用--reload(热重载会破坏monkey patch状态),日志统一走--access-logfile和--error-logfile - 关键区别:
WSGIServer的spawn是协程上限,gunicorn的--worker-connections才是实际生效的并发控制点,两者数值不等效
最容易被忽略的其实是数据库连接池和协程数的匹配关系——如果SQLAlchemy的pool_size设成10,但--worker-connections是1000,那990个协程会在连接池前排队,性能反而不如不加gevent。协程不是万能胶,它只是把“等”的时间腾出来干别的,前提是下游资源(DB、Redis、HTTP服务)也跟得上。


















