事件架構處理流程
每個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伺服器的均衡效果。