众所周知,Nginx 性能高,而 Nginx 的高性能与其架构是分不开的。那么 Nginx 到底是怎么样的呢?html
Nginx 在启动后,在 unix/linux 系统中会以 daemon 的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程。咱们也能够手动地关掉后台模式,让 Nginx 在前台运行,而且经过配置让 Nginx 取消 master 进程,从而可使 Nginx 以单进程方式运行。很显然,生产环境下咱们确定不会这么作,因此关闭后台模式,通常是用来调试用的。因此,咱们能够看到,Nginx 是以多进程的方式来工做的,固然 Nginx 也是支持多线程的方式的,只是咱们主流的方式仍是多进程的方式,这也是 Nginx 的默认方式。Nginx 采用多进程的方式有诸多好处,因此这里就主要讲解 Nginx 的多进程模式。linux
刚才讲到,Nginx 在启动后,会有一个 master 进程和多个 worker 进程。master 进程主要用来管理 worker 进程,包含:接收来自外界的信号,向各 worker 进程发送信号,监控 worker 进程的运行状态,当 worker 进程退出后(异常状况下),会自动从新启动新的 worker 进程。而基本的网络事件,则是放在 worker 进程中来处理了。多个 worker 进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个 worker 进程中处理,一个 worker 进程,不可能处理其它进程的请求。worker 进程的个数是能够设置的,通常咱们会设置与机器cpu核数一致,这里面的缘由与 Nginx 的进程模型以及事件处理模型是分不开的。Nginx 的进程模型,能够由下图来表示:nginx
worker 进程之间是平等的,每一个进程,处理请求的机会也是同样的。当咱们提供 80 端口的 http 服务时,一个链接请求过来,每一个进程都有可能处理这个链接,怎么作到的呢?首先,每一个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先创建好须要 listen 的 socket(listenfd)以后,而后再 fork 出多个 worker 进程。全部 worker 进程的 listenfd 会在新链接到来时变得可读,为保证只有一个进程处理该链接,全部 worker 进程在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,在读事件里调用 accept 接受该链接。当一个 worker 进程在 accept 这个链接以后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开链接,这样一个完整的请求就是这样的了。咱们能够看到,一个请求,彻底由 worker 进程来处理,并且只在一个 worker 进程中处理。apache
那么,Nginx 采用这种进程模型有什么好处呢?固然,好处确定会不少了。首先,对于每一个 worker 进程来讲,独立的进程,不须要加锁,因此省掉了锁带来的开销,同时在编程以及问题查找时,也会方便不少。其次,采用独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在工做,服务不会中断,master 进程则很快启动新的 worker 进程。固然,worker 进程的异常退出,确定是程序有 bug 了,异常退出,会致使当前 worker 上的全部请求失败,不过不会影响到全部请求,因此下降了风险。编程
上面讲了不少关于 Nginx 的进程模型,接下来,咱们来看看 Nginx 是如何处理事件的。服务器
有人可能要问了,Nginx 采用多 worker 的方式来处理请求,每一个 worker 里面只有一个主线程,那可以处理的并发数颇有限啊,多少个 worker 就能处理多少个并发,何来高并发呢?非也,这就是 Nginx 的高明之处,Nginx 采用了异步非阻塞的方式来处理请求,也就是说,Nginx 是能够同时处理成千上万个请求的。想一想 apache 的经常使用工做方式(apache 也有异步非阻塞版本,但因其与自带某些模块冲突,因此不经常使用),每一个请求会独占一个工做线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操做系统来讲,是个不小的挑战,线程带来的内存占用很是大,线程的上下文切换带来的 cpu 开销很大,天然性能就上不去了,而这些开销彻底是没有意义的。网络
为何 Nginx 能够采用异步非阻塞的方式来处理呢,或者异步非阻塞究竟是怎么回事呢?咱们先回到原点,看看一个请求的完整过程。多线程
首先,请求过来,要创建链接,而后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操做,若是不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu 就会让出去给别人用了,对单线程的 worker 来讲,显然不合适,当网络事件越多时,你们都在等待呢,cpu 空闲下来没人用,cpu利用率天然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有什么区别,注意,别增长无谓的上下文切换。因此,在 Nginx 里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,立刻返回 EAGAIN,告诉你,事件还没准备好呢,你慌什么,过会再来吧。好吧,你过一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就能够先去作其它事情,而后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查一下事件的状态,你能够作更多的事情了,但带来的开销也是不小的。因此,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像 select/poll/epoll/kqueue 这样的系统调用(events里面能够配置)。它们提供了一种机制,让你能够同时监控多个事件,调用他们是阻塞的,但能够设置超时时间,在超时时间以内,若是有事件准备好了,就返回。这种机制正好解决了咱们上面的两个问题,拿 epoll 为例:架构
- 当事件没准备好时,放到 epoll 里面,事件准备好了,咱们就去读写,当读写返回 EAGAIN 时,咱们将它再次加入到 epoll 里面。这样,只要有事件准备好了,咱们就去处理它,只有当全部事件都没准备好时,才在 epoll 里面等着。这样,咱们就能够并发处理大量的并发了,固然,这里的并发请求,是指未处理完的请求,线程只有一个,因此同时能处理的请求固然只有一个了,只是在请求间进行不断地切换而已,切换也是由于异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你能够理解为循环处理多个准备好的事件,事实上就是这样的。
- 与多线程相比,这种事件处理方式是有很大的优点的,不须要建立线程,每一个请求占用的内存也不多,没有上下文切换,事件处理很是的轻量级。并发数再多也不会致使无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。 我以前有对链接数进行过测试,在 24G 内存的机器上,处理的并发请求数达到过 200 万。如今的网络服务器基本都采用这种方式,这也是nginx性能高效的主要缘由。
咱们以前说过,推荐设置 worker 的个数为 cpu 的核数,在这里就很容易理解了,更多的 worker 数,只会致使进程来竞争 cpu 资源了,从而带来没必要要的上下文切换。并且,nginx为了更好的利用多核特性,提供了 cpu 亲缘性的绑定选项,咱们能够将某一个进程绑定在某一个核上,这样就不会由于进程的切换带来 cache 的失效。像这种小的优化在 Nginx 中很是常见,同时也说明了 Nginx 做者的苦心孤诣。好比,Nginx 在作 4 个字节的字符串比较时,会将 4 个字符转换成一个 int 型,再做比较,以减小 cpu 的指令数等等。并发
如今,知道了 Nginx 为何会选择这样的进程模型与事件模型了。对于一个基本的 Web 服务器来讲,事件一般有三种类型,网络事件、信号、定时器。从上面的讲解中知道,网络事件经过异步非阻塞能够很好的解决掉。如何处理信号与定时器?
首先,信号的处理。对 Nginx 来讲,有一些特定的信号,表明着特定的意义。信号会中断掉程序当前的运行,在改变状态后,继续执行。若是是系统调用,则可能会致使系统调用的失败,须要重入。对于 Nginx 来讲,若是nginx正在等待事件(epoll_wait 时),若是程序收到信号,在信号处理函数处理完后,epoll_wait 会返回错误,而后程序可再次进入 epoll_wait 调用。
另外,再来看看定时器。因为 epoll_wait 等函数在调用的时候是能够设置一个超时时间的,因此 Nginx 借助这个超时时间来实现定时器。nginx里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入 epoll_wait前,先从该红黑树里面拿到全部定时器事件的最小时间,在计算出 epoll_wait 的超时时间后进入 epoll_wait。因此,当没有事件产生,也没有中断信号时,epoll_wait 会超时,也就是说,定时器事件到了。这时,nginx会检查全部的超时事件,将他们的状态设置为超时,而后再去处理网络事件。由此能够看出,当咱们写 Nginx 代码时,在处理网络事件的回调函数时,一般作的第一个事情就是判断超时,而后再去处理网络事件。
咱们能够用一段伪代码来总结一下 Nginx 的事件处理模型:
while (true) { for t in run_tasks: t.handler(); update_time(&now); timeout = ETERNITY; for t in wait_tasks: /* sorted already */ if (t.time <= now) { t.timeout_handler(); } else { timeout = t.time - now; break; } nevents = poll_function(events, timeout); for i in nevents: task t; if (events[i].type == READ) { t.handler = read_handler; } else { /* events[i].type == WRITE */ t.handler = write_handler; } run_tasks_add(t); }
参考:http://tengine.taobao.org/book/index.html