这是我参与更文挑战的第20天,活动详情查看: 更文挑战
Nginx的进程
Nginx在启动后会有一个master进程和多个worker进程
master进程主要用来管理worker进程,master进程接收来自外界发来的信号,再根据信号做相关操作,比如我们一般用kill -HUP pid来从容地重启Nginx或重新加载配置,在从容重启过程中,服务是不中断的,master进程在接收到HUP信号后,会先重新加载配置文件,然后再启动新的worker进程,并向所有老的worker进程发送信号,告诉他们可以退出了,新的worker在启动后就开始接收新的请求,而老的worker在收到来自master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后再退出 直接给master进程发送信号是比较老的操作方式,Nginx在0.8版本之后引入了一系列命令行参数来方便管理,比如./nginx -s reload就是重启Nginx,./nginx -s stop就是来停止Nginx的运行
每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd),然后再fork出多个worker进程,所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接;当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接;即一个请求完全由worker进程来处理,而且只在一个worker进程中处理
worker采用进程模型
- 每个worker进程都是独立的进程,不需要加锁,省掉了锁带来的开销,同时在编程以及问题查找时也会方便很多
- 独立的进程可以让互相之间不会影响:一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程
- Nginx采用异步非阻塞方式处理请求,因此可以同时处理成千上万个请求;apache的常用工作方式是每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求,这会对操作系统造成很大的压力,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销也很大,性能就上不去了
Nginx异步非阻塞工作方式
- Nginx采用异步非阻塞的事件处理机制,具体到系统底层调用就是像select/poll/epoll/kqueue这样的系统调用,它们提供了一种可以同时监控多个事件的机制,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回
- 拿epoll为例,当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面,这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着
- 因为线程只有一个,所以同时能处理的请求也只有一个,只是在请求间进行不断地切换,切换也是因为异步事件未准备好而主动让出的,这里的切换是没有任何代价;与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级,并发数再多也不会导致无谓的资源浪费(上下文切换),更多的并发数,只是会占用更多的内存而已
- Nginx推荐设置worker的个数为cpu的核数,因为更多的worker数只会导致进程来竞争cpu资源,从而带来不必要的上下文切换,而且nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效;像这种小的优化在Nginx中很多,比如在做4个字节的字符串比较时,会将4个字符转换成一个int型,再作比较,以减少cpu的指令数等等
Nginx代码中的定时器处理机制
由于epoll_wait等函数在调用时可以设置一个超时时间,所以Nginx借助这个超时时间来实现定时器;nginx里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入epoll_wait前,先从该红黑树里面拿到所有定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait;所以,当没有事件产生,也没有中断信号时,epoll_wait会超时,也就是说,定时器事件到了,这时,nginx会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件
当我们写Nginx代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件
connection的概念
- Nginx中的connection就是对tcp连接的封装,其中包括连接的socket,读事件,写事件
- 利用Nginx封装的connection可以很方便的使用Nginx来处理与连接相关的事情,比如,建立连接,发送与接受数据等
- Nginx中的http请求的处理就是建立在connection之上的,所以Nginx不仅可以作为一个web服务器,也可以作为邮件服务器
- 利用Nginx提供的connection,也可以与任何后端服务打交道
Nginx使用connection处理连接的生命周期
- Nginx在启动时会解析配置文件,得到需要监听的端口与ip地址,然后在Nginx的master进程先初始化好这个监控的socket(创建socket,设置addrreuse等选项,绑定到指定的ip地址端口,再listen),然后再fork出多个子进程出来,然后子进程会竞争accept新的连接
- 客户端向Nginx发起连接,当客户端与服务端通过三次握手建立好一个连接后,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
worker进程的最大连接数
- Nginx的每个worker进程都有一个连接数的最大上限,这个上限与系统对fd的限制不一样,在操作系统中,通过ulimit -n可以得到一个进程所能够打开的fd的最大数,即nofile,因为每个socket连接会占用掉一个fd,所以这也会限制我们进程的最大连接数,当然也会直接影响到我们程序所能支持的最大并发数,当fd用完后,再创建socket时就会失败
- Nginx通过设置worker_connectons来设置每个进程支持的最大连接数,如果该值大于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,因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接
多worker进程公平竞争机制
- 多个空闲的进程会竞争一个客户端连接,如果某个进程得到accept的机会比较多,它的空闲连接很快就用完了,如果不提前做一些控制,当accept到一个新的tcp连接后,因为无法得到空闲连接,而且无法将此连接转交给其它进程,最终会导致此tcp连接得不到处理而中止掉
- Nginx为保证进程之间的公平竞争,要求只有获得accept_mutex的进程才会去添加accept事件,也就是说,Nginx会控制进程是否添加 accept事件,Nginx使用一个叫ngx_accept_disabled的变量来控制是否去竞争accept_mutex锁,首先计算ngx_accept_disabled的值,这个值是Nginx单进程的所有连接总数的八分之一,减去剩下的空闲连接数量,得到的这个ngx_accept_disabled有一个规律,当剩余连接数小于总连接数的八分之一时,其值才大于0,而且剩余的连接数越小,这个值越大,当ngx_accept_disabled大于0时,不会去尝试获取accept_mutex锁,并且将ngx_accept_disabled减1,于是每次执行到此处时,都会去减1,直到小于0
- 不去获取accept_mutex锁,就是等于让出获取连接的机会,当空余连接越少时,ngx_accept_disable 越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大,不去accept,自己的连接就控制下来了,其它进程的连接池就会得到利用,Nginx就控制了多进程间连接的平衡了