workers值设为2×CPU核心数+1只是起点,不是答案:CPU密集型宜近核数,I/O密集型重单worker并发能力;应从$(nproc)+1起步压测,配合--max-requests防泄漏,并验证worker-class是否生效。

workers值设为2×CPU核心数+1只是起点,不是答案
很多人直接套用--workers=2*$(nproc)+1,结果压测时QPS不升反降。这是因为Gunicorn的worker数不是“越多越好”,而是要匹配应用的真实负载特征:纯CPU密集型(如图像缩放、数值计算)适合接近$(nproc);而绝大多数Django项目是I/O密集型(查DB、调外部API、读文件),此时单个worker的并发能力比数量更重要。
实操建议:
- 先用
lscpu | grep "^CPU(s)"确认物理核心数,不是逻辑线程数 - 同步worker(
--worker-class=sync)起步值用$(nproc) + 1,比如4核机器从5开始 - 搭配
--max-requests=1000 --max-requests-jitter=100防内存泄漏,避免worker越跑越慢 - 压测时用
ps aux | grep gunicorn | grep -v grep | wc -l看真实活跃进程数,不是配置值
用gevent时--workers和--worker-connections必须配合调
换成gevent后,--workers的意义变了:它只控制预加载的协程调度器数量,真正撑并发的是--worker-connections。比如--workers=3 --worker-connections=1000,理论并发是3000,但前提是你的数据库连接池、Redis客户端、HTTP会话管理都支持同等规模的异步等待。
容易踩的坑:
立即学习“Python免费学习笔记(深入)”;
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
- 没在入口文件顶部加
from gevent import monkey; monkey.patch_all(),导致threading.local或某些C扩展模块行为异常 - 用了
psycopg2原版驱动,但geventpatch 不兼容,出现TimeoutError: Operation not permitted -
--worker-connections设太高(如>2000),但系统ulimit -n没调,gunicorn启动就报Too many open files
看到503 Service Unavailable别急着加worker
gunicorn access日志里频繁出现503 Service Unavailable,90%不是并发不够,而是worker连接池已满——说明请求处理太慢,或者连接没释放。这时候加--workers只会让问题更隐蔽:更多worker争抢数据库连接、更多线程挤占内存、上下文切换开销陡增。
优先检查这些点:
- 数据库连接池是否配小了?Django的
CONN_MAX_AGE设为0或过低,每次请求都新建连接 - 是否有未关闭的
requests.Session()或urllib3.PoolManager?它们会卡住连接 - 用
gunicorn --preload了吗?没preload时每个worker单独import Django,冷启动延迟高,容易触发超时 - 观察
top中CPU使用率:长期低于60%但QPS上不去,基本可判定是I/O阻塞,不是CPU瓶颈
--threads混用时,--workers × --threads不能盲目突破2×CPU+1
用--worker-class=gthread开启线程模式后,有人会把--workers设成2、--threads设成16,以为能轻松达到32并发。但Python的GIL会让多线程在CPU密集场景收益极低,反而因锁竞争拖慢响应;而在I/O密集场景,线程调度开销又比gevent协程高得多。
真实约束条件:
- 总并发上限仍受制于系统资源:每线程默认栈空间约8MB,16线程×2 worker = 256MB内存仅用于线程栈
- Django ORM不是线程安全的,多个线程共享同一个
connection对象会导致DatabaseError: connection already closed - 除非你明确知道所有第三方库(含数据库驱动、缓存客户端、日志handler)都支持线程安全,否则别用
gthread
--workers=5起步,盯着gunicorn.access.log里的503和502、ps aux里的活跃进程数、top里的CPU/内存曲线三者交叉验证——参数不是算出来的,是压出来的。最常被忽略的一点:改完配置后,一定要systemctl restart gunicorn并确认旧进程已退出,否则新配置根本没生效。

















