Gunicorn 是如何工作的
前一阵子被 HTTP 并发请求导致的竞争条件所困扰,以及自己在实现一个具有多个 worker 的 TCP Server 的时候受困于应到如何调度请求。
针对上面这个问题,我想出了两个解决方案:
- 设想 A:由一个调度线程负责收取请求数据,并将数据暂存于缓冲区,调度 Worker 处理数据,再将结果存暂存在另一个缓冲区等待主线程将结果写入 socket
- 模型 B:将 socket 直接分配给不同的线程处理(这是最传统的多线程模式)
针对上面两个设想,
- 设想 A:对于此设想,如果负载为计算密集型应用则相对可行,如果负载为 IO 密集型应用则毫无意义。而且为了保证线程安全还需要特别注意锁的问题。
- 模型 B:对于此模型,如果使用 Python 实现,要面对 GIL 的问题,常见的 Python 服务器多使用多进程来并行多任务,在多个进程中是如何处理 IO 问题?须知文件描述符并不会随着
fork()
被一并复制。
于是我决定研究一下我们最常用的 gunicorn
是怎么处理这个问题的。
Bootstrap
略过处理配置信息的过程,Gunicorn 的功能起始于 gunicorn.arbiter.Arbiter
,它通过调用 fork()
启动了多个 Worker,并管理这些 Worker 的声明周期。Gunicron 提供了多种不同的 Worker,本文中只分析最简单的 SyncWorker
。
SO_REUSEADDR
& SO_REUSEPORT
在启动一个 socket server 时,有一步 bind()
操作,把 socket 与监听地址、端口相绑定。在默认情况下,对同一个地址、端口绑定多个 socket 时会发生冲突,此时应当为 socket 设置参数 SO_REUSEADDR
和 SO_REUSEPORT
。
SO_REUSEADDR
这个选项是个比较常用。在默认情况下,假设进程 A 有一个 socket 监听 0.0.0.0:21
,进程 B 有另一个 socket 试图监听 192.168.1.10:21
,此时会发生错误 EADDRINUSE
。原因是 0.0.0.0
包含了全部的 IP 地址,系统会认为 192.168.1.10:21
已经有 socket 在监听,故返回错误。设置此参数后,因为 192.168.1.10
与 0.0.0.0
事实上不是同一个地址,系统允许第二个 socket 进行绑定。
SO_REUSEPORT
这个选项是最近(Linux Kernel 3.9+)才加入的。假如有两个进程 A 和 B 各有一个进程需要监听 0.0.0.0:21
,设置此参数后,系统会允许多个 socket 监听在同一个地址的同一个端口上。
需要注意的是,并非所有的操作系统都支持这个选项。Windows 只支持 SO_REUSEADDR
,其行为相当于 SO_REUSEADDR
与 SO_REUSEPORT
共同作用的结果,更多细节可查询 MSDN 中相关说明,BSD、Android、iOS/macOS 亦各不相同。
Gunicorn 中的应用
上文中说到,Gunicorn 会在启动是 fork()
生成多个 Worker。