每次搜索 nginx 网络处理模型都一头雾水,这里简单总结一下。
首先 master 进程 bind()
、listen()
TCP 端口 (nonblocking),然后 fork()
出多个 worker 进程,socket fd 默认传递给各个 worker 进程;
然后每个 worker 进程 epoll_create()
-> epoll_ctl()
-> epoll_wait()
来等待 socket 连接事件,对于这里的细节,具体有以下几种情况
-
1. accept_mutex
在有连接事件的时候,所有 worker 都会被唤醒;
如果 accept_mutex on(1.11.3 版本之前的默认值),各个 worker 就会去抢 accept_mutex,抢到的进程再去 accept 连接;
如果 accept_mutex 设置为 off,各个 worker 直接去尝试 accept 连接,当然内核会保证只有一个进程 accepct 成功; 这种情况带来了大量用户态/内核态上下文切换,增加了 cpu 占用(但是连接处理延迟会降低)。 -
2. reuseport
1.9.1 版本开始,可以在listen
指令后添加reuseport
选项,打开 socket 的 SO_REUSEPORT 支持 (Linux 3.9)。 这样 master 在创建 socket 的时候会设置 SO_REUSEPORT,并且一次性创建多个 80 端口的 socket 并完成listen
, 随后将其分配给不同的 worker,每个 worker 拿到的都是不同的 fd。 在有新的连接时,由内核来选择把连接事件交给哪个 fd,等同于在选择 worker,这样可以避免 epoll 事件唤醒所有 worker 的 “惊群” 现象。
这个方案的劣势是,内核无法得知 worker 进程的状态,如果一个 worker 阻塞了,内核继续分配新连接给它,就会出现问题,因此该选项也没有默认启用。
(由于与 accept_mutex 功能上的重复,因此该选项与 accept_mutex 是互斥的,只能二选一) -
3. EPOLLEXCLUSIVE
从 1.11.3 版本开始,accept_mutex 默认 off,epoll_ctl
的新选项EPOLLEXCLUSIVE
(Linux 4.5, glibc 2.24)可以让 epoll 事件仅发送给一个或多个进程,而不会发送给所有进程,避免了 “惊群” 现象,并且没有上文 reuseport 方案的问题。
参考:
https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1
http://nginx.org/en/docs/events.html#epoll
https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/
https://stackoverflow.com/questions/15636319/why-is-accept-mutex-on-as-default-in-nginx
https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale
https://www.nginx.com/blog/performance-tuning-tips-tricks
http://www.aosabook.org/en/nginx.html
https://www.nginx.com/blog/thread-pools-boost-performance-9x