设为2×cpu核心数不总是最优,因其忽略应用类型:cpu密集型宜近核心数,i/o密集型更依赖单worker并发能力(如gevent+worker-connections),盲目增加worker反致争抢与调度开销。

为什么 --workers 设为 CPU 核心数 × 2 并不总是最优?
很多人直接套用 Gunicorn 官方建议的 --workers 值(2 × $(nproc)),但在真实生产中,这常导致进程争抢、上下文切换开销上升,反而压低 QPS。关键要看应用类型:纯 CPU 密集型服务(如数值计算)适合接近 CPU 核心数的 worker 数;而多数 Web 应用是 I/O 密集型(数据库查询、HTTP 调用、文件读写),此时更依赖异步等待能力,单个 worker 的并发处理能力(由 --worker-connections 和事件模型决定)比 worker 数量更重要。
- 用
ps aux | grep gunicorn观察实际活跃 worker 进程数,而非仅看配置值 - 若
top中 CPU 使用率长期低于 60% 但 QPS 上不去,大概率是 I/O 阻塞或数据库连接池不足,不是 worker 不够 - 对 Flask/Django 等同步框架,
--workers建议从$(nproc) + 1起步,配合--max-requests=1000防内存泄漏,再压测调优
用 gevent 还是 eventlet?选错会拖垮吞吐
默认的 sync worker 在高并发下极易被阻塞,必须换异步 worker。但 gevent 和 eventlet 行为差异很大:gevent 依赖 monkey patch,若项目中用了未 patch 的标准库模块(如某些 C 扩展或 threading.local 不当使用),会导致静默失败或数据错乱;eventlet 对 patch 更宽容,但与某些数据库驱动(如较老版本的 psycopg2)存在兼容问题,出现 TimeoutError: Operation not permitted。
- 启用前必须完整 patch:对
gevent,在入口文件顶部加from gevent import monkey; monkey.patch_all() - 数据库连接必须交由对应异步驱动管理,例如用
psycopg2cffi或asyncpg(配合gunicorn --worker-class gevent --worker-connections 1000) - 压测时关注
gunicorn.access日志中的503 Service Unavailable——这往往意味着 worker 连接池已满,不是并发不够,而是响应太慢或连接未及时释放
--keep-alive 和 --timeout 的取值矛盾点在哪?
--keep-alive 控制 HTTP Keep-Alive 的秒数,值越大,复用 TCP 连接越多,减少握手开销;但若设得过大(如 > 30),在流量突增时旧连接占满 worker,新请求排队,反而增加 P99 延迟。--timeout 是 worker 处理单个请求的最长秒数,设得太小(如 30)会导致正常慢查询被粗暴杀掉,返回 502 Bad Gateway;设得太大(如 120)又会让卡死的 worker 长期占用资源。
- 典型平衡点:
--keep-alive=5(覆盖大部分 HTTP/1.1 客户端默认行为),--timeout=60(给 DB 查询和外部 API 留出缓冲,但不纵容长尾) - 若后端有大量
SELECT ... FOR UPDATE或调用第三方支付接口,可单独对相关路由加超时装饰器,而非全局拉高--timeout - 务必配
--graceful-timeout=30,确保 reload 时正在处理的请求能完成,避免Connection reset by peer
日志和监控缺失会让调优变成盲人摸象
没有指标支撑的参数调整等于猜。Gunicorn 自身只输出基础 access/error 日志,无法反映 worker 排队、请求堆积、协程阻塞等关键瓶颈。必须补上两层观测:
- 在
gunicorn.conf.py中启用accesslog = "/var/log/gunicorn/access.log",并用loglevel = "info",避免debug级别打爆磁盘 - 用
gunicorn --statsd-host=localhost:8125接入 StatsD,导出gunicorn.workers、gunicorn.request.duration、gunicorn.request.errors等指标到 Grafana - 特别注意
gunicorn.workers.idle指标持续为 0 ——说明所有 worker 都在忙,要么是并发真高,要么是某个下游(如 Redis timeout)把整个链路拖住了
真正卡住 QPS 的,往往不是 Gunicorn 参数本身,而是它背后那个没加连接池的 MySQL 客户端,或者忘了设 pool_pre_ping=True 的 SQLAlchemy 引擎。参数只是放大器,底座不稳,调得再细也没用。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











