python中的works的工作原理
workers到底在做什么、为什么需要它、它和线程/协程的关系、进程如何分工、请求是怎么被分配的、怎么根据业务算出合适的 worker 数。
- 总体架构:ASGI + 事件循环 + 进程模型
• ASGI 应用(FastAPI、Starlette 等)是一个“异步应用对象”。
• Uvicorn 是 ASGI 服务器,实现了:网络 IO、HTTP 解析、事件循环(默认 uvloop)、把请求转成 ASGI 事件交给应用处理。
• Gunicorn 是一个“进程管理器”(prefork 模型,像 Nginx 的 master/worker)。它负责:
• 启动一个 master(arbiter) 进程;
• fork 出 N 个 worker 子进程(-w / --workers 就是这个 N);
• 监听端口、接管信号、健康检查、崩溃拉起、优雅重载等;
• 每个 worker 内部再运行一个 Uvicorn 事件循环(-k uvicorn.workers.UvicornWorker)。
你也可以直接用 uvicorn --workers N … 启动多进程,但生产更推荐让 Gunicorn 负责进程管理,Uvicorn 只负责协议栈和事件循环。
- 为什么是“多进程”而不是“多线程”
• Python GIL 限制了同一进程内同时执行字节码的线程数量。
• 多个 worker = 多个独立 Python 进程,每个进程有自己的 GIL,可以真正用满多核。
• 一个 worker 内部用的是 异步事件循环(asyncio/uvloop):
• 一个事件循环线程就能处理大量并发连接(网络 IO 不阻塞时);
• 但如果你做 CPU 重活(加密、压缩、ML 推理),会阻塞这个事件循环,导致单 worker 吞吐下降。
结论:IO 密集用“少量进程 + 每进程大量协程”,CPU 密集则“多进程分摊重活”。
- 请求是怎么分发给 workers 的?
有两种常见 socket 接收模式(Gunicorn 控):
1. master 接收:master 把新连接 round-robin 分发给各 worker(传统、可控)。
2. SO_REUSEPORT:每个 worker 都 bind 同一个端口,内核把新连接直接分配给某个 worker(减少 master 抖动,扩展性更好)。
无论哪种,每个 worker 都维护自己的事件循环,在 loop 里把“连接 → HTTP 报文 → ASGI 事件 → app 调用 → 响应”跑一遍。
Keep-Alive 的连接会在同一 worker 内复用,不会跨 worker。
- worker、线程、协程的关系与区别
• worker(进程):–workers N 创建 N 个进程;最粗粒度的并行,绕过 GIL。
• 线程(gthread/gevent 线程池):某些 worker-class 有 --threads;在 Python 中受 GIL 影响,适合 IO 混合或少量阻塞。
• 协程(asyncio 任务):Uvicorn 的核心;单线程事件循环上创建成千上万 task,靠 IO 切换实现高并发。前提:应用函数必须是异步且不做阻塞计算/阻塞 IO。
在 uvicorn.workers.UvicornWorker 下,建议不要再叠线程;如果你必须调用阻塞库,用 asyncio.to_thread() 或 loop.run_in_executor() 把它丢给线程/进程池。
-
不同 worker-class 的差异(Gunicorn 视角)
• sync:同步阻塞,简单但并发差。
• gthread:同步 + 线程池;IO 混合场景可用,但有 GIL。
• gevent/eventlet:协作式并发(猴补丁),生态偏 WSGI。
• uvicorn.workers.UvicornWorker:ASGI 原生异步 + uvloop/httptools,FastAPI 首选。
• UvicornH11Worker:用纯 Python h11 解析 HTTP(兼容性更广,吞吐略低)。 -
–workers 如何影响性能(吞吐、延迟、抖动)
• 吞吐:更多 worker = 更多 CPU 并行度;但过多会引发上下文切换、缓存失效、内存压力,反而掉速。
• 延迟:worker 太少,排队时间(队列等待)变长;太多,进程争抢导致抖动上升。
• 内存:每个 worker 都要加载你的应用与依赖(甚至模型),内存线性增长;可以用 --preload(见下一节)配合写时复制节省一部分内存,但别指望对“会频繁写”的对象有奇效。 -
–preload/preload_app 的写时复制(COW)原理
• 开启 preload 时,Gunicorn 在 master 里先导入 app,再 fork 出 workers。
• Linux 的 Copy-On-Write:fork 初始时不复制内存页,父子共享;当某个进程修改某页时才复制那一页。
• 对于只读的大对象(如加载到CPU 内存的只读表、词典、路由树),多个 worker 可以共享大量物理页,节省内存。
• 但注意:
• 若对象在 worker 中被写入/修改,马上失去共享优势;
• GPU 显存不参与 COW,每个 worker 都要各自占用显存(重推/模型场景要谨慎)。 -
如何“算”出合适的 worker 数(不仅仅是 2×CPU+1)
Gunicorn 的经验公式:workers = 2 × CPU核数 + 1。但更科学的做法是结合业务特征和性能目标:
• 设每个请求的平均 服务时间(S)(秒),单 worker 的有效并发容量(Cw)(async 任务上限 / 实测并发),目标 RPS。
• 用粗略的 Little’s Law 直觉:并发 ≈ 吞吐 × 响应时间。
若单 worker 在稳定状态下能支撑 Cw ≈ RPS_per_worker × S,
则需要的 worker 近似为:
workers ≈ (目标RPS × S) / Cw(向上取整,再预留 20–30% 余量)。
例子(IO 密集 API):
• 目标 3,000 RPS,平均响应时间 S = 50ms = 0.05s;
• 单 worker 经压测能稳定跑 Cw ≈ 400 并发(事件循环+DB IO);
• 需要 workers ≈ (3000 × 0.05) / 400 = 0.375 → 取 1–2;
但考虑 p95 延迟与抖动、滚动升级、安全余量,往往配 8–12 个更稳(8 核)。
例子(CPU 密集):
• 单请求 CPU 计算 100ms,单 worker 几乎难并发(会阻塞事件循环),
一个 worker 的“有效并发”≈ 1–2;
8 核机器就接近 8–12 个 worker(不要太多,避免争抢)。
-
关键参数对照与调优建议
• --workers N:进程数(最重要)。
• -k uvicorn.workers.UvicornWorker:ASGI 模式。
• --timeout:单请求最大处理时长(默认 30s,后端慢时需要上调)。
• --graceful-timeout:优雅停止给的收尾时间(处理完在途请求)。
• --keep-alive:HTTP Keep-Alive 空闲秒数(默认 2);API 网关/浏览器前置时可适度调高。
• --max-requests / --max-requests-jitter:worker 处理一定请求数后重启,缓解内存碎片/泄漏。
• --preload:减少启动时的重复加载、配合 COW 节省内存(对 GPU 无效)。
• 系统层:ulimit -n(文件描述符)、tcp_tw_reuse、backlog、容器 CPU/内存限制等。 -
8 核机器的几种“可解释”推荐
A. 典型 FastAPI IO-密集(DB/Redis/外部 API)
gunicorn app.main:app
-w 12
-k uvicorn.workers.UvicornWorker
-b 0.0.0.0:8000
–timeout 120 --graceful-timeout 30
–keep-alive 10
–max-requests 5000 --max-requests-jitter 500
–preload
解释:12 个进程让 CPU 有余量处理突发;keep-alive 提高连接复用;max-requests 周期性重启缓解内存增长;preload 降低 RSS。
B. 计算/推理偏重(CPU-bound)
gunicorn app.main:app
-w 8
-k uvicorn.workers.UvicornWorker
-b 0.0.0.0:8000
–timeout 300 --graceful-timeout 60
–max-requests 1000 --max-requests-jitter 200
解释:基本与核数等同,避免过多进程争用;超时上调;周期性重启避免长时间累积的碎片。
C. 单机多实例 + 负载均衡
• 每实例 -w 6~8,跑 2–3 个实例,由前面的 Nginx/Envoy/ALB 做分流;
• 实际上更稳、更易滚动升级(蓝绿/金丝雀)。
-
阻塞点与“协程失效”的典型坑
• 在异步端点里调用阻塞库(Requests、pymysql 同步模式、耗时的本地函数)会卡住事件循环 → 单 worker 吞吐暴跌;
对策:改用异步库(httpx[async]、asyncpg、数据库驱动的 async 版本)或 asyncio.to_thread()/进程池。
• 大 JSON 序列化、压缩、加解密、图像编解码等 CPU 重活——放执行器/子进程。
• 大文件上传/下载:用 StreamingResponse,别把整块文件搬进内存。 -
生命周期与优雅重启
• Lifespan(ASGI startup/shutdown)在每个 worker 启动/关闭时触发:连接池、缓存、模型、线程池都在这里初始化/释放。
• 信号:
• HUP/USR2:平滑重载;
• TERM:优雅终止(先停止接新连接,等待在途完成,超时再杀)。
• 配合 --graceful-timeout 与负载均衡的逐个摘流,可以实现零停机发布。
⸻
一句话总括
• --workers = 进程并行度,是“绕开 GIL、用满多核”的核心手段;
• 单 worker = 事件循环 + 海量协程,擅长 IO 并发,怕阻塞;
• “合适的 workers” 取决于:CPU/IO 比例、单请求耗时、目标 RPS、内存预算与稳定性约束;
• 2×CPU+1 是保守默认,真正的最佳值要压测出来。