在說nginx前,先來看看什麼是「驚群」?簡單說來,多執行緒/多程序(linux下執行緒程序也沒多大區別)等待同乙個socket事件,當這個事件發生時,這些執行緒/程序被同時喚醒,就是驚群。可以想見,效率很低下,許多程序被核心重新排程喚醒,同時去響應這乙個事件,當然只有乙個程序能處理事件成功,其他的程序在處理該事件失敗後重新休眠(也有其他選擇)。這種效能浪費現象就是驚群。
驚群通常發生在server 上,當父程序繫結乙個埠監聽socket,然後fork出多個子程序,子程序們開始迴圈處理(比如accept)這個socket。每當使用者發起乙個tcp連線時,多個子程序同時被喚醒,然後其中乙個子程序accept新連線成功,餘者皆失敗,重新休眠。
那麼,我們不能只用乙個程序去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為解決驚群做了什麼:
[cpp]view plain
copy
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
else
//拿到鎖的話,置flag為ngx_post_events,這意味著ngx_process_events函式中,任何事件都將延後處理,會把accept事件都放到ngx_posted_accept_events鍊錶中,epollin|epollout事件都放到ngx_posted_events鍊錶中
if(ngx_accept_mutex_held) else
} } }
。。。 。。。
//linux下,呼叫ngx_epoll_process_events函式開始處理
(void
) ngx_process_events(cycle, timer, flags);
。。。 。。。
//如果ngx_posted_accept_events鍊錶有資料,就開始accept建立新連線
if(ngx_posted_accept_events)
//釋放鎖後再處理下面的epollin epollout請求
if(ngx_accept_mutex_held)
if(delta)
ngx_log_debug1(ngx_log_debug_event, cycle->log, 0,
"posted events %p"
, ngx_posted_events);
//然後再處理正常的資料讀寫請求。因為這些請求耗時久,所以在ngx_process_events裡ngx_post_events標誌將事件都放入ngx_posted_events鍊錶中,延遲到鎖釋放了再處理。
if(ngx_posted_events) else
} }
從上面的注釋可以看到,無論有多少個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 plain
copy
static
ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
else
} wev = c->write;
if((revents & epollout) && wev->active) else
} } ngx_mutex_unlock(ngx_posted_events_mutex);
return
ngx_ok;
}
看看ngx_use_accept_mutex在何種情況下會被開啟:
[cpp]view plain
copy
if(ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) else
當nginx worker數量大於1時,也就是多個程序可能accept同乙個監聽的控制代碼,這時如果配置檔案中accept_mutex開關開啟了,就將ngx_use_accept_mutex置為1。
再看看有些負載均衡作用的ngx_accept_disabled是怎麼維護的,在ngx_event_accept函式中:
[cpp]view plain
copy
ngx_accept_disabled = ngx_cycle->connection_n / 8
- 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 plain
copy
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
//ngx_accept_mutex_held置為1,表示拿到鎖了,返回
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;
return
ngx_ok;
} //處理沒有拿到鎖的邏輯,ngx_disable_accept_events會把監聽控制代碼從epoll中取出
if(ngx_accept_mutex_held)
ngx_accept_mutex_held = 0;
} return
ngx_ok;
}
ok,關於鎖的細節是如何實現的,這篇限於篇幅就不說了,下篇帖子再來講。現在大家清楚nginx是怎麼處理驚群了吧?簡單了說,就是同一時刻只允許乙個nginx worker在自己的epoll中處理監聽控制代碼。它的負載均衡也很簡單,當達到最大connection的7/8時,本worker不會去試圖拿accept鎖,也不會去處理新連線,這樣其他nginx worker程序就更有機會去處理監聽控制代碼,建立新連線了。而且,由於timeout的設定,使得沒有拿到鎖的worker程序,去拿鎖的頻繁更高。
「驚群」,看看nginx是怎麼解決它的
在說nginx前,先來看看什麼是 驚群 簡單說來,多執行緒 多程序 linux下執行緒程序也沒多大區別 等待同乙個socket事件,當這個事件發生時,這些執行緒 程序被同時喚醒,就是驚群。可以想見,效率很低下,許多程序被核心重新排程喚醒,同時去響應這乙個事件,當然只有乙個程序能處理事件成功,其他的程...
「驚群」,看看nginx是怎麼解決它的
在說nginx前,先來看看什麼是 驚群 簡單說來,多執行緒 多程序 linux下執行緒程序也沒多大區別 等待同乙個socket事件,當這個事件發生時,這些執行緒 程序被同時喚醒,就是驚群。可以想見,效率很低下,許多程序被核心重新排程喚醒,同時去響應這乙個事件,當然只有乙個程序能處理事件成功,其他的程...
詳解nginx驚群問題的解決方式
對於nginx的驚群問題,我們首先需要理解的是,在nginx啟動過程中,master程序會監聽配置檔案中指定的各個埠,然後master程序就會呼叫fork 方法建立各個子程序,根據程序的工作原理,子程序是會繼承父程序的全部記憶體資料以及監聽的埠的,也就是說worker程序在啟動之後也是會監聽各個埠的...