文章来源:http://blog.seclibs.com/nginx...php
Nginx程序架构图以下html
后面就先按照这个图所展现出来的内容对Nginx的架构进行一次梳理,文中所涉及到的内容,主要都是针对Linux系统的。python
最上面的Master进程是管理员直接控制的,也只有Master进行接受管理员信号,一个Master用户能够fork多个Worker进程,一个Worker进程能够响应多个用户请求。linux
每一个Worker都是由核心模块core和多个模块modules组成的,好比有http协议的ht_core模块,为了功能完善还有不少其它模块,如实现负载均衡的ht_upstream模块,ht_proxy反代模块,ht_fastcgi模块,memcache模块等。nginx
由于Nginx是高度模块化的,咱们在用到哪一个模块的时候,便将哪一个模块编译或者载入就能够了,好比基于ht_core能够与web通讯, 基于ht_fastcgi模块可与php通讯,基于memcache模块可与mamcache通讯。web
这里来解释一下什么是FastCGI,CGI全称”通用网关接口”(Common Gateway Interface),用于HTTP服务器与其它机器上的程序服务通讯交流的一种工具,CGI程序须运行在网络服务器上;可是因为CGI的性能和安全性都比较差,处理高并发几乎是不可用的,因此就诞生了FastCGI,FastCGI是一个可伸缩地、高速地在HTTP服务器和动态脚本语言间通讯的接口(FastCGI接口在Linux下是socket(能够是文件socket,也能够是ip socket)),主要优势是把动态语言和HTTP服务器分离开来。多数流行的HTTP服务器都支持FastCGI,包括Apache、Nginx和lightpd,同时它也被许多脚本语言所支持,包括PHP等。shell
Nginx不支持对外部动态程序的直接调用或者解析,全部的外部程序(包括PHP)必须经过FastCGI接口来调用。FastCGI接口在Linux下是socket(能够是文件socket,也能够是ip socket)。为了调用CGI程序,还须要一个FastCGI的wrapper,这个wrapper绑定在某个固定socket上,如端口或者文件socket。当Nginx将CGI请求发送给这个socket的时候,经过FastCGI接口,wrapper接收到请求,而后派生出一个新的线程,这个线程调用解释器或者外部程序处理脚本并读取返回数据;接着,wrapper再将返回的数据经过FastCGI接口,沿着固定的socket传递给Nginx;最后,Nginx将返回的数据发送给客户端,这就是Nginx+FastCGI的整个运做过程。数据库
FastCGI的主要优势是把动态语言和HTTP服务器分离开来,是Nginx专注处理静态请求和向后转发动态请求,而PHP/PHP-FPM服务器专注解析PHP动态请求。后端
说完Nginx+FastCGI后,继续说前面的模块,memcache是一个分布式的高速缓存系统,经常使用来作缓存服务器、将从数据库查询的数据缓存起来,减小数据库查询、加快查询速度。缓存
而后说图中用户与Nginx交互的部分,在与用户请求进行交互的时候,经过kevent、epoll和select来实现多路复用,实现处理并发用户请求,这里是由于Nginx采用的是多进程(单线程)的模式,采用多进程能够提升并发效率,而且各进程之间相互独立,一个Worker进程挂掉以后不会影响其余进程的运行。
到这里为止上面图中上半部分就说完了,主要就是nginx与用户和后端程序之间的关系,此外Nginx还提供了缓存机制,支持高级I/O机制、sendfile机制、AIO机制(异步非阻塞I/O)、mmap机制等。
咱们先来讲sendfile机制,在网上查到的全部资料里都提到sendfile机制能够提升文件传输的性能。在传统的文件传输中,使用的是read/write方式来进行文件与socket的传输,所须要通过的流程是这样的
总结一下就是硬盘—>内核buf—>用户buf—>socket相关缓冲区—>协议引擎,用一张图来表示,就是这个样子的
而在引入sendfile机制之后,数据的流程变为了这样
能够发现,引入sendfile机制之后,省去了拷贝到用户buf的过程,流程就变成了下图的样子
nginx在支持了sendfile系统调用后,避免了内核层与用户层的上线文切换(content swith)工做,大大减小了系统性能的开销。
前面说了这么多,都没有绕开内核缓冲区和用户缓冲区,那它们分别又是什么东西?
这里首先先区分一下缓冲区buffer和缓存cache是两个彻底不一样的东西,buffer是减小调用次数,集中调用,提升系统性能的,而cache是将读取过的数据保留下来,若是从新读取的时候发现已经读取过,就不须要再去读硬盘数据了。
上图是一个计算机系统运行时的简化模型,在说用户进程和系统进程以前还须要再说一下内核态(kernel mode)和用户态(user mode),内核态能够访问系统资源,好比CPU、IO设备、进程管理、内存、进程间通讯IPC、网络通讯等,这些资源在用户进程中是不能直接访问的,须要通过操做系统才能够,这些有操做系统提供的功能也叫作系统调用。
下图是用户经过shell来对文件进行操做的示例图,它们都是通过内核来进行操做的,而提供这些限制的基础就是CPU提供的内核态和用户态。
前面说了用户进程在访问系统资源的时候,须要先切换到内核态,在这以前有不少的堆栈、内存环境等须要提早准备好,在调用结束之后,必须恢复到用户态,这其中的堆栈等又必须回到用户进程的上下文,这其中的切换就会消耗大量的资源。因此用户缓冲区就是在读取文件的时候申请的一块内存空间,也就是buffer,而后程序都是从buffer中获取数据的,只有在内存空间使用完后才会进行下一次调用来填充buffer,这样就减小了系统调用的次数,减小了在用户态和内核态之间切换的消耗时间。
固然内核也有它本身的缓冲区,在用户进程要从磁盘读取数据的时候,内核通常不会去读磁盘,而是将内核缓冲区中的数据复制给用户进程缓冲区,若是内核缓冲区没有数据的话,内核会将请求加入到请求队列中,而后将进程挂起,去处理其余的进程,直到内核缓冲区读取到数据之后,才会将内核缓冲区中的数据复制给用户进程缓冲区,而后通知进程,固然不一样的io模型,在调度方式上也是有一些差别的;因此内核缓冲区就在OS级别,提升了磁盘的IO效率。
到这里sendfile机制也就说完了,接着用刚刚讲的内核和用户进程的知识来讲一下AIO机制(异步非阻塞I/O),咱们先来看阻塞,先上图,看一下阻塞和非阻塞的区别
在两个图的对比当中,能够看到,在阻塞IO中,若是数据没有准备好你就只能等着,直到数据准备好之后才能够再继续执行,这对于Nginx的Worker来讲明显是很不适用的,而非阻塞的IO,当数据没有准备好时,我能够返回先去作其余的事情,过一会再来问一下,若是没有准备好,我再去处理其余的事情,直到你准备好,我过来开始拷贝数据,在这期间明显能够处理不少的事情,对于大量访问的时候也是很是好的,可是这样还有一个问题,虽然非阻塞,可是每隔一段时间就须要请求一下,也是很是浪费资源的,因此也就有了异步,也就是提供一种机制(select/poll/epoll/kquene这样的系统调用),让你能够同时监控多个事件,调用他们是阻塞的,可是能够设置超时时间,在超时时间以内,若是有事件准备好了就返回。
这样对于大量的并发就很是的友好了,这里的并发请求,是指未处理完的请求,线程只有一个,同时处理的请求只有一个,只是在请求间不断切换,切换是由于异步事件未准备好,主动让出的。这里的切换没有什么代价,能够理解为在循环处理多个准备好的事件;与多线程相比,不需建立线程,每一个请求占用的内存也不多,没有上下文切换,事件处理很是轻量级,没有上下文切换的开销,更多并发,只会占更多的内存,这也是如今的网络服务器基本都使用的方式。
最后还有一个mmap机制,mmap机制也就是内存映射,传统的web服务器进行页面输入的时候,都是将硬盘的页面先输入到内核缓冲区,再有内核缓冲区复制一份到web服务器上,mmap机制就是让内核缓冲区与磁盘进行映射,web服务器直接复制页面内容便可,省去了从硬盘复制到内核缓冲区这一过程。
上面就是把Nginx所涉及到的功能都说了一遍,从总体角度来看Nginx的功能是这样的。
咱们启动Nginx的时候首先会启动一个Master进程,Master进程会根据配置文件的要求fork相应个数的Worker进程(推荐设置worker数与cpu的核数一致,由于更多的worker,会致使进程竞争cpu资源,从而带来没必要要的上下文切换,设置为auto即为与cpu一致),当Worker进程接收客户端请求时,若是使用缓存功能,会从缓存中加载数据直接返回给客户端,若是客户端请求的内容内存中没有,就会将请求代理到后端服务器取资源,若是后端服务器是HTTP就使用http模块,若是是php就是用FastCGI模块,而后取出数据后又会将数据在本地缓存下来以提升性能,缓存时基于key-value结构,检索性能时O(1)恒定不变,把key缓存在内存中,检索起来也是很是迅速的。
在Worker接受请求这里还有一些操做,这里来补充一下。
在Nginx启动后,Master进程fork出的多个Worker进程,Master能监控Worker进程的运行状态,若是有Worker异常退出后,会自动启动新的Worker进程,在Nginx0.8以前,咱们是直接给Master进程发信号的,在重启或者从新加载配置的时候,Master进程在接收到信号以后,会先从新加载配置文件,而后再启动新的Worker进程,并向全部老的Worker进程发送再也不接受新请求的信号,而且在处理完全部未处理完的请求后退出,新的Worker在启动后,就开始接受新的请求了;在Nginx0.8以后,咱们不会直接对Master发送信号了,好比在执行./nginx -s reload的时候,会启动一个新的Nginx进程,该进程解析到reload参数后,知道要从新加载配置文件,它就会向Master进程发送信号,以后的处理与以前直接给Master进程发信号同样了。
还有一点就是所谓的“惊群现象”,在启动后,Master进程首先经过socket()来建立一个sock文件描述符来监听,而后fork出的Worker进程会继承父进程Master的socket文件描述符sockfd,以后Worker进程accept()后将建立已链接描述符(connected descriptor),而后经过已链接描述符来与客户端通讯,因为每个Worker进程都拥有Master的sockfd,那当连接进来的时候,全部的Worker都会收到通知,而且争着去创建连接,这就是“惊群现象”,这时大量的进程被激活又挂起,只有一个进程能够accept()到这个链接,就消耗了大量的系统资源。在Nginx中提供了一个accept_mutex的东西,这是在accept上加了一把共享锁,即每一个Worker进程在执行accept以前都须要先获取锁,获取不到就放弃执行accept(),只有有了这把锁才会去accept(),同一时刻就只有一个Worker进程去接收请求了,这样就不会出现惊群问题了。
可是我在查资料的时候发现了另一个状况
在对启动了20个worker的nginx进行压力测试的时候发现:若是把配置文件中event配置块中的accept_mutex开关打开(1.11.3版本以前默认开),就会出现worker压力不均,少许的worker的cpu利用率达到了98%,大部分的worker的压力只有1%左右;若是把accept_mutex关掉,全部的worker的压力差异就不大,并且QPS会有大幅提高;引用自博客园-sxhlinux
根据他的测试,在请求属于大量短连接的时候,打开accept_mutex选项是一个比较好的选择,避免了Worker争夺资源而形成的上下文切换以及try_lock的锁的开销,可是对于传输大量数据的TCP长连接来讲,打开accept_mutex将会致使压力集中在某个Worker进程上,特别是将worker_connection值设置过大的时候,影响更加明显,具体的设置,须要根据实际状况来进行断定。并且目前新版的Linux内核中增长了EPOLLEXCLUSIVE选项,nginx从1.11.3版本以后也增长了对NGX_EXCLUSIVE_EVENT选项的支持,这样就能够避免多worker的epoll出现的惊群效应,今后以后accept_mutex从默认的on变成了默认off。
很明显全部的文件配置都与具体的实际状况有关,要更好的配置好相关内容必须对全部的内容都要有一个详细的了解才能够。
参考文档
Zero Copy I: User-Mode Perspective
文章首发公众号和我的博客
公众号:无意的梦呓(wuxinmengyi)