- 事件管理机制
Nginx是以事件驱动的,也就是说Nginx内部流程的向前推进基本都是靠各种事件的触发来驱动,否则Nginx将一直阻塞在函数epoll_wait()或suspend函数,Nginx事件一般分为I/O事件和定时事件,当一个事件到来后,监听FD的工作进程就开始处理事件,并执行回调函数,开始处理与响应。
I/O多路复制机制,Nginx封装了各种系统平台下的I/O事件处理机制,使得在跨平台时高效运行。下图列举了一些常见的I/O事件处理机制:
上图中,epoll模型是目前最高效的机制,它相比与select模型而言,最大的好处是不会随着被监控描述符数目的增加而导致效率急速下降。select模型是采用遍历扫描来判断每个描述符是否有事件发生,当监控的描述符数目越多,自然好事越大,而且由于受系统默认限制,select模型最多只能同时监控1024个描述符。相反,epoll模型同时监控的描述符个数不受限制;其次,epoll模型对事件的响应是触发式的,也即无需扫描,而只需对有事件发生的描述符做处理。epoll模型有两种触发方式,水平触发和边缘触发。
水平触发:支持阻塞和非阻塞状态,当描述符从未就绪变为就绪时,内核通过epoll告诉进程该描述符有事件发生,之后如果进程一直不对这个就绪状态做出任何操作,则内核会持续通知,直到事件处理完成。
边缘触发:高速工作方式(只支持非阻塞),当描述符从未就绪变为就绪时,内核通过epoll告诉进程该描述符有事件发生,之后就算进程一直不对这个就绪状态做任何操作,它也不提示,它只读一次。
- 进程负载均衡机制
说到I/O复用,自然有另一个问题,那就是负载均衡,如何调度进程。在一般情况下,配置Nginx执行时,工作进程都会有多个,由于各个工作进程相互独立的接受客户端请求、处理、响应,所以就可能会出现负载不均衡的情况,比如极端情况可能会是1个工作进程当前有3000个请求等待处理;而另一个进程当前却只有300个请求等待处理,这时候就设计到I/O负载均衡。如果进程并没有处于过载状态,那么就会去争用锁(争用套接字的控制权),如果争锁成功,就会把争取到的套接字加入自己的监控机制里,争锁失败就会把监听套接口从自己的监听事件中去掉。此时设置的监控机制的阻塞点的超时时间限制在一个比较短的范围内,超时更快,那么就更频繁的从阻塞中跳出来,也即有更多的机会去争抢互斥锁。当进程过载时,所做的工作就是计数,使ngx_accept_disabled减1,直到为0(当一个进程的活动连接数超过最大可承受连接数的7/8,此值将大于0,此时发生过载),那么此进程有可以去争锁。拥有锁的进程必须尽量缩短自身持锁时间,以让其他进程拥有更多机会。如果在处理新建连接事件的过程中,在监听套接口上又来了新的请求,此时此新的请求只有等到下次被进程争取倒锁并被加入监控机制中时才会被抓取出来。在同一时刻,监听套接口只能被一个进程监控,但是可以被多个进程拥有。
- 超时管理机制
另一个和事件有关的问题是超时Nginx的超时管理机制,事件超时意味着等待的事件没有在指定的时间内到达,Nginx有必要对这些可能发生超时的事件进行统一的管理 ,并在发生事件超时时做出相应的处理,比如回收资源,返回错误等。超时管理要解决两个问题:第一,超时事件对象的组织,Nginx采用的是红黑树;第二,超时事件对象的超时检测。Nginx提供了两种方案,一种是定时检测机制,每过一段时间对红黑树管理的超时事件进行扫描;另一种是先计算出距离当前最快发生超时的时间是多长(假定epoll_wait最长阻塞t秒)后去进行一次检测,然后更新当前时间。当某个进程初时化时,都需要建立一棵超时红黑树,当需要对某一事件进行超时监控时,就会把它加入到超时红黑树中。
上面提到的超时检测的两种方案中,方案一也就是定时去查看超时红黑树,是否有超时事件,该方案简单直观、容易理解 ,但是可能导致一些超时事件得不到及时的处理,我们只能通过具体的环境来修改定时时间。第二种方案由于是利用最快发生的事件的时刻减去当前时刻,这种方案在客户端请求较多的情况下,有可能要不停的去调用时间系统函数,从而影响性能。对于超时事件,直接将其移出红黑树,设置其超时标记,并调用该事件对应的回调函数进行处理。
- 请求处理与响应
Nginx在请求前都需要配置Nginx服务器,这里面主要包括server、location等内容,那和当有多个server上下文的时候,一个server配置至少一个监听套接口(对于只有端口没有Ip地址的server除外),所有的listen配置都以[port,addr]的形式组织在http核心配置的数组内,如果有server没有配置端口将启用默认端口。当FD与server绑定后,接下来要做的就是将监听套接口所对应的事件对象加入到Nginx的事件监控机制里(如果没有启动accept_mutex),那么就直接加入 ,否则就要先抢锁,然后抢到锁的才能加入到自己的事件监控机制内。需要注意的是主进程在创建完工作进程后并没有关闭这些监听套接口,但主进程却没有进行accept()客户端连接请求,因为如果工作进程down掉后,主进程会创建新的工作进程,并把这些监听套接口传递过去。
当有客户端发起连接请求,监控监听套接口的事件管理机制就会捕获到可读事件,工作进程便执行相应的回调函数,工作进程每次捕获到监听套接口上的事件后,只接受一个客户端请求,其它请求将等到再一次触发事件时才被接受。http的请求部分是调用回调函数(在handler模块处理)后,将请求发送到静态处理部分或后端服务器,在请求的数据返回时还需要经过filter模块的过滤然后才以字符串的形式存储在新申请的缓存中,将数据送到out发送链中。
在请求的过程中,还涉及到子请求,所谓子请求并不是由客户端直接发起的,它是由于Nginx在处理客户端请时,根据自身逻辑而内建的新请求,如下图所示:
子请求拥有主请求的几乎所有的特征,它的存在是为了提高并发能力,进一步提高效率。如果某个客户端的请求访问了多处资源(比如访问了a.html,b.html,c.html),那么针对每一处资源访问建立一个子请求并让它们同时进行,效率自然高很多。虽然一个进程只能处理一个请求,但是如果多个进程可以分别处理几个子请求,那从宏观上来讲,提高了请求的并发处理能力。目前官方提高的Nginx模块代码仅在filter阶段发起子请求,而此时的父请求的结果数据已经产生,如果在handler阶段发起子请求,由于此时父请求的结果数据还未产生,所以在数据同步方面需要做一些处理。那么多个子请求的顺序如何组织,才能将它当作最终的响应数据发回给客户端 ,否则客户端所请求的数据就不是原本正确的结果。
子请求的管理可以采用树加链表的形式,如下图所示,子请求以链表的形式链接:
每一个被触发的子请求会立即从链表中移除,但因最终的数据组合问题又有可能被停止而最终重新加入到链表中(虽然被触发了,但是迟迟数据没有返回,最后又没法被重新触发,所以需要Nginx重新将其加入到链表中)。
- 定位机制
Nginx里面location的存储是在队列里面,但是在匹配location时采用完全匹配,所以将队列里面的值以树的形式进行存储。在server定位中需要注意的是,如果有虚拟主机的存在,需要HTTP中带有host请求头,这样才能根据server内容做进一步的虚拟主机的物理定位 。