網(wǎng)站建設(shè)教程開源代碼下載競(jìng)價(jià)托管 微競(jìng)價(jià)
文章目錄
- 驚群概述
- Nginx 解決方案之鎖的設(shè)計(jì)
- 鎖結(jié)構(gòu)體
- 原子鎖創(chuàng)建
- 原子鎖獲取
- 原子鎖實(shí)現(xiàn)
- 原子鎖釋放
- Nginx 解決方案之驚群效應(yīng)
- 總結(jié):
驚群概述
在說nginx前,先來看看什么是“驚群”?簡(jiǎn)單說來,多線程/多進(jìn)程(linux下線程進(jìn)程也沒多大區(qū)別)等待同一個(gè)socket事件,當(dāng)這個(gè)事件發(fā)生時(shí),這些線程/進(jìn)程被同時(shí)喚醒,就是驚群。可以想見,效率很低下,許多進(jìn)程被內(nèi)核重新調(diào)度喚醒,同時(shí)去響應(yīng)這一個(gè)事件,當(dāng)然只有一個(gè)進(jìn)程能處理事件成功,其他的進(jìn)程在處理該事件失敗后重新休眠(也有其他選擇)。這種性能浪費(fèi)現(xiàn)象就是驚群。
驚群通常發(fā)生在server 上,當(dāng)父進(jìn)程綁定一個(gè)端口監(jiān)聽socket,然后fork出多個(gè)子進(jìn)程,子進(jìn)程們開始循環(huán)處理(比如accept)這個(gè)socket。每當(dāng)用戶發(fā)起一個(gè)TCP連接時(shí),多個(gè)子進(jìn)程同時(shí)被喚醒,然后其中一個(gè)子進(jìn)程accept新連接成功,余者皆失敗,重新休眠。
那么,我們不能只用一個(gè)進(jìn)程去accept新連接么?然后通過消息隊(duì)列等同步方式使其他子進(jìn)程處理這些新建的連接,這樣驚群不就避免了?沒錯(cuò),驚群是避免了,但是效率低下,因?yàn)檫@個(gè)進(jìn)程只能用來accept連接。對(duì)多核機(jī)器來說,僅有一個(gè)進(jìn)程去accept,這也是程序員在自己創(chuàng)造accept瓶頸。所以,我仍然堅(jiān)持需要多進(jìn)程處理accept事件。
其實(shí),在linux2.6內(nèi)核上,accept系統(tǒng)調(diào)用已經(jīng)不存在驚群了(至少我在2.6.18內(nèi)核版本上已經(jīng)不存在)。大家可以寫個(gè)簡(jiǎn)單的程序試下,在父進(jìn)程中bind,listen,然后fork出子進(jìn)程,所有的子進(jìn)程都accept這個(gè)監(jiān)聽句柄。這樣,當(dāng)新連接過來時(shí),大家會(huì)發(fā)現(xiàn),僅有一個(gè)子進(jìn)程返回新建的連接,其他子進(jìn)程繼續(xù)休眠在accept調(diào)用上,沒有被喚醒。
但是很不幸,通常我們的程序沒那么簡(jiǎn)單,不會(huì)愿意阻塞在accept調(diào)用上,我們還有許多其他網(wǎng)絡(luò)讀寫事件要處理,linux下我們愛用epoll解決非阻塞socket。所以,即使accept調(diào)用沒有驚群了,我們也還得處理驚群這事,因?yàn)閑poll有這問題。上面說的測(cè)試程序,如果我們?cè)谧舆M(jìn)程內(nèi)不是阻塞調(diào)用accept,而是用epoll_wait,就會(huì)發(fā)現(xiàn),新連接過來時(shí),多個(gè)子進(jìn)程都會(huì)在epoll_wait后被喚醒!
nginx就是這樣,master進(jìn)程監(jiān)聽端口號(hào)(例如80),所有的nginx worker進(jìn)程開始用epoll_wait來處理新事件(linux下),如果不加任何保護(hù),一個(gè)新連接來臨時(shí),會(huì)有多個(gè)worker進(jìn)程在epoll_wait后被喚醒,然后發(fā)現(xiàn)自己accept失敗。
Nginx 解決方案之鎖的設(shè)計(jì)
首先我們要知道在用戶空間進(jìn)程間鎖實(shí)現(xiàn)的原理,起始原理很簡(jiǎn)單,就是能弄一個(gè)讓所有進(jìn)程共享的東西,比如 mmap 的內(nèi)存,比如文件,然后通過這個(gè)東西來控制進(jìn)程的互斥。
Nginx 中使用的鎖是自己來實(shí)現(xiàn)的,這里鎖的實(shí)現(xiàn)分為兩種情況,一種是支持原子操作的情況,也就是由 NGX_HAVE_ATOMIC_OPS 這個(gè)宏來進(jìn)行控制的,一種是不支持原子操作,這是是使用文件鎖來實(shí)現(xiàn)。
鎖結(jié)構(gòu)體
如果支持原子操作,則我們可以直接使用 mmap,然后 lock 就保存 mmap 的內(nèi)存區(qū)域的地址
如果不支持原子操作,則我們使用文件鎖來實(shí)現(xiàn),這里 fd 表示進(jìn)程間共享的文件句柄,name 表示文件名
typedef struct {
#if (NGX_HAVE_ATOMIC_OPS) ngx_atomic_t *lock;
#else ngx_fd_t fd; u_char *name;
#endif
} ngx_shmtx_t;
原子鎖創(chuàng)建
// 如果支持原子操作的話,非常簡(jiǎn)單,就是將共享內(nèi)存的地址付給loc這個(gè)域
ngx_int_t ngx_shmtx_create(ngx_shmtx_t *mtx, void *addr, u_char *name)
{ mtx->lock = addr; return NGX_OK;
}
原子鎖獲取
TryLock,它是非阻塞的,也就是說它會(huì)嘗試的獲得鎖,如果沒有獲得的話,它會(huì)直接返回錯(cuò)誤。
Lock,它也會(huì)嘗試獲得鎖,而當(dāng)沒有獲得他不會(huì)立即返回,而是開始進(jìn)入循環(huán)然后不停的去獲得鎖,知道獲得。不過 Nginx 這里還有用到一個(gè)技巧,就是每次都會(huì)讓當(dāng)前的進(jìn)程放到 CPU 的運(yùn)行隊(duì)列的最后一位,也就是自動(dòng)放棄 CPU。
原子鎖實(shí)現(xiàn)
如果系統(tǒng)庫(kù)支持的情況,此時(shí)直接調(diào)用OSAtomicCompareAndSwap32Barrier,即 CAS。
#define ngx_atomic_cmp_set(lock, old, new) OSAtomicCompareAndSwap32Barrier(old, new, (int32_t *) lock)
如果系統(tǒng)庫(kù)不支持這個(gè)指令的話,Nginx 自己還用匯編實(shí)現(xiàn)了一個(gè)。
static ngx_inline ngx_atomic_uint_t ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old, ngx_atomic_uint_t set)
{ u_char res; __asm__ volatile ( NGX_SMP_LOCK " cmpxchgl %3, %1; " " sete %0; " : "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory"); return res;
}
原子鎖釋放
Unlock 比較簡(jiǎn)單,和當(dāng)前進(jìn)程 id 比較,如果相等,就把 lock 改為 0,說明放棄這個(gè)鎖。
#define ngx_shmtx_unlock(mtx) (void) ngx_atomic_cmp_set((mtx)->lock, ngx_pid, 0)
Nginx 解決方案之驚群效應(yīng)
nginx的每個(gè)worker進(jìn)程在函數(shù)ngx_process_events_and_timers中處理事件,(void) ngx_process_events(cycle, timer, flags);封裝了不同的事件處理機(jī)制,在linux上默認(rèn)就封裝了epoll_wait調(diào)用。我們來看看ngx_process_events_and_timers為解決驚群做了什么:
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
。。。 。。。//ngx_use_accept_mutex表示是否需要通過對(duì)accept加鎖來解決驚群?jiǎn)栴}。//當(dāng)nginx worker進(jìn)程數(shù)>1時(shí)且配置文件中打開accept_mutex時(shí),這個(gè)標(biāo)志置為1if (ngx_use_accept_mutex) {//ngx_accept_disabled表示此時(shí)滿負(fù)荷,沒必要再處理新連接了,我們?cè)趎ginx.conf曾經(jīng)配置了每一個(gè)nginx worker//進(jìn)程能夠處理的最大連接數(shù),當(dāng)達(dá)到最大數(shù)的7/8時(shí),ngx_accept_disabled為正,說明本nginx worker進(jìn)程非常繁忙,//將不再去處理新連接,這也是個(gè)簡(jiǎn)單的負(fù)載均衡if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//獲得accept鎖,多個(gè)worker僅有一個(gè)可以得到這把鎖。獲得鎖不是阻塞過程,都是立刻返回,獲取成功的話//ngx_accept_mutex_held被置為1。拿到鎖,意味著監(jiān)聽句柄被放到本進(jìn)程的epoll中了,如果沒有拿到鎖,//則監(jiān)聽句柄會(huì)被從epoll中取出。if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}//拿到鎖的話,置flag為NGX_POST_EVENTS,這意味著ngx_process_events函數(shù)中,任何事件都將延后處理,//會(huì)把a(bǔ)ccept事件都放到ngx_posted_accept_events鏈表中,epollin|epollout事件都放到//ngx_posted_events鏈表中if (ngx_accept_mutex_held) {flags |= NGX_POST_EVENTS;} else {//拿不到鎖,也就不會(huì)處理監(jiān)聽的句柄,這個(gè)timer實(shí)際是傳給epoll_wait的超時(shí)時(shí)間,//修改為最大ngx_accept_mutex_delay意味著epoll_wait更短的超時(shí)返回,以免新連接長(zhǎng)時(shí)間沒有得到處理if (timer == NGX_TIMER_INFINITE|| timer > ngx_accept_mutex_delay){timer = ngx_accept_mutex_delay;}}}}
。。。 。。。//linux下,調(diào)用ngx_epoll_process_events函數(shù)開始處理(void) ngx_process_events(cycle, timer, flags);
。。。 。。。//如果ngx_posted_accept_events鏈表有數(shù)據(jù),就開始accept建立新連接if (ngx_posted_accept_events) {ngx_event_process_posted(cycle, &ngx_posted_accept_events);}//釋放鎖后再處理下面的EPOLLIN EPOLLOUT請(qǐng)求if (ngx_accept_mutex_held) {ngx_shmtx_unlock(&ngx_accept_mutex);}if (delta) {ngx_event_expire_timers();}ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"posted events %p", ngx_posted_events);//然后再處理正常的數(shù)據(jù)讀寫請(qǐng)求。因?yàn)檫@些請(qǐng)求耗時(shí)久,所以在ngx_process_events里NGX_POST_EVENTS//標(biāo)志將事件都放入ngx_posted_events鏈表中,延遲到鎖釋放了再處理。if (ngx_posted_events) {if (ngx_threaded) {ngx_wakeup_worker_thread(cycle);} else {ngx_event_process_posted(cycle, &ngx_posted_events);}}
}
從上面的注釋可以看到,無論有多少個(gè)nginx worker進(jìn)程,同一時(shí)刻只能有一個(gè)worker進(jìn)程在自己的epoll中加入監(jiān)聽的句柄。這個(gè)處理accept的nginx worker進(jìn)程置flag為NGX_POST_EVENTS,這樣它在接下來的ngx_process_events函數(shù)(在linux中就是ngx_epoll_process_events函數(shù))中不會(huì)立刻處理事件,延后,先處理完所有的accept事件后,釋放鎖,然后再處理正常的讀寫socket事件。我們來看下ngx_epoll_process_events是怎么做的:
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
。。。 。。。events = epoll_wait(ep, event_list, (int) nevents, timer);
。。。 。。。ngx_mutex_lock(ngx_posted_events_mutex);for (i = 0; i < events; i++) {c = event_list[i].data.ptr;。。。 。。。rev = c->read;if ((revents & EPOLLIN) && rev->active) {
。。。 。。。
//有NGX_POST_EVENTS標(biāo)志的話,就把a(bǔ)ccept事件放到ngx_posted_accept_events隊(duì)列中,
//把正常的事件放到ngx_posted_events隊(duì)列中延遲處理if (flags & NGX_POST_EVENTS) {queue = (ngx_event_t **) (rev->accept ?&ngx_posted_accept_events : &ngx_posted_events);ngx_locked_post_event(rev, queue);} else {rev->handler(rev);}}wev = c->write;if ((revents & EPOLLOUT) && wev->active) {
。。。 。。。
//同理,有NGX_POST_EVENTS標(biāo)志的話,寫事件延遲處理,放到ngx_posted_events隊(duì)列中if (flags & NGX_POST_EVENTS) {ngx_locked_post_event(wev, &ngx_posted_events);} else {wev->handler(wev);}}}ngx_mutex_unlock(ngx_posted_events_mutex);return NGX_OK;
}
看看ngx_use_accept_mutex在何種情況下會(huì)被打開:
// 如果使用了 master worker,并且 worker 個(gè)數(shù)大于 1,并且配置文件里面有設(shè)置使用
// accept_mutex. 的話,設(shè)置ngx_use_accept_mutex if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) { ngx_use_accept_mutex = 1; // 下面這兩個(gè)變量后面會(huì)解釋。 ngx_accept_mutex_held = 0; ngx_accept_mutex_delay = ecf->accept_mutex_delay; } else { ngx_use_accept_mutex = 0; }
ngx_use_accept_mutex 這個(gè)變量,如果有這個(gè)變量,說明 Nginx 有必要使用 accept 互斥體,這個(gè)變量的初始化在 ngx_event_process_init 中。
ngx_accept_mutex_held 表示當(dāng)前是否已經(jīng)持有鎖。
ngx_accept_mutex_delay 表示當(dāng)獲得鎖失敗后,再次去請(qǐng)求鎖的間隔時(shí)間,這個(gè)時(shí)間可以在配置文件中設(shè)置的。
再看看有些負(fù)載均衡作用的ngx_accept_disabled是怎么維護(hù)的,在ngx_event_accept函數(shù)中:
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
表明,當(dāng)已使用的連接數(shù)占到在nginx.conf里配置的worker_connections總數(shù)的7/8以上時(shí),ngx_accept_disabled為正,這時(shí)本worker將ngx_accept_disabled減1,而且本次不再處理新連接。
最后,我們看下ngx_trylock_accept_mutex函數(shù)是怎么玩的:
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{ // 嘗試獲得鎖 if (ngx_shmtx_trylock(&ngx_accept_mutex)) { // 如果本來已經(jīng)獲得鎖,則直接返回Ok if (ngx_accept_mutex_held && ngx_accept_events == 0 && !(ngx_event_flags & NGX_USE_RTSIG_EVENT)) { return NGX_OK; } // 到達(dá)這里,說明重新獲得鎖成功,因此需要打開被關(guān)閉的listening句柄。 if (ngx_enable_accept_events(cycle) == NGX_ERROR) { ngx_shmtx_unlock(&ngx_accept_mutex); return NGX_ERROR; } ngx_accept_events = 0; // 設(shè)置獲得鎖的標(biāo)記。 ngx_accept_mutex_held = 1; return NGX_OK; } // 如果我們前面已經(jīng)獲得了鎖,然后這次獲得鎖失敗// 則說明當(dāng)前的listen句柄已經(jīng)被其他的進(jìn)程鎖監(jiān)聽// 因此此時(shí)需要從epoll中移出調(diào)已經(jīng)注冊(cè)的listen句柄// 這樣就很好的控制了子進(jìn)程的負(fù)載均衡 if (ngx_accept_mutex_held) { if (ngx_disable_accept_events(cycle) == NGX_ERROR) { return NGX_ERROR; } // 設(shè)置鎖的持有為0. ngx_accept_mutex_held = 0; } return NGX_OK;
}
如上代碼,當(dāng)一個(gè)連接來的時(shí)候,此時(shí)每個(gè)進(jìn)程的 epoll 事件列表里面都是有該 fd 的。搶到該連接的進(jìn)程先釋放鎖,在 accept。沒有搶到的進(jìn)程把該 fd 從事件列表里面移除,不必再調(diào)用 accept,造成資源浪費(fèi)。
同時(shí)由于鎖的控制(以及獲得鎖的定時(shí)器),每個(gè)進(jìn)程都能相對(duì)公平的 accept 句柄,也就是比較好的解決了子進(jìn)程負(fù)載均衡。
總結(jié):
簡(jiǎn)單了說,就是同一時(shí)刻只允許一個(gè)nginx worker在自己的epoll中處理監(jiān)聽句柄。它的負(fù)載均衡也很簡(jiǎn)單,當(dāng)達(dá)到最大connection的7/8時(shí),本worker不會(huì)去試圖拿accept鎖,也不會(huì)去處理新連接,這樣其他nginx worker進(jìn)程就更有機(jī)會(huì)去處理監(jiān)聽句柄,建立新連接了。而且,由于timeout的設(shè)定,使得沒有拿到鎖的worker進(jìn)程,去拿鎖的頻繁更高。