標籤:style blog http color 使用 os strong 檔案
轉自:江南煙雨
驚群問題的產生
在建立串連的時候,Nginx處於充分發揮多核CPU架構效能的考慮,使用了多個worker子進程監聽相同連接埠的設計,這樣多個子進程在accept建立新串連時會有爭搶,這會帶來著名的“驚群”問題,子進程數量越多越明顯,這會造成系統效能的下降。一般情況 下,有多少CPU核心就有配置多少個worker子進程。假設現在沒有使用者連入伺服器,某一時刻恰好所有的子進程都休眠且等待新串連的系統調用(如 epoll_wait),這時有一個使用者向伺服器發起了串連,核心在收到TCP的SYN包時,會啟用所有的休眠worker子進程。最終只有最先開始執行 accept的子進程可以成功建立新串連,而其他worker子進程都將accept失敗。這些accept失敗的子進程被核心喚醒是不必要的,他們被喚 醒會的執行很可能是多餘的,那麼這一時刻他們佔用了本不需要佔用的資源,引發了不必要的進程切換,增加了系統開銷。
如何解決驚群問題-post事件處理機制很多作業系統的最新版本的核心已經在事件驅動機制中解決了驚群問題,但Nginx作為可移植性極高的web伺服器,還是在自身的應用程式層面上較好的解決了這一問題。Nginx規定了同一時刻只有唯一一個worker子進程監聽web連接埠,這一就不會發生驚群了,此時新串連事件只能喚醒唯一的正在監聽連接埠的worker子進程。如何限制在某一時刻是有一個子進程監聽web連接埠呢?在開啟accept_mutex鎖的情況下,只有調用ngx_trylock_accept_mutex方法後,當前的worker進程才會去試著監聽web連接埠。那麼,什麼時候釋放ngx_accept_mutex鎖呢?顯然不能等到這批事件全部執行完。因為這個worker進程上可能有許多活躍的串連,處理這些串連上的事件會佔用很長時間,其他worker進程很難得到處理新串連的機會。如何解決長時間佔用ngx_accept_mutex的 問題呢?這就要依靠post事件處理機制,Nginx設計了兩個隊列:ngx_posted_accept_events隊列(存放新串連事件的隊列)和 ngx_posted_events隊列(存放普通事件的隊列)。這兩個隊列都是ngx_event_t類型的雙鏈表。定義如下:
ngx_thread_volatile ngx_event_t *ngx_posted_accept_events;ngx_thread_volatile ngx_event_t *ngx_posted_events;
下面結合具體代碼進行分析驚群問題的解決。首先看 worker進程中ngx_process_events_and_timers事件處理函數(src/event/ngx.event.c),它處於 worker進程的ngx_worker_process_cycle方法中,迴圈處理時間,是事件驅動機制的核心,既會處理普通的網路事件,也會處理定 時器事件。ngx_process_events_and_timers是Nginx實際處理web業務的方法,所有業務的執行都是由它開始的,它涉及 Nginx完整的事件驅動機制!!特別重要~
voidngx_process_events_and_timers(ngx_cycle_t *cycle){ ngx_uint_t flags; ngx_msec_t timer, delta; if (ngx_timer_resolution) { timer = NGX_TIMER_INFINITE; flags = 0; } else { 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加鎖來解決驚群問題。當使用了master模式,nginx worker進程數>1時且設定檔中開啟accept_mutex時,這個標誌置為1 它在函數ngx_event_process_int中被設定,原始碼為: if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) { ngx_use_accept_mutex = 1; ngx_accept_mutex_held = 0; ngx_accept_mutex_delay = ecf->accept_mutex_delay; } else { ngx_use_accept_mutex = 0; }*/ if (ngx_use_accept_mutex) { //負載平衡處理 if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { //調用ngx_trylock_accept_mutex方法,嘗試擷取accept鎖 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } //拿到鎖 if (ngx_accept_mutex_held) { /*給flags增加標記NGX_POST_EVENTS,這個標記作為處理時間核心函數ngx_process_events的一個參數,這個函數中所有事件將延後處理。會把accept事件都放到ngx_posted_accept_events鏈表中,epollin|epollout普通事件都放到ngx_posted_events鏈表中 */ 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; //事件處理核心函數 (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); } //釋放鎖後再處理ngx_posted_events鏈表中的普通事件 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); } }}
上面代碼中要進行說明的是,flags被設定後作為函數ngx_process_events方法的一個參數,在epoll模組中這個介面的實現方法是ngx_epoll_process_events(其具體代碼見http://blog.csdn.net/xiajun07061225/article/details/9250341)。當falgs標誌位含有nGX_POST_EVENTS時是不會立即呼叫事件的handler回調方法的,代碼如下所示:
//事件需要延後處理 if (flags & NGX_POST_EVENTS) { /*如果要在post隊列中延後處理該事件,首先要判斷它是新連線時間還是普通事件 以確定是把它加入到ngx_posted_accept_events隊列或者ngx_posted_events隊列中。*/ queue = (ngx_event_t **) (rev->accept ? &ngx_posted_accept_events : &ngx_posted_events); //將該事件添加到相應的延後隊列中 ngx_locked_post_event(rev, queue); } else { //立即呼叫事件回調方法來處理這個事件 rev->handler(rev); }通過上面的 代碼可以看出,先處理ngx_posted_accept_events隊列中的事件,處理完畢後立即釋放ngx_accept_mutex鎖,接著再處 理ngx_posted_events隊列中事件。這樣大大減少了ngx_accept_mutex鎖佔用的時間
下面看看ngx_trylock_accept_mutex的具體實現(src/event/ngx_event_accept.c):
ngx_int_tngx_trylock_accept_mutex(ngx_cycle_t *cycle){ //嘗試擷取accept_mutex鎖。注意是非阻塞的。返回1表示成功,返回0表示失敗。 //ngx_accept_mutex 定義:ngx_shmtx_t ngx_accept_mutex;(ngx_shmtx_t是Nginx封裝的互斥鎖,用於經常間同步) if (ngx_shmtx_trylock(&ngx_accept_mutex)) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex locked"); //擷取到鎖,但是標誌位ngx_accept_mutex_held為1,表示當前進程已經擷取到鎖了,立即返回。 if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { return NGX_OK; } //將所有監聽事件添加到當前的epoll等事件驅動模組中 if (ngx_enable_accept_events(cycle) == NGX_ERROR) { //添加失敗,必須釋放互斥鎖 ngx_shmtx_unlock(&ngx_accept_mutex); return NGX_ERROR; } //標誌位設定 ngx_accept_events = 0; //當前進程已經擷取到鎖 ngx_accept_mutex_held = 1; return NGX_OK; } ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "accept mutex lock failed: %ui", ngx_accept_mutex_held); //擷取鎖失敗,但是標誌位ngx_accept_mutex_held仍然為1,即當前進程還處在擷取到鎖的狀態,這是不正確的 if (ngx_accept_mutex_held) { //將所有監聽事件從事件驅動模組中移除 if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; } //沒有擷取到鎖,設定標誌位 ngx_accept_mutex_held = 0; } return NGX_OK;}
調用這個方法的結果是,要麼唯一擷取到鎖且其epoll等事件驅動模組開始監控web連接埠上的新串連事件。這種情況下調用process_events方 法時就會既處理已有串連上的事件,也處理新串連的事件。要麼沒有擷取到鎖,當前進程不會收到新串連事件。這種情況下process_events只處理已 有串連上的事件。
參考:http://russelltao.iteye.com/blog/1405352