Cybernated Complex

Try to keep complex in the cyberspace.


Gunicorn 是如何工作的

前一阵子被 HTTP 并发请求导致的竞争条件所困扰,以及自己在实现一个具有多个 worker 的 TCP Server 的时候受困于应到如何调度请求。

针对上面这个问题,我想出了两个解决方案:

  1. 设想 A:由一个调度线程负责收取请求数据,并将数据暂存于缓冲区,调度 Worker 处理数据,再将结果存暂存在另一个缓冲区等待主线程将结果写入 socket
  2. 模型 B:将 socket 直接分配给不同的线程处理(这是最传统的多线程模式)

针对上面两个设想,

  1. 设想 A:对于此设想,如果负载为计算密集型应用则相对可行,如果负载为 IO 密集型应用则毫无意义。而且为了保证线程安全还需要特别注意锁的问题。
  2. 模型 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_REUSEADDRSO_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.100.0.0.0 事实上不是同一个地址,系统允许第二个 socket 进行绑定。

SO_REUSEPORT

这个选项是最近(Linux Kernel 3.9+)才加入的。假如有两个进程 A 和 B 各有一个进程需要监听 0.0.0.0:21,设置此参数后,系统会允许多个 socket 监听在同一个地址的同一个端口上。

需要注意的是,并非所有的操作系统都支持这个选项。Windows 只支持 SO_REUSEADDR,其行为相当于 SO_REUSEADDRSO_REUSEPORT 共同作用的结果,更多细节可查询 MSDN 中相关说明,BSD、Android、iOS/macOS 亦各不相同。

Gunicorn 中的应用

上文中说到,Gunicorn 会在启动是 fork() 生成多个 Worker。

select() & non-blocking socket

Connection: Keep-Alive

结语


Reference