“惊群”,看看nginx是怎么解决它的 (转自CSDN)

在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事 件发生时,这些线程/进程被同时唤醒,就是惊群。能够想见,效率很低下,许多进程被内核从新调度唤醒,同时去响应这一个事件,固然只有一个进程能处理事件 成功,其余的进程在处理该事件失败后从新休眠(也有其余选择)。这种性能浪费现象就是惊群。linux


惊群一般发生在server 上,当父进程绑定一个端口监听socket,而后fork出多个子进程,子进程们开始循环处理(好比accept)这个socket。每当用户发起一个 TCP链接时,多个子进程同时被唤醒,而后其中一个子进程accept新链接成功,余者皆失败,从新休眠。nginx


那么,咱们不能只用一个进程去accept新链接么?而后经过消息队列等同步方式使其余子进程处理这些新建的链接,这样惊群不就避免了?没错,惊群 是避免了,可是效率低下,由于这个进程只能用来accept链接。对多核机器来讲,仅有一个进程去accept,这也是程序员在本身创造accept瓶 颈。因此,我仍然坚持须要多进程处理accept事件。程序员


其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。你们能够写个简单的程序试 下,在父进程中bind,listen,而后fork出子进程,全部的子进程都accept这个监听句柄。这样,当新链接过来时,你们会发现,仅有一个子 进程返回新建的链接,其余子进程继续休眠在accept调用上,没有被唤醒。网络


可是很不幸,一般咱们的程序没那么简单,不会愿意阻塞在accept调用上,咱们还有许多其余网络读写事件要处理,linux下咱们爱用epoll 解决非阻塞socket。因此,即便accept调用没有惊群了,咱们也还得处理惊群这事,由于epoll有这问题。上面说的测试程序,若是咱们在子进程 内不是阻塞调用accept,而是用epoll_wait,就会发现,新链接过来时,多个子进程都会在epoll_wait后被唤醒!多线程


nginx就是这样,master进程监听端口号(例如80),全部的nginx worker进程开始用epoll_wait来处理新事件(linux下),若是不加任何保护,一个新链接来临时,会有多个worker进程在 epoll_wait后被唤醒,而后发现本身accept失败。如今,咱们能够看看nginx是怎么处理这个惊群问题了。负载均衡


nginx的每一个worker进程在函数ngx_process_events_and_timers中处理事件,(void) ngx_process_events(cycle, timer, flags);封装了不一样的事件处理机制,在linux上默认就封装了epoll_wait调用。咱们来看看 ngx_process_events_and_timers为解决惊群作了什么:socket

[cpp] view plaincopy函数

  1. void  post

  2. ngx_process_events_and_timers(ngx_cycle_t *cycle)  性能

  3. {  

  4. 。。。 。。。  

  5.     //ngx_use_accept_mutex表示是否须要经过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1  

  6.     if (ngx_use_accept_mutex) {  

  7.             //ngx_accept_disabled 表示此时满负荷,不必再处理新链接了,咱们在nginx.conf曾经配置了每个nginx worker进程可以处理的最大链接数,当达到最大数的 7/8时,ngx_accept_disabled为正,说明本nginx worker进程很是繁忙,将再也不去处理新链接,这也是个简单的负载均衡  

  8.         if (ngx_accept_disabled > 0) {  

  9.             ngx_accept_disabled--;  

  10.   

  11.         } else {  

  12.                 // 得到accept锁,多个worker仅有一个能够获得这把锁。得到锁不是阻塞过程,都是马上返回,获取成功的话 ngx_accept_mutex_held被置为1。拿到锁,意味着监听句柄被放到本进程的epoll中了,若是没有拿到锁,则监听句柄会被从 epoll中取出。  

  13.             if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  

  14.                 return;  

  15.             }  

  16.   

  17.                         // 拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,任何事件都将延后处理,会把 accept事件都放到ngx_posted_accept_events链表中,epollin|epollout事件都放到 ngx_posted_events链表中  

  18.             if (ngx_accept_mutex_held) {  

  19.                 flags |= NGX_POST_EVENTS;  

  20.   

  21.             } else {  

  22.                     //拿不到锁,也就不会处理监听的句柄,这个timer实际是传给epoll_wait的超时时间,修改成最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以避免新链接长时间没有获得处理  

  23.                 if (timer == NGX_TIMER_INFINITE  

  24.                     || timer > ngx_accept_mutex_delay)  

  25.                 {  

  26.                     timer = ngx_accept_mutex_delay;  

  27.                 }  

  28.             }  

  29.         }  

  30.     }  

  31. 。。。 。。。  

  32.         //linux下,调用ngx_epoll_process_events函数开始处理  

  33.     (void) ngx_process_events(cycle, timer, flags);  

  34. 。。。 。。。  

  35.         //若是ngx_posted_accept_events链表有数据,就开始accept创建新链接  

  36.     if (ngx_posted_accept_events) {  

  37.         ngx_event_process_posted(cycle, &ngx_posted_accept_events);  

  38.     }  

  39.   

  40.         //释放锁后再处理下面的EPOLLIN EPOLLOUT请求  

  41.     if (ngx_accept_mutex_held) {  

  42.         ngx_shmtx_unlock(&ngx_accept_mutex);  

  43.     }  

  44.   

  45.     if (delta) {  

  46.         ngx_event_expire_timers();  

  47.     }  

  48.   

  49.     ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,  

  50.                    "posted events %p", ngx_posted_events);  

  51.         //而后再处理正常的数据读写请求。由于这些请求耗时久,因此在ngx_process_events里NGX_POST_EVENTS标志将事件都放入ngx_posted_events链表中,延迟到锁释放了再处理。  

  52.     if (ngx_posted_events) {  

  53.         if (ngx_threaded) {  

  54.             ngx_wakeup_worker_thread(cycle);  

  55.   

  56.         } else {  

  57.             ngx_event_process_posted(cycle, &ngx_posted_events);  

  58.         }  

  59.     }  

  60. }  


从上面的注释能够看到,不管有多少个nginx worker进程,同一时刻只能有一个worker进程在本身的epoll中加入监听的句柄。这个处理accept的nginx worker进程置flag为NGX_POST_EVENTS,这样它在接下来的ngx_process_events函数(在linux中就是 ngx_epoll_process_events函数)中不会马上处理事件,延后,先处理完全部的accept事件后,释放锁,而后再处理正常的读写 socket事件。咱们来看下ngx_epoll_process_events是怎么作的:

[cpp] view plaincopy

  1. static ngx_int_t  

  2. ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)  

  3. {  

  4. 。。。 。。。  

  5.     events = epoll_wait(ep, event_list, (int) nevents, timer);  

  6. 。。。 。。。  

  7.     ngx_mutex_lock(ngx_posted_events_mutex);  

  8.   

  9.     for (i = 0; i < events; i++) {  

  10.         c = event_list[i].data.ptr;  

  11.   

  12. 。。。 。。。  

  13.   

  14.         rev = c->read;  

  15.   

  16.         if ((revents & EPOLLIN) && rev->active) {  

  17. 。。。 。。。  

  18. //有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理  

  19.             if (flags & NGX_POST_EVENTS) {  

  20.                 queue = (ngx_event_t **) (rev->accept ?  

  21.                                &ngx_posted_accept_events : &ngx_posted_events);  

  22.   

  23.                 ngx_locked_post_event(rev, queue);  

  24.   

  25.             } else {  

  26.                 rev->handler(rev);  

  27.             }  

  28.         }  

  29.   

  30.         wev = c->write;  

  31.   

  32.         if ((revents & EPOLLOUT) && wev->active) {  

  33. 。。。 。。。  

  34. //同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中  

  35.             if (flags & NGX_POST_EVENTS) {  

  36.                 ngx_locked_post_event(wev, &ngx_posted_events);  

  37.   

  38.             } else {  

  39.                 wev->handler(wev);  

  40.             }  

  41.         }  

  42.     }  

  43.   

  44.     ngx_mutex_unlock(ngx_posted_events_mutex);  

  45.   

  46.     return NGX_OK;  

  47. }  


看看ngx_use_accept_mutex在何种状况下会被打开:

[cpp] view plaincopy

  1. if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {  

  2.     ngx_use_accept_mutex = 1;  

  3.     ngx_accept_mutex_held = 0;  

  4.     ngx_accept_mutex_delay = ecf->accept_mutex_delay;  

  5.   

  6. else {  

  7.     ngx_use_accept_mutex = 0;  

  8. }  


当nginx worker数量大于1时,也就是多个进程可能accept同一个监听的句柄,这时若是配置文件中accept_mutex开关打开了,就将ngx_use_accept_mutex置为1。

再看看有些负载均衡做用的ngx_accept_disabled是怎么维护的,在ngx_event_accept函数中:

[cpp] view plaincopy

  1. ngx_accept_disabled = ngx_cycle->connection_n / 8  

  2.                       - ngx_cycle->free_connection_n;  


代表,当已使用的链接数占到在nginx.conf里配置的worker_connections总数的7/8以上时,ngx_accept_disabled为正,这时本worker将ngx_accept_disabled减1,并且本次再也不处理新链接。


最后,咱们看下ngx_trylock_accept_mutex函数是怎么玩的:

[cpp] view plaincopy

  1. ngx_int_t  

  2. ngx_trylock_accept_mutex(ngx_cycle_t *cycle)  

  3. {  

  4. //ngx_shmtx_trylock是非阻塞取锁的,返回1表示成功,0表示没取到锁  

  5.     if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  

  6.   

  7. //ngx_enable_accept_events会把监听的句柄都塞入到本worker进程的epoll中  

  8.         if (ngx_enable_accept_events(cycle) == NGX_ERROR) {  

  9.             ngx_shmtx_unlock(&ngx_accept_mutex);  

  10.             return NGX_ERROR;  

  11.         }  

  12. //ngx_accept_mutex_held置为1,表示拿到锁了,返回  

  13.         ngx_accept_events = 0;  

  14.         ngx_accept_mutex_held = 1;  

  15.   

  16.         return NGX_OK;  

  17.     }  

  18.   

  19. //处理没有拿到锁的逻辑,ngx_disable_accept_events会把监听句柄从epoll中取出  

  20.     if (ngx_accept_mutex_held) {  

  21.         if (ngx_disable_accept_events(cycle) == NGX_ERROR) {  

  22.             return NGX_ERROR;  

  23.         }  

  24.   

  25.         ngx_accept_mutex_held = 0;  

  26.     }  

  27.   

  28.     return NGX_OK;  

  29. }                                

OK,关于锁的细节是如何实现的,这篇限于篇幅就不说了,下篇帖子再来说。如今你们清楚nginx是怎么处理惊群了吧?简单了说,就是同一时刻只容许一个 nginx worker在本身的epoll中处理监听句柄。它的负载均衡也很简单,当达到最大connection的7/8时,本worker不会去试图拿 accept锁,也不会去处理新链接,这样其余nginx worker进程就更有机会去处理监听句柄,创建新链接了。并且,因为timeout的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。

相关文章
相关标签/搜索