Nginx 高性能,与其架构有关。
Nginx架构: nginx运行时,在unix系统中以daemon形式在后台运行,后台进程包含一个master进程和多个worker进程。Nginx以多进程形式工作,也支持多线程方式,丹nginx默认采用多进程方式,也是主流方式。
多进程模式,会有一个master进程和多个worker进程。
Master进程管理worker进程,包括:
接收来自外界的信号;
向各worker进程发送信号;
监控work进程状态;
当worker退出后(异常情况下),自动重新启动新worker进程。
多个worker进程之间对等,竞争来自客户端的请求,一个请求,只会在一个worker中处理,一个worker进程不会处理其他进程的请求。
Worker进程个数的设置,一般设置与机器cpu核数一致。
进程模式的好处:
每个worker进程相互独立,无需加锁,节省锁开销;
采用独立的进程,不会相互影响,一个进程退出,其他进程服务不会中断;
Worker异常退出,会导致当前worker上的所有请求失败,不过不会影响所有请求,降低了风险。
多进程模式对并发的支持
每个worker只有一个主线程,采用异步非阻塞方式来处理请求,使得nginx可以同时处理成千上万个请求。相比Apache,每个请求会独占一个工作线程,并发上千时,就同时有几千的线程在处理请求,线程带来的内存占用很大,线程的上下午切换带来的cpu开销也大,性能就上不去了。
异步非阻塞是什么呢?
一个请求的完整过程:请求过来,建立连接,然后接收数据,接收数据后,再发送数据。
具体到系统底层,就是读写事件,当读写时间没有准备好时,如果不用非阻塞的方式来调用,就得阻塞调用了,事件没准备好,就只能等,等事件准备好再继续。阻塞调用会进入内核等待,让出cpu,对单线程的worker来说,显然不合适,当网络事件越多时,等待很多,cpu利用率上不去。非阻塞就是,事件没有准备好,马上返回eagain,表示事件还没准备好,过会儿再来,过一会,再来检查一下事件,直到事件准备好为止,在这期间,你可以先去做其他事情,然后再来看看事件好了没。这时,虽不阻塞了,但是还得不时来检查事件的状态,带来的开销也不小。所以有了异步非阻塞的事件处理机制,具体到系统调用就是像 select/poll/epoll/kquene这样的系统调用。提供一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但是可以设置超时时间,在超时时间之内,如果有事件准备好了就返回。这种机制解决了上面的两个问题,以epoll为例,当事假没准备好时,放到epoll里,事件准备好了,就去读写,当读写返回eagain时,将它再次加入epoll,这样,只要有事件准备好了,就去处理它,只有当所有事件都没有准备好时,才在epoll里等着。这样,就可以支持大量的并发,这里的并发请求,是指未处理完的请求,线程只有一个,同时处理的请求只有一个,只是在请求间不断切换,切换是因为异步事件未准备好,主动让出的。这里的切换没有什么代价,可以理解为在循环处理多个准备好的事件,事实上也是。与多线程相比,这种事件处理方式有很大优势,不需创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常轻量级,没有上下文切换的开销,更多并发,只会占更多的内存而已。现在的网络服务器基本都采用这种方式,也是nginx性能高效的主要原因。
推荐设置worker数与cpu的核数一致,因为更多的worker,会导致进程竞争cpu资源,从而带来不必要的上下文切换。
怎样操作运行的nignx呢?master进程会接收来自外界发来的信号,因此要控制nginx,通过kill向master进程发送信号就可以了。如 kill –HUP pid,重启nginx,或重新加载配置,而不中断服务。Master进程在接到这个信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发信号,不再接收新的请求,并且在处理完所有未处理完的请求后,退出。新的worker启动后,就开始接收新的请求。
直接给master发信号,是比较老的操作方法,在nginx0.8版本后,可以使用命令行参数,方便管理,如./nginx –s reload ,重启nginx; ./nginx –s stop,停止nginx。这种方式的内部原理是,执行命令时,会启动一个新的nginx进程,该进程在解析到reload参数后,知道目标是控制nginx重新加载配置文件,它会向master进程发送信号,接下来的处理,和直接向master进程发送信号一样。
Worker进程是怎么处理请求的呢?
一个连接请求过来,每个进程都有可能处理这个连接。Worker进程是从master进程fork出来的,在master进程里,先建立好需要listen的socket(listenfd)后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为了保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,开始读取请求,解析请求,产生数据后,再返回给客户端,最后才断开连接,这就是一个完整的请求处理。一个请求,完全由worker处理,且只在一个worker里处理。
Nginx中connection是对tcp连接的封装,包括连接的socket,读事件,写事件。
Nginx怎么处理一个连接的呢?nginx在启动时,会解析配置文件,得到需要监听的端口与ip,然后在nginx的master进程里,先初始化这个监控的socket,然后再fork出多个子进程,子进程竞争accept新的连接。此时,客户端就可以像nginx发起连接了,当客户端与服务器通过三次握手建立好一个连接,nginx的某一个子进程会accept成功,得到这个socket,然后创建nginx对连接的封装,接着,设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后,nginx或客户端主动关掉连接。
Nginx也可以作为客户端来请求其他server的数据,此时,与其它server创建的连接,也封装在ngx_connection中。
Nginx中,每个进程会有一个连接数的最大上限,这个上限与系统对fd的限制不一样。操作系统中,使用ulimit -n,可以得到一个进程所能打开的fd的最大数,即nofile,因为每个socket会占用一个fd,所以这个会限制进程的最大连接数,fd用完后,再创建socket,就会失败。Nginx通过设置worker_connections来设置每个进程支持的最大连接数,如果该值大于nofile,那么实际的最大连接数是nofile,nginx会有警告。Nginx在实现时,是通过一个连接池来管理的,每个worker进程都有一个独立的连接池,连接池大小是worker_connections。这里连接池里面保存的其实不是真实的连接,只是一个worker_connections大小的ngx_connection_t结构的数组。Nginx通过一个链表free_connections来保存所有的空闲ngx_connection_t.每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。
Worker_connections,表示每个worker所能建立连接的最大值,一个nginx能建立的最大连接数是:worker_connections * worker_processes.因此对于HTTP请求本地资源,最大并发可以是 worker_connections * worker_processes.而如果是HTTP作为反向代理来说,最大并发数是 worker_connections * worker_processes/2.因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务器的连接,占用2个连接。
如何保证worker进程竞争处理连接的公平呢?
如果某个进程得到accept的机会比较多,它的空闲连接会很快用完,如果不提前做一些控制,当accept到一个新的tcp连接后,因为无法得到空闲连接,而且无法将此连接转交其他进程,最终导致此tcp连接得不到处理。而其他进程有空余连接,却没有处理机会。如何解决这个问题呢?
Nginx的处理得先打开accept_mutex,此时只有获得了accept_mutex的进程才会去添加accept事件,nginx会控制进程是否添加accept事件。Nginx使用一个叫ngx_accept_disabled变量控制是否竞争accept_mutex锁。这个变量与worker进程的剩余连接数有关,当该变量大于0时,就不去尝试获取锁,等于让出获取连接的机会。这样就可以控制多进程间连接的平衡了。
http请求是请求应答式的,如果我们知道每个请求头与相应体的长度,那么我们可以在一个连接上面执行多个请求。即长连接。如果当前请求需要有body,那么nginx就需要客户端在请求头中指定content-length来表面body的大小,否则返回400错误。那么响应体的长度呢?http协议中关于响应body长度的确定:
1 对于http1.0 协议来说,如果响应头中有content-length头,则以content-length的长度就可以知道body的长度,客户端在接收body时,可以依照这个长度接收数据,接收完后,就表示该请求完成。如果没有content-length,客户端会一直接收数据,直到服务端主动端口连接,才表示body接收完
2 对于http1.1 协议,如果响应头中transfer-encoding为chunked传输,表示body是流式输出,body被分成多个块,每块的开始会标示出当前块的长度,此时,body不需要指定长度。如果是非chunked传输,而且有Content-length,则按照content-length来接收数据。否则,非chunked且没有content-length,则客户端接收数据,知道服务器主动断开。
客户端请求头中connection为close,表示客户端要关掉长连接,如果是keep-alive,则客户端需要打开长连接。客户端的请求中没有connection这个头,根据协议,如果是http1.0,默认是close,如果是http1.1,默认是keep-alive。如果要keep-alive,nginx在输出完响应体后,会设置当前连接的keepalive属性,然后等待客户端下一次请求,nginx设置了keepalive的等待最大时间。一般来说,当客户端需要多次访问同一个server时,打开keepalive的优势非常大。
http1.1中引入Pipeline,就是流水线作业,可以看做是keepalive的升华。Pipeline也是基于长连接的。目前就是利用一个连接做多次请求,如果客户端要提交多个请求,对于keepalive,第二个请求,必须要等到第一个请求的响应接收完后,才能发起。得到两个响应的时间至少是2*RTT。而对于pipeline,客户端不必等到第一个请求处理完,就可以发起第二个请求。得到两个响应的时间可能能够达到1*RTT。Nginx是直接支持pipeline的。Nginx对pipeline中的多个请求的处理不是并行的,而是一个接一个的处理,只是在处理第一个请求的时候,客户端就可以发起第二个请求。这样,nginx利用pipeline可以减少从处理完一个请求后到等待第二个请求的请求头数据的时间。
具体参见http://seanlook.com/2015/05/17/nginx-install-and-config/
或者http://blog.csdn.net/guodongxiaren/article/details/40950249
安装nginx
yum install nginx-1.6.3
Nginx配置文件主要有4部分,main(全局设置)、server(主机设置)、upstream(上游服务器设置,主要为反向代理,负载均衡相关配置)和location(url匹配特定位置的设置),每部分包含若干指令。
Main部分的设置影响其他所有部分的设置;
Server部分主要用于指定虚拟机主机域名,ip和端口;
Upstream的指令用于设置一系列的后端服务器,设置反向代理及后端服务器的负载均衡;
Location部分用于匹配网页位置(如,跟目录“/”,”/images”等)。
它们之间的关系是,server继承main,location继承server,upstream既不会继承指令也不会被继承。