nginx:服务器集群
2018-01-09 13:44 ZengGW 阅读(448) 评论(0) 编辑 收藏 举报一、Nginx的事件处理机制
对于一个基本的web服务器来说,事件通常有三种类型,网络事件、信号、定时器。
首先看一个请求的基本过程:建立连接---接收数据---发送数据 。
再次看系统底层的操作 :上述过程(建立连接-->接收数据-->发送数据)在系统底层就是读写事件。
1)如果采用阻塞调用的方式,当读写事件没有准备好时,肯定不能够进行读写事件操作,只能等待直到事件准备好,才能进行读写事件。这样一来请求就会被耽搁 ,就造成了阻塞。阻塞调用会进入内核等待,当有其他事件进行操作时,cpu就会空出来给其他事件,对单线程的worker来说,显然不合适,当网络事件越多时,所有事件都基于等待状态,cpu则一直处于空闲状态,那么cpu的利用率自然就很低 。
2)既然很多事件在没有准备好的状态下阻塞调用CPU的利用率太低,不太合理,那么采用非阻塞方式。非阻塞就是:当有事件时马上返回EAGAIN, 提示事件还没准备好,需要等事件准备好了才能进行接下来的操作,这时候只能等待一段时间再来检查一下事件状态,如此这样循环下去直到事件准备好了为止,在这期间,你就可以先去做其它事情,然后再来看看事件好了没。虽然不会产生阻塞了,但你要隔一段时间就来检查一下事件的状态,你可以做更多的事情了,但是带来的轮询的开销很大。
小结:非阻塞调用方式不会产生阻塞调用的方式产生的大量时间阻塞,造成的cpu利用率低的情况,但是通过不断检查事件的状态来判断是否进行读写操作,这样带来的开销很大。
3基于阻塞和非阻塞调用带来的副作用,就又了异步非阻塞的事件处理机制。具体到系统调用就是像select/poll/epoll/kqueue(这几个都是I/O多路复用机制,有兴趣的可以看看相关的知识点)这样的系统调用。他们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时(有效期)时间,在有效期内,如果有事件准备好了,就返回事件准备就绪的数量,反之则返回0。这种机制解决了我们上面两个问题。
以epoll为例:当事件没有准备好时,就放入epoll(队列)里面。如果有事件准备好了,那么就去处理;如果事件返回的是EAGAIN,那么继续将其放入epoll里面。从而,只要有事件准备好了,我们就去处理她,只有当所有时间都没有准备好时,才在epoll里 面等着。这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件。
4)与多线程的比较:
与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。
小结:通过异步非阻塞的事件处理机制,Nginx实现由进程循环处理多个准备好的事件,从而实现高并发和轻量级。
master/worker结构:一个master进程,生成一个或多个worker进程(在启动nginx的时候,会产生一个主程序master,和多个worker进程,worker进程的数量一般与对应服务器的cpu数量相对应,why?原因在于如果worker的数量超出了cpu的数量,那么会造成进程争用cpu资源的问题,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效)
二、Nginx的进程(内部)模型、
nginx是以多进程的方式来工作的,当然nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式,也是nginx的默认方式。nginx采用多进程的方式有很多优势.
(1) nginx在启动后,会有一个master进程和多个worker进程。master进程主要用来管理worker进程,master进程包含:接收来自外界请求的信号,向各worker进程发送信号,监控 worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。而基本的网络事件,则是放在worker进程中来处理了 。多个worker进程之间是对等(相同,级别什么都一样,没有谁先谁后之分,公平竞争来自客户端的每一个请求)的,他们同等竞争来自客户端的请求,各进程互相之间是独立的 。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能同事处理其它进程的请求。 worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的 (上边已经说过了,就两点:1.避免不必要的竞争cpu资源造成上下文切换;2.基于nginx的一个多核特性,提供cpu亲缘性,也就是一个work进程对应一个cpu,可以避免不必要的上下文切换,减小系统资源和压力,造成cache的失效)。
(2)Master接收到信号以后怎样进行处理呢?讲该请求交给那个worker呢(./nginx -s reload )?首先master进程在接到信号后,会先重新加载配置文件,然后再启动新的进程,并发出信号通知所有旧的进程你们处理完手上已有的任务就不要再接新的任务了。新的进程在启动后,就开始接收新的请求,而老的进程在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后直接退出;
(3) worker进程又是如何accept请求的呢?我们前面有提到,worker进程之间是平等(再次强调,work进程之间是平级,没有优先级之说)的,每个进程,accept请求的机会是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能accept这个连接,那是怎么accept请求的呢?首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket之后,然后再fork出多个worker进程,这样每个worker进程都可以去accept这个socket(当然不是同一个socket,只是每个进程的这个socket会监控在同一个ip地址与端口,这个在网络协议里面是允许的)。一般来说,当一个连接进来后,所有在accept在这个socket上面的进程,都会收到通知,而只有一个进程可以accept这个连接,其它的则accept失败,这是所谓的惊群现象。针对惊群现象nginx提供了一个accept_mutex这个参数,从名字上,我们可以看这是一个加在accept上的一把共享锁。有了这把锁之后,同一时刻,就只会有一个进程在accpet连接,这样就不会有惊群问题了。accept_mutex是一个可控选项,我们可以显示地关掉,默认是打开的。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理(一个work进程同一时间只能处理一个请求)。
(4):,nginx采用这种进程模型master-worker的好处是什么呢?最大的好处就是耦合度低,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程。当然,worker进程的异常退出,肯定是程序有bug了,异常退出,会导致当前worker上的所有请求失败,不过不会影响到所有请求,所以降低了风险。
(5).有人可能要问了,nginx采用多worker的方式来处理请求,每个worker里面只有一个主线程,那能够处理的并发数很有限啊,多少个worker就能处理多少个并发,何来高并发呢?其实nginx采用了异步非阻塞的方式来处理请求,nginx是可以同时处理成千上万个请求的 .对于IIS服务器每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。我们之前说过,推荐设置worker的个数为cpu的核数就很好理解了;
三、Nginx如何处理一个请求
首先,nginx在启动时,会读取解析配置文件,得到需要监听的端口与ip地址,然后在nginx的master进程里面,先初始化好这个监控的socket(创建socket,设置一些选项,绑定到指定的ip地址端口,再listen),然后再fork(一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程 )出多个子进程出来,然后子进程会竞争accept新的连接。此时,客户端就可以向nginx发起连接了。当客户端与nginx进行三次握手(第一次是客户端向服务器发送syn包(syn=j),并进入SYN_SEND状态等待服务器回答;第二次是服务器收到客户端发送的syn包,SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;第三次客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手),与nginx建立好一个连接后,此时,某一个子进程会accept成功,得到这个建立好的连接的socket,然后创建nginx对连接的封装,即ngx_connection_t结构体。接着,设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后,nginx或客户端来主动关掉连接,到此,一个连接就完成了。
当然,nginx也是可以作为客户端来请求其它server的数据的(如upstream模块),此时,与其它server创建的连接,也封装在ngx_connection_t中。作为客户端,nginx先获取一个ngx_connection_t结构体,然后创建socket,并设置socket的属性( 比如非阻塞)。然后再通过添加读写事件,调用connect/read/write来调用连接,最后关掉连接,并释放ngx_connection_t。
说明:nginx在实现时,是通过一个连接池来管理的,每个worker进程都有一个独立的连接池,连接池的大小是worker_connections。这里的连接池里面保存的其实不是真实的连接,它只是一个worker_connections大小的一个ngx_connection_t结构的数组。并且,nginx会通过一个链表free_connections来保存所有的空闲ngx_connection_t,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。
注意:在这里,很多人会误解worker_connections这个参数的意思,认为这个值就是nginx所能建立连接的最大值。其实不然,这个值是表示每个worker进程所能建立连接的最大值,所以,一个nginx能建立的最大连接数,应该是worker_connections * worker_processes。当然,这里说的是最大连接数,对于HTTP请求本地资源来说,能够支持的最大并发数量是worker_connections * worker_processes,而如果是HTTP作为反向代理来说,最大并发数量应该是worker_connections * worker_processes/2。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。
四、Nginx典型的应用场景:负载均衡
负载均衡的原理:客户端向反向代理发送请求,接着反向代理根据某种负载机制转发请求至目标服务器(这些服务器都运行着相同的应用),并把获得的内容返回给客户端,期中,代理请求可能根据配置被发往不同的服务器。
原文博客地址:http://www.cnblogs.com/jiekzou/,非常感谢原作者