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。則說明用戶希望server時間精度為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子進程監聽同樣port的設計,這樣多個子進程在accept建立新連接時會有爭搶,這會帶來的“驚群”問題。子進程數量越多越明顯,這會造成系統性能的下降。
master進程開始監聽Webport,fork出多個worker子進程,這些子進程同一時候監聽同一個Webport。普通情況下,有多少CPU核心就有配置多少個worker子進程。這樣全部的worker子進程都在承擔著Webserver的角色。從而發揮多核機器的威力。如果如今沒實用戶連入server。某一時刻恰好全部的子進程都休眠且等待新連接的系統調用,這時有一個用戶向server發起了連接,內核在收到TCP的SYN包時。會激活全部的休眠worker子進程。終于僅僅有最先開始運行accept的子進程能夠成功建立新連接,而其它worker子進程都將accept失敗。
這些accept失敗的子進程被內核喚醒是不必要的,他們被喚醒會的運行非??赡苁嵌嘤嗟?#xff0c;那么這一時刻他們占用了本不須要占用的資源,引發了不必要的進程切換,添加了系統開銷。
非常多操作系統的最新版本號的內核已經在事件驅動機制中攻克了驚群問題,但Nginx作為可移植性極高的webserver。還是在自身的應用層面上較好的攻克了這一問題。既然驚群是個多子進程在同一時刻監聽同一個port引起的。那么Nginx的解決方法也非常easy,它規定了同一時刻僅僅能有唯一一個worker子進程監聽Webport,這樣就不會發生驚群了。此時新連接時間就僅僅能喚醒唯一正在監聽port的worker子進程。
怎樣限制在某一時刻僅能有一個子進程監聽webport呢?在打開accept_mutex鎖的情況下。僅僅有調用ngx_trylock_accept_mutex方法后。當前的worker進程才會去試著監聽webport。
該方法詳細實現例如以下:
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方法的調用。當前進程的時間驅動模塊已經開始監 聽全部的port,這時須要把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等事件驅動模塊開始監控webport上的新連接事件。這種情況下,調用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的使用方法非常easy,當它為負數時,不會觸發負載均衡操作,正常獲取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進程就有機會去處理很多其它的新連接。以達到整個webserver的均衡效果。
?
版權聲明:本文博主原創文章,博客,未經同意不得轉載。
轉載于:https://www.cnblogs.com/yxwkf/p/4854229.html
總結
以上是生活随笔為你收集整理的Nginx——事件驱动机制(雷霆追风问题,负载均衡)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2015-09-14-初级vector
- 下一篇: springmvc 中controlle