首页 > 代码库 > Nginx——事件驱动机制(惊群问题,负载均衡)
Nginx——事件驱动机制(惊群问题,负载均衡)
事件框架处理流程
每个worker子进程都在ngx_worker_process_cycle方法中循环处理事件,处理分发事件则在ngx_worker_process_cycle方法中调用ngx_process_events_and_timers方法,循环调用该方法就是 在处理所有事件,这正是事件驱动机制的核心。该方法既会处理普通的网络事件,也会处理定时器事件。
ngx_process_events_and_timers方法中核心操作主要有以下3个:
1) 调用所使用事件驱动模块实现的process_events方法,处理网络事件
2) 处理两个post事件队列中的事件,实际上就是分别调用ngx_event_process_posted(cycle, &ngx_posted_accept_events)和ngx_event_process_posted(cycle,&ngx_posted_events)方法
3) 处理定时事件,实际上就是调用ngx_event_expire_timers()方法
下面是ngx_process_events_and_timers方法中的时间框架处理流程图以及源代码,可以结合理解:
源代码如下:
void ngx_process_events_and_timers(ngx_cycle_t *cycle) { ngx_uint_t flags; ngx_msec_t timer, delta; /*如果配置文件中使用了timer_resolution配置项,也就是ngx_timer_resolution值大于0, 则说明用户希望服务器时间精度为ngx_timer_resolution毫秒。这时,将ngx_process_changes 的timer参数设为-1,告诉ngx_process_change方法在检测时间时不要等待,直接搜集所有已经 就绪的时间然后返回;同时将flag参数初始化为0,它是在告诉ngx_process_changes没有任何附加 动作*/ if (ngx_timer_resolution) { timer = NGX_TIMER_INFINITE; flags = 0; } else { /*如果没有使用timer_resolution,那么将调用ngx_event_find_timer()方法,获取最近一个将要 触发的时间距离现在有多少毫秒,然后把这个值赋予timer参数,告诉ngx_process_change方法在 检测事件时如果没有任何事件,最多等待timer毫秒就返回;将flag参数设置为UPDATE_TIME,告诉 ngx_process_change方法更新换成的时间*/ timer = ngx_event_find_timer(); flags = NGX_UPDATE_TIME; #if (NGX_THREADS) if (timer == NGX_TIMER_INFINITE || timer > 500) { timer = 500; } #endif } /*ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。 当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1 */ if (ngx_use_accept_mutex) { /*ngx_accept_disabled表示此时满负荷,没必要再处理新连接了, 我们在nginx.conf曾经配置了每一个nginx worker进程能够处理的最大连接数, 当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙, 将不再去处理新连接,这也是个简单的负载均衡 */ if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { /*获得accept锁,多个worker仅有一个可以得到这把锁。获得锁不是阻塞过程, 都是立刻返回,获取成功的话ngx_accept_mutex_held被置为1。拿到锁,意味 着监听句柄被放到本进程的epoll中了,如果没有拿到锁,则监听句柄会被 从epoll中取出。*/ if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } /*拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中, 任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中, epollin | epollout事件都放到ngx_posted_events链表中 */ if (ngx_accept_mutex_held) { flags |= NGX_POST_EVENTS; } else { /*获取锁失败,意味着既不能让当前worker进程频繁的试图抢锁,也不能让它经过太长事件再去抢锁 下面的代码:即使开启了timer_resolution时间精度,牙需要让ngx_process_change方法在没有新 事件的时候至少等待ngx_accept_mutex_delay毫秒之后再去试图抢锁 而没有开启时间精度时, 如果最近一个定时器事件的超时时间距离现在超过了ngx_accept_mutex_delay毫秒,也要把timer设 置为ngx_accept_mutex_delay毫秒,这是因为当前进程虽然没有抢到accept_mutex锁,但也不能让 ngx_process_change方法在没有新事件的时候等待的时间超过ngx_accept_mutex_delay,这会影响 整个负载均衡机制*/ if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) { timer = ngx_accept_mutex_delay; } } } } //计算ngx_process_events消耗的时间 delta = ngx_current_msec; //linux下,调用ngx_epoll_process_events函数开始处理 (void) ngx_process_events(cycle, timer, flags); //函数处理消耗时间 delta = ngx_current_msec - delta; ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "timer delta: %M", delta); //如果ngx_posted_accept_events链表有数据,就开始accept建立新连接 if (ngx_posted_accept_events) { ngx_event_process_posted(cycle, &ngx_posted_accept_events); } //释放锁后再处理下面的EPOLLIN EPOLLOUT请求 if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } //如果ngx_process_events消耗的时间大于0,那么这时可能有新的定时器事件触发 if (delta) { //处理定时事件 ngx_event_expire_timers(); } ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "posted events %p", ngx_posted_events); //ngx_posted_events链表中有数据,进行处理 if (ngx_posted_events) { if (ngx_threaded) { ngx_wakeup_worker_thread(cycle); } else { ngx_event_process_posted(cycle, &ngx_posted_events); } } }
惊群问题
在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来的“惊群”问题,子进程数量越多越明显,这会造成系统性能的下降。
master进程开始监听Web端口,fork出多个worker子进程,这些子进程同时监听同一个Web端口。一般情况下,有多少CPU核心就有配置多少个worker子进程,这样所有的worker子进程都在承担着Web服务器的角色,从而发挥多核机器的威力。假设现在没有用户连入服务器,某一时刻恰好所有的子进程都休眠且等待新连接的系统调用,这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最终只有最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都将accept失败。这些accept失败的子进程被内核唤醒是不必要的,他们被唤醒会的执行很可能是多余的,那么这一时刻他们占用了本不需要占用的资源,引发了不必要的进程切换,增加了系统开销。
很多操作系统的最新版本的内核已经在事件驱动机制中解决了惊群问题,但Nginx作为可移植性极高的web服务器,还是在自身的应用层面上较好的解决了这一问题。既然惊群是个多子进程在同一时刻监听同一个端口引起的,那么Nginx的解决方法也很简单,它规定了同一时刻只能有唯一一个worker子进程监听Web端口,这样就不会发生惊群了,此时新连接时间就只能唤醒唯一正在监听端口的worker子进程。
如何限制在某一时刻仅能有一个子进程监听web端口呢?在打开accept_mutex锁的情况下,只有调用ngx_trylock_accept_mutex方法后,当前的worker进程才会去试着监听web端口。
该方法具体实现如下:
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) { /* 使用进程间的同步锁,试图获取accept_mutex。注意,ngx_trylock_accept_mutex返回1表示成功 拿到锁,返回0表示获取锁失败。这个获取所的过程是非阻塞的,此时一旦锁被其他worker子进程占 用,该方法会立刻返回。 */ if (ngx_shmtx_trylock(&ngx_accept_mutex)) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex locked"); /*如果获取到accept_mutex锁,但ngx_accept_mutex_held为1,则立刻返回。ngx_accept_mutex_held 是一个标志位,当它为1时,表示当前进程已经获取到锁了*/ if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { //ngx_accept_mutex锁之前已经获取到了,立刻返回 return NGX_OK; } //将所有监听连接的事件添加到当前epoll等事件驱动模块中 if (ngx_enable_accept_events(cycle) == NGX_ERROR) { /*既然将监听句柄添加到事件驱动模块失败,就必须释放ngx_accept_mutex锁*/ ngx_shmtx_unlock(&ngx_accept_mutex); return NGX_ERROR; } /*经过ngx_enable_accept_events方法的调用,当前进程的时间驱动模块已经开始监 听所有的端口,这时需要把ngx_accept_mutex_heald标志置为1,方便本进程的其他模 块了解它目前已经获取到了锁*/ ngx_accept_events = 0; ngx_accept_mutex_held = 1; return NGX_OK; } /*如果ngx_shmtx_trylock返回0,则表明获取ngx_accept_mutex锁失败,这时如果 ngx_accept_mutex_held标志还为1,即当前进程还在获取到锁的状态,这显然不正确,需要处理*/ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex lock failed: %ui", ngx_accept_mutex_held); if (ngx_accept_mutex_held) { /*ngx_disable_accept_events(会将所有监听连接的读事件从事件驱动模块中移除*/ if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; } /*在没有获取到ngx_accept_mutex锁时,必须把ngx_accept_mutex_held置为0*/ ngx_accept_mutex_held = 0; } return NGX_OK; }
在上面的代码中,ngx_accept_mutex是进程间的同步锁(见http://blog.csdn.net/walkerkalr/article/details/38237147),ngx_accept_mutex_held是当前进程的一个全局变量,他们的定义如下:
ngx_shmtx_t ngx_accept_mutex; ngx_uint_t ngx_accept_mutex_held;
因此,在调用ngx_try_accept_mutex方法后,如果没有获取到锁,当前进程调用process_events时只能处理已有连接上的事件。如果唯一获取到锁且其epoll等事件驱动模块开始监控web端口上的新连接事件。这种情况下,调用process_events时就会既处理已有连接上的事件,也处理新连接的事件,但这样的话,什么时候释放ngx_accept_mutex锁呢?如果等到这批事件全部执行完,由于这个worker进程上可能有很多活跃的连接,处理这些连接上的事件会占用很长时间,也就是说,会很长时间都没有释放ngx_accept_mutex锁,这样,其他worker进程就很难得到处理新连接的机会。
如何解决长时间占用ngx_accept_mutex的问题呢?这就要依靠ngx_posted_accept_events队列(存放新连接事件的队列)和ngx_posted_events队列(存放普通事件的队列)。实际上ngx_posted_accepted_events队列和ngx_posted_events队列把事件进行了归类,以使先处理ngx_posted_accept_events队列中的事件,处理完后就要释放ngx_accept_mutex锁,接着再处理ngx_posted_events队列中的时间,这样就大大减少了ngx_accept_mutex锁占用的时间。
负载均衡
在建立连接时,在多个子进程争抢处理一个新连接时间时,一定只有一个worker子进程最终会成功简历连接,随后,它会一直处理这个连接直到连接关闭。那么,如果有的子进程很勤奋,他们抢着建立并处理了大部分连接,而有的子进程则运气不好,只处理了少量连接,这对多核CPU架构下的应用是很不利的,因为子进程之间应该是平等的,每个子进程应该尽量独占一个CPU核心。子进程间负载不均衡,必定会影响整个服务的性能。
与惊群问题的解决方法一样,只有打开了accept_mutex锁,才能实现子进程间的负载均衡。在这里,初始化了一个全局变量ngx_accept_disabled,他就是负载均衡机制实现的关键阈值,实际上它就是一个整型数据。
ngx_int_t ngx_accept_disabled;
这个阈值是与连接池中连接的使用密切相关的,在建立连接时会进行赋值,如下所示
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
因此,在启动时该阈值是一个负值,其绝对值是连接总数的7/8。其实ngx_accept_disabled的用法很简单,当它为负数时,不会触发负载均衡操作,正常获取accept锁,试图处理新连接。而当ngx_accept_disabled是正数时,就会触发Nginx进行负载均衡操作了,nginx此时不再处理新连接事件,取而代之的仅仅是ngx_accept_disabled值减1,,这表示既然经过一轮事件处理,那么相对负载肯定有所减小,所以要相应调整这个值。如下所示
if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { //调用ngx_trylock_accept_mutex方法,尝试获取accept锁 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; }
Nginx各worker子进程间的负载均衡仅在某个worker进程处理的连接数达到它最大处理总数的7/8时才会触发,这时该worker进程就会减少处理新连接的机会,这样其他较空闲的worker进程就有机会去处理更多的新连接,以达到整个web服务器的均衡效果。