「驚羣」,看看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進程,去拿鎖的頻繁更高。

相關文章
相關標籤/搜索