众所周知,nginx 性能高,而 nginx 的高性能与其架构是分不开的。
1、nginx 多进程模式架构
nginx 启动后,会有一个master 进程和多个 worker 进程。
master 进程用来管理 worker 进程,功能包含:接收来自外界的信号,向各个worker 进程发送信号,监控woker进程的运行状态。
当worker 进程退出(异常退出),会自动重启启动新的worker 进程。
worker 进程:处理基本的网络事件。多个worker 进程之间对等。同等竞争来自客户端的请求,各进程独立。每个请求只能在一个worker 进程中处理,每个进程不会处理其他进程请求。
worker的个数可以设置,一般设置为cpu核数一致。
2、nginx 多进程模型的优势
那么,nginx 采用这种进程模型有什么好处呢?
- 首先,对于每个 worker 进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查上时,也会方便很多。
- 其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程,降低了风险。
3、worker 进程处理请求流程
nginx 在 0.8 版本之后,引入了一系列命令行参数,来方便我们管理。
例如:
重启 nginx:
./nginx -s reload
停止 nginx:
./nginx-s stop
我们执行命令时,启动了一个新的 nqinx 进程,新的 nginx 进程在解析到reload 参数后,就控制 nginx 重新加载配置文件,它会向 master 进程发送信号。
那么worker 进程又是如何处理请求的呢?
假如:我们提供 80 端口的 http 服务,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?
首先,在 master 进程里面,先建立好需要 listen 的 socket ,然后再 fork 出多个 worker 进程。
这样每个 worker 进程都可以去 accept 这个 socket (每个进程的socket 会监控在同一个 ip 地址与端口,这个在网络协议里面是允许的)。
当一个连接进来后,所有在 accept 在这个 socket 上面的进程,都会收到通知,而只有一个进程可以accept 这个连接,其它的则 accept 失败,这是所谓的惊群现象。
nginx 是怎么来解决惊群现象呢?
nginx提供了一个 accept mutex,从名字上,我们可以知道这是一个加在 accept 上的共享锁。同一时刻,就只会有一个进程在 accpet 连接。accept mutex 是一个可控选项,默认是打开,我们可以显示地关掉。
当一个 worker 进程在 accept 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这就是一个完整的请求。
由此正如前面所说:一个请求,完全由 worker 进程来处理,而且只在一个 worker 进程中处理。
4、nginx如何处理事件?
上面说了很多关干的进程模型,接下来,我们来看看 nginx的是如何处理事件的。
可能有人会问:nginx 采用多 worker 的方式来处理请求,每个 worker 里面只有一个主线程,那高并发处理怎么做到的?
nginx非常高明,采用了异步非阻塞的方式来处理请求,可以同时处理成千上万个请求。
nginx 异步非阻塞到底是怎么回事呢?
前面所说,请求的完整过程如下:
首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。
具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作。
如果不用非阻塞的方式来调用,那就得阻塞调用,事件没有准备好,那就只能等了,等事件准备好了,再继续。阻塞调用会进入内核等待,cpu 利用率自然上不去了,更别谈高并发了。在 nginx 里面,最忌讳阻寒的系统调用。不要阻塞那就非阻塞。非阻塞就是,事件没有准备好,马上返回 EAGAIN,告诉你,事件还没准备好,过会再来。过一会,再来检查一下事件,直到事件准备好了为止,在这期间,可以先去做其它事情,然后再来看看事件好了没。
虽然不阻塞了,但你得不时地过来检査一下事件的状态,你可以做更多的事情了,但带来的开销也是不小的。
所以,才会有了异步非阻塞的事件处理机制。
异步非阻塞的事件处理机制,具体到系统调用就是像 selectpoll/epoll/kgueue 这样的系统调用 ,可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。
这种机制正好解决了我们上面的两个问题。
以epoll 为例 ,当事件没准备好时,放到epoll 里面,事件准备好了,我们就去读写,当读写返回 EAGAIN 时,我们将它再次加入到 epoll 里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在 epol 里面等着。这样,我们就可以并发处理大量的并发。
当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求只有一个,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件。
与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。
有人对连接数进行过测试,在 24G 内存的机器上,处理的并发请求数达到过 200万。
这也是 nginx 性能高效的主要原因。
我们之前说过,推荐设置 worker 的个数为 cpu 的核数,在这里就很容易理解了,过多的 worker 数,只会导致进程来竞争 cpu 资源,从而带来不必要的上下文切换。
而且,nginx为了更好的利用多核特性,提供了cpu 亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来 cache 的失效。
像这种小的优化在 nginx 中非常常见,同时也说明了 nginx 作者在性能优化上的深厚造诣。比如,nginx 在做 4个字节的字符串比较时,会将 4个字符转换成一个 int 型,再作比较,以减少 cpu 的指令数等等。
至此,相信大家应该都清楚 nginx 什么会选择这样的进程模型与事件模型了吧~
5、nginx如何处理信号与定时器?
对于一个 web 服务器来说,事件通常有三种类型,网络事件、信号、定时器。通过上面的讲解,网络事件通过异步非阻塞事件机制可以很好的解决掉。
(1)、nginx如何处理信号
首先,信号的处理。对nginx 来说,有一些特定的信号,代表着特定的意义。信号会中断掉程序当前的运行在改变状态后,继续执行。如果是系统调用,则可能会导致系统调用的失败,需要重入。
对于 nginx来说,如果nginx 正在等待事件(epoll wait 时),如果程序收到信号,在信号处理函数处理完后,epoll wait 会返回错误,然后程序可再次进入 epoll wait 调用。
(2)、nginx如何处理定时器
我们再来看看定时器。
由于 epoll wait 等函数在调用的时候,可以设置一个超时时间,所以 nginx 借助这个超时时间来实现定时器。
nginx里面的定时器事件是放在一个最小堆里面,每次在进入 epoll wait 前,先从最小堆里面拿到所有定时器事件的最小时间,在计算出 epoll wait 的超时时间后进入 epoll wait。
所以,当没有事件产生,也没有中断信号时,epoll wait 会超时,也就是说,定时器事件到了。这时,nginx 会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件。
由此可以看出,当我们写 nginx 代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件。