nginx事件机制

ngixn事件

主要是说nginx对TCP网络事件的管理,但是对于系统底层关于这块的管理又不全是一样。
比如linux 2.6之前的版本或unix大部分是poll或者select,2.6以后用的epoll.
分别对应于nginx的处理就是ngx_poll_module、ngx_select_module、ngx_epoll_module,
当然还有其他针对别的操作系统的事件模块。

事件的创建

事件创建是不需要被创建的,nginx会在启动时,预分配所有的读事件和写事件,只需要调用相应的方法,往事件模块中增加或者删除事件。
每一个连接会对应一个读事件,一个写事件。
读事件:当读事件被添加到事件驱动模块中时,对应该事件的TCP连接上一旦出现可读事件就会回调该事件的handler方法。
写事件:当写事件被添加到事件驱动模块中,写事件被操作时,当连接对应的套接字缓冲区中有可用空间时,事件收集器会处理这个可写事件。

ngixn对连接的定义

对于web服务器,每一个用户需要至少对应一个tcp连接,一个连接至少需要一个读事件,一个写事件。同时,还会有向上游服务器(这里请了解反向代理相关概念)请求的连接,
ngx_ connection_ t 连接池由connections和free_connections组成,connections是指向整个连接池数组的首部,free_connection是指向第一个空闲连接,连接之间又会形成一个单链表。
连接与事件:连接池、读事件、写事件、分别是三个一一对应的数组根据数组序号就可以将连接和事件对应起来

核心模块

ngx_events_module会在nginx启动时调用,会由ngx_event_core_module模块决策到底使用哪种机制去管理事件。如下图:

ngx_event_core_module

1.打开accept_mutex配置,
2.关闭ngx_use_accept_mutex锁
3.初始化红黑树实现的定时器
4.调用配置项指定的事件模块的Init方法
5.指定timer_resolution毫秒后调用ngx_timer_sigal_handler方法
6.向cycle->files数组预分配运行时使用的连接句柄
7.预分配cycle->connections数组充当连接池
8.预分配所有读事件到cycle->read_events数组
9.预分配所有写事件到cycle->write_events数组
10.将上述3个数组中相同序号下的读写事件放置到connections连接中
11.讲connections数组的连接构成单链表,由cycle->free_connections指向空闲连接首部
12.设置监听端口上读事件的处理方法
13.讲监听连接的读事件添加到时间驱动模块

epoll事件驱动模块

这个模块来看一下ngx_epoll_module如何基于epoll实现nginx的事件驱动,同时可以看看如何在nginx如何在几十万并发连接下做到高效利用服务器资源的。
场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收到TCP包),也就是说,在每一时刻,进程只需要处理这100万连接中的一 小部分连接。那么,如何才能高效地处理这种场景?
先来看看select和poll事件: 把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接。如果每次收集事件时,都把这100万连接的套接字传给操作系统,而由于用户态内存到内核态内存的大量复制,并且内核在寻找这些连接上没有处理事件会有很大的资源浪费。
附上linux图谱一张
epoll方式:将原先的select、poll方式分成了3个部分,epoll_create建立epoll对象,调用epoll_ctl向epoll对象添加这100万个连接套接字,调用epoll_wait收集发生的事件的连接,epoll_wait的效率显然非常高。所有添加到epoll中的事件都会与设备驱动程序建立回调关系。相应的事件在发生的时候会调用这里的回调方法。这个回调方法在内核中兼做ep_poll_callback,这些事件会被添加到双向链表中。epoll_wait检查是否有事件发生的连接只是检查双向链表是否存在元素而已。事件的查找是基于红黑树来实现的,也非常快。

事件驱动框架处理流程

1.accept方法建立新的连接
2.设置负载均衡阈值
3.连接池中获取一个连接
4.为连接分配内存
5.设置新的套接字属性
6.添加连接到epoll监控
7.处理新连接

解决负载群惊

群惊:程序开始监听web端口,fork多个子进程,这些子进程开始同时监听同一个端口,一般情况下,子进程数量和cpu核心数相同。如果没有用户连接服务器,某一个时刻恰好所有子进程都休眠且等待新连接的系统调用,这是有用户请求,内核收到SYN包后会激活所有子进程,当然,只有最开始执行accept的进程可以成功建立连接,但是其他的进程已经被唤醒了,这唤醒是不必要的浪费资源。
nginx在这里加了一步进程间的同步锁,只有获取到锁的进程才能监听web端口处理事件。关于锁的释放并不是在所有都处理完后释放,这样估计就单进程模式了。这边nginx是分开新连接的事件队列和普通的事件队列去解决的。区分之后会处理新连接队列,处理后立即释放锁资源。

文件的异步IO

这里的文件IO也是基于Linux底层实现的。Nginx把读取文件的操作异步提交至内核后,内核会通知IO设备独立执行,这样Nginx会继续充分占用cpu,当大量读事件堆积时,电梯算法优势会被发挥,降低随机读取磁盘的成本。
异步的io是不会读取文件缓存的,就算当前要被读取的文件存在io设备的缓存中,也是不会去读取的,所以选择异步IO的还是要看实际应用场景。

nginx与tcp


建立连接是在内核中完成的(上图),nginx只是从内核中获取了该连接。
如果accept中取出不及时,可能会导致linux的accept队列满,会影响导致syn队列满,而导致服务器无法再建立连接。

send方法内核示意图

rev方法内核示意图