声明:本文的内容源于http://tengine.taobao.org/相干资料,如果想深入了解,可以到该网站查看。
nginx的高性能在业界已是众人皆知了,性能究竟有多高?官方测试Nginx能够支持5万并发连接,在实际生产环境中可支持2~4万并发的连接数是没有啥问题的。根据实战Nginx书中描写,同等硬件环境下,Nginx的处理能力相当于Apache的5~10倍。而这么高的性能,与其架构是分不开的。
nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包括1个master进程和多个worker进程。master进程主要用来管理worker进程,包括:接收来自外界的信号、向各worker进程发送信号和监控worker进程的运行状态等。当worker进程退出后(异常情况下),会自动重新启动新的worker进程。而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户真个要求,各进程相互之间是独立的。1个要求,只可能在1个worker进程中处理,1个worker进程,不可能处理其它进程的要求。worker进程的个数是可以设置的,1般我们会设置与机器cpu核数1致,这里面的缘由与nginx的进程模型和事件处理模型是分不开的。
nginx的进程模型,可以由下图来表示:
从上图中我们可以看到,master来管理worker进程,所以我们只需要与master进程通讯就好了。master进程会接收来自外界发来的信号,再根据信号做不同的事情。所以我们要控制nginx,只需要通过kill向master进程发送信号就好了。
那末,worker进行又是如何处理要求的呢?前面有提到,worker进程之间是同等的,每一个进程,处理要求的机会也是1样的。首先,每一个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)以后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有1个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当1个worker进程在accept这个连接以后,就开始读取要求,解析要求,处理要求,产生数据后,再返回给客户端,最后才断开连接,这样1个完全的要求就是这样的了。我们可以看到,1个要求,完全由worker进程来处理,而且只在1个worker进程中处理。
nginx采取这类进程模型有甚么好处呢?固然,好处肯定会很多了。首先,对每一个worker进程来讲,独立的进程,不需要加锁,所以省掉了锁带来的开消,同时在编程和问题查找时,也会方便很多。其次,采取独立的进程,可让相互之间不会影响,1个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程。固然,worker进程的异常退出,肯定是程序有bug了,异常退出,会致使当前worker上的所有要求失败,不过不会影响到所有要求,所以下降了风险。固然,好处还有很多,大家可以渐渐体会。
到这里,有人可能要问了,nginx采取多worker的方式来处理要求,每一个worker里面只有1个主线程,那能够处理的并发数很有限啊,多少个worker就可以处理多少个并发,何来高并发呢?非也,这就是nginx的高明的地方,nginx采取了异步非阻塞的方式来处理要求,也就是说,nginx是可以同时处理不计其数个要求的。想一想apache的经常使用工作方式(apache也有异步非阻塞版本,但因其与自带某些模块冲突,所以不经常使用),每一个要求会独占1个工作线程,当并发数上到几千时,就同时有几千的线程在处理要求了。这对操作系统来讲,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开消很大,自然性能就上不去了,而这些开消完全是没成心义的。
作甚异步非阻塞?我们先回到原点,看看1个要求的完全进程。首先,要求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。具体到系统底层,就是读写事件,而当读写事件没有准备好时,必定不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给他人用了,对单线程的worker来讲,明显不适合,当网络事件越多时,大家都在等待呢,cpu空闲下来没人用,cpu利用率自然上不去了,更别谈高并发了。好吧,你说加进程数,这跟apache的线程模型有甚么区分?注意,别增加无谓的上下文切换。所以,在nginx里面,最忌讳阻塞的系统调用了。不要阻塞,那就非阻塞喽。非阻塞就是,事件没有准备好,马上返回EAGAIN,告知你,事件还没准备好呢,你慌甚么,过会再来吧。好吧,你过1会,再来检查1下事件,直到事件准备好了为止,在这期间,你就能够先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但你得不时地过来检查1下事件的状态,你可以做更多的事情了,但带来的开消也是不小的。所以,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了1种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间以内,如果有事件准备好了,就返回。这类机制正好解决了我们上面的两个问题,拿epoll为例(在后面的例子中,我们多以epoll为例子,以代表这1类函数),当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这样,我们就能够并发处理大量的并发了,固然,这里的并发要求,是指未处理完的要求,线程只有1个,所以同时能处理的要求固然只有1个了,只是在要求间进行不断地切换而已,切换也是由于异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这类事件处理方式是有很大的优势的,不需要创建线程,每一个要求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会致使无谓的资源浪费(上下文切换)。更多的并发数,只是会占用更多的内存而已。
现在的网络服务器基本都采取这类方式,这也是nginx性能高效的主要缘由。