Nginx平台构架
深入理解Nginx模块发开与架构解析读书笔记。
nginx在启动后,在unix系统中会以daemon的方式(能够手动关闭 nginx.conf daemon off)在后台执行,后台进程包括一个master进程和多个worker进程。master进程主要用来管理worker进程,包括:接收来自外界的信号,向各worker进程发送信号,监控worker进程的执行状态。当worker进程退出后(异常情况下),会自己主动又一次启动新的worker进程。而主要的网络事件,则是放在worker进程中来处理了。
多个worker进程之间是对等的,他们同等竞争来自client的请求。各进程互相之间是独立的。一个请求,仅仅可能在一个worker进程中处理。一个worker进程。不可能处理其它进程的请求。worker进程的个数是能够设置的。一般我们会设置与机器cpu核数一致。这里面的原因与nginx的进程模型以及事件处理模型是分不开的。nginx的进程模型,能够由下图来表示。
在nginx启动后。假设我们要操作nginx,从上文中我们能够看到,master来管理worker进程,所以我们仅仅须要与master进程通信即可了。master进程会接收来自外界发来的信号,再依据信号做不同的事情。比方,./nginx -s reload。就是来重新启动nginx,./nginx -s stop,就是来停止nginx的执行。怎样做到的呢?我们还是拿reload来说。我们看到,执行命令时,我们是启动一个新的nginx进程。而新的nginx进程在解析到reload參数后,就知道我们的目的是控制nginx来又一次载入配置文件了。它会向master进程发送信号。
首先master进程在接到信号后。会先又一次载入配置文件,然后再启动新的worker进程,并向全部老的worker进程发送信号,告诉他们能够光荣退休了。新的worker在启动后,就開始接收新的请求,而老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的全部未处理完的请求处理完毕后。再退出。
worker进程之间是平等的,每一个进程。处理请求的机会也是一样的。
当我们提供80port的http服务时,一个连接请求过来。每一个进程都有可能处理这个连接,怎么做到的呢?首先。每一个worker进程都是从master进程fork过来。在master进程里面,先建立好须要listen的socket(listenfd)之后,然后再fork出多个worker进程。
全部worker进程的listenfd会在新连接到来时变得可读。为保证仅仅有一个进程处理该连接,全部worker进程在注冊listenfd读事件前抢accept_mutex,抢到相互排斥锁的那个进程注冊listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就開始读取请求。解析请求,处理请求,产生数据后。再返回给client,最后才断开连接,这样一个完整的请求就是这种了。我们能够看到,一个请求,全然由worker进程来处理,并且仅仅在一个worker进程中处理。
异步非堵塞特性:我们先回到原点,看看一个请求的完整过程。
首先。请求过来。要建立连接,然后再接收数据,接收数据后,再发送数据。详细到系统底层,就是读写事件,而当读写事件没有准备好时,必定不可操作,假设不用非堵塞的方式来调用。那就得堵塞调用(事件没有准备好,那就仅仅能等)。堵塞调用会进入内核等待。cpu就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢。cpu空暇下来没人用,cpu利用率自然上不去了。更别谈高并发了。好吧。你说加进程数,这跟apache的线程模型有什么差别。注意。别添加无谓的上下文切换。所以,在nginx里面。最忌讳堵塞的系统调用了。不要堵塞,那就非堵塞喽。非堵塞就是,事件没有准备好,立即返回EAGAIN(所请求资源临时不可用,稍后再訪问可能可用)。
你过一会,再来检查一下事件。直到事件准备好了为止。在这期间。你就能够先去做其它事情,然后再来看看事件好了没。
尽管不堵塞了,但你得不时地过来检查一下事件的状态。你能够做很多其它的事情了,但带来的开销也是不小的。所以。才会有了异步非堵塞的事件处理机制。详细到系统调用就是像select/poll/epoll/kqueue这种系统调用。它们提供了一种机制,让你能够同一时候监控多个事件,调用他们是堵塞的,但能够设置超时时间,在超时时间之内,假设有事件准备好了,就返回。这种机制正好攻克了我们上面的两个问题,拿epoll为例(在后面的样例中,我们多以epoll为样例,以代表这一类函数),当事件没准备好时。放到epoll里面。事件准备好了。我们就去读写。当读写返回EAGAIN时,我们将它再次添加到epoll里面。这样,仅仅要有事件准备好了。我们就去处理它,仅仅有当全部事件都没准备好时,才在epoll里面等着。这样。我们就能够并发处理大量的并发了。当然。这里的并发请求,是指未处理完的请求,线程仅仅有一个,所以同一时候能处理的请求当然仅仅有一个了,仅仅是在请求间进行不断地切换而已,切换也是由于异步事件未准备好,而主动让出的。这里的切换是没有不论什么代价。你能够理解为循环处理多个准备好的事件,其实就是这种。与多线程相比。这种事件处理方式是有非常大的优势的。不须要创建线程,每一个请求占用的内存也非常少,没有上下文切换。事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。很多其它的并发数,仅仅是会占用很多其它的内存而已。
本文作者之前有对连接数进行过測试,在24G内存的机器上,处理的并发请求数达到过200万。如今的网络server基本都採用这种方式,这也是nginx性能高效的主要原因。
推荐设置worker的个数为cpu的核数。在这里就非常easy理解了。很多其它的worker数,仅仅会导致进程来竞争cpu资源。
nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们能够将某一个进程绑定在某一个核上,这样就不会由于进程的切换带来cache的失效。
首先,信号的处理。对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);
}
Nginx环境搭建。
cat /etc/issue
RHEL Server release 6.6
Uname -r
Kernel 版本号:2.6.32
cd nginx-x.x.x/
./configure –prefix =/home/renwh/nginx --without-http_rewrite_module --without-http_gzip_module
Make
Make install
cd /home/renwh/nginx/bin
./nginx
(servise iptables stop)
浏览器直接訪问该机ip地址。能够看到nginx确认界面,则开启成功。
默认port:80