高併發中的驚群效應簡介

2021-09-30 01:12:59 字數 4880 閱讀 2201

所謂驚群效應,就是多個程序或者執行緒在等待同乙個事件,當事件發生時,所有程序或者執行緒都會被核心喚醒。然後,通常只有乙個程序獲得了該事件,並進行處理;其他程序在發現獲取事件失敗後,又繼續進入了等待狀態。這在一定程度上降低了系統效能。

具體來說,驚群通常發生在伺服器的監聽等待呼叫上。伺服器建立監聽socket,然後fork多個程序,在每個程序中呼叫accept或者epoll_wait等待終端的連線。

驚醒所有程序/執行緒,導致n-1個程序/執行緒做了無效的排程、上下文切換,cpu瞬時增高。

多個程序/執行緒爭搶資源,所以涉及到同步問題,需對資源進行加鎖保護,加解鎖加大系統cpu開銷。

在高併發(多執行緒/多程序/多連線)中,會產生驚群的情況有:

以多程序為例,在主程序建立監聽描述符listenfd後,fork多個子程序,多個程序共享listenfd,accept是在每個子程序中,當乙個新連線來的時候,會發生驚群。

在核心2.6之前,所有程序accept都會驚醒,但只有乙個可以accept成功,其他返回egain。

在核心2.6及之後,解決了驚群問題,在核心中增加了乙個互斥等待變數。乙個互斥等待行為與睡眠基本類似,主要的不同點在於:

1)當乙個程序有wq_flag_excluseve標誌置位,它被新增到等待佇列的尾部。若沒有這個標誌置位,則新增到佇列首部。

2)當wake_up被在乙個等待佇列上呼叫時,它在喚醒第乙個有wq_flag_exclusive標誌的程序後停止。

對於互斥等待的行為,比如對乙個listen後的socket描述符,多執行緒阻塞accept時,系統核心只會喚醒所有正在等待此時間的佇列中的第乙個程序/執行緒,佇列中的其他程序/執行緒則繼續等待下一次事件的發生。這樣,就避免了多個程序/執行緒同時監聽同乙個socket描述符時的驚群問題。

epoll驚群分兩種:

一是在fork之前建立epollfd,所有程序共用乙個epoll。

二是在fork之後建立epollfd,每個程序獨用乙個epoll。

1. 主程序建立listenfd,建立epollfd。

2. 主程序fork多個子程序。

3. 每個子程序把listenfd加到epollfd中。

4. 當乙個新連線進來時,會觸發epoll驚群,多個子程序的epoll同時會觸發。

分析:這裡的epoll驚群跟accept驚群是類似的,共享乙個epollfd,加鎖或標記解決。在新版本的epoll中已解決,但在核心2.6及之前是存在的。

epoll_wait的驚群問題在這個patch epoll: add epollexclusive flag 21 jan 2016已經解決了。通過新增epollexclusive標誌標識,在喚醒時只喚醒乙個等待程序。相比而言,核心在解決accept的驚群時是作為乙個問題進行了修復,即無需設定標誌;而對於epoll_wait,則作為新增乙個功能選項。這主要是因為accept等待的是乙個socket,並且這個socket的連線只能被乙個程序處理,核心可以很明確的進行這個預設,因此,accept只喚醒乙個程序才是更優的選擇。而對於epoll_wait,等待的是多個socket上的事件,有連線事件、讀寫事件等,這些事件可以同時被乙個程序處理,也可以同時被多個程序分別處理,核心不能進行唯一程序處理的假定,因此,提供乙個設定標誌讓使用者決定。

1. 主程序建立listendfd。

2. 主程序建立多個子程序。

3. 每個子程序建立自已的epollfd。

4. 每個子程序把listenfd加入到epollfd中。

5. 當乙個連線進來時,會觸發epoll驚群,多個子程序epoll同時會觸發。

分析:雖然listenfd是同乙個,但每個子程序的epollfd是不同的epollfd,當新連線過來時,但核心不知道該發給哪個監聽程序,accept會觸發驚群。所以,對於這種情況,驚群還是會出現。

這裡說的nginx驚群,其實就是上面的問題(fork之後建立epollfd),下面看看nginx是怎麼處理驚群的。

在nginx中使用的epoll,是在建立子程序後建立的epollfd。因此,會出現上面的驚群問題,即每個子程序worker都會驚醒。

在nginx中,整個流程如下:

1 主線程建立listenfd。

2 主線程fork多個子程序(根據配置)。

3 子程序建立epollfd。

4 獲得accept鎖,只有乙個子程序把listenfd加到epollfd中,同一時間只有乙個程序會把監聽描述符加到epoll中。

5 迴圈監聽。

在nginx中,解決驚群的方法,使用了互斥鎖還解決。nginx的每個worker程序,都會在函式ngx_process_events_and_timers()中處理不同的事件,然後通過ngx_process_events()封裝了不同的事件處理機制,在linux上預設採用epoll_wait()。

nginx主要在ngx_process_events_and_timers()函式中解決驚群現象。

void ngx_process_events_and_timers(ngx_cycle_t *cycle)

else

if (ngx_accept_mutex_held) else }}

}... ...

(void) ngx_process_events(cycle, timer, flags); // 實際呼叫ngx_epoll_process_events函式開始處理

... ...

if (ngx_posted_accept_events)

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鍊錶中,延遲到鎖釋放了再處理。

}

分析:nginx裡採用了主動輪詢的方法,去把監聽描述符放到epoll中或從epoll移出(這個是nginx的精髓所在,因為大部分的併發架構都是被動的)。nginx中採用互斥鎖去解決誰來accept問題,保證了同一時刻只有乙個worker接收新連線(所以nginx並沒有驚群問題)。nginx根據自已的載負(最大連線的7/8)情況,決定去不去搶鎖,簡單方便地解決負載,防止程序因業務太多而導致所有業務都不及時處理。

總結: nginx採用互斥鎖和主動輪詢的方法,使得nginx中並無驚群。

在多執行緒設計中,經常會用到互斥和條件變數的問題。當乙個執行緒解鎖,並通知其他執行緒的時候,就會出現驚群的現象。

pthread_mutex_lock/pthread_mutex_unlock:執行緒互斥鎖的加鎖及解鎖函式。

pthread_cond_wait:執行緒池中的消費者執行緒等待執行緒條件變數被通知;

pthread_cond_signal/pthread_cond_broadcast:生產者執行緒通知執行緒池中的某個或一些消費者執行緒池,接收處理任務;

這裡的驚群現象出現在3裡,pthread_cond_signal,語義上看,是通知乙個執行緒。呼叫此函式後,系統會喚醒在相同條件變數上等待的乙個或多個執行緒(可參看手冊)。如果通知了多個執行緒,則發生了驚群。

傳統用法:所有執行緒共用乙個鎖,共用乙個條件變數。當pthread_cond_signal通知時,就可能會出現驚群。

解決驚群的方法:所有執行緒共用乙個鎖,每個執行緒有自已的條件變數。當pthread_cond_signal通知時,定向通知某個執行緒的條件變數,不會出現驚群。

linux核心的3.9版本帶來了so_reuseport特性,該特性支援多個程序或者執行緒繫結到同一埠,提高伺服器程式的效能,允許多個套接字bind()以及listen()同乙個tcp或udp埠,並且在核心層面實現負載均衡。

在未開啟so_reuseport的時候,由乙個監聽socket將新接收的連線請求交給各個工作者處理。

在使用so_reuseport後,多個程序可以同時監聽同乙個ip和埠,然後由核心決定將新連線傳送給哪個程序,顯然會降低每個worker接收新連線時的鎖競爭。

下面,讓我們好好比較一下多程序(執行緒)伺服器程式設計傳統方法和使用so_reuseport的區別。

執行在linux系統上的網路應用程式,為了利用多核的優勢,一般使用以下典型的多程序(多執行緒)伺服器模型:

1.單執行緒listener/accept,多個工作執行緒接受任務分發,雖然cpu工作負載不再成為問題,但是仍然存在問題:

(1)單執行緒listener,在處理高速率海量連線的時候,一樣會成為瓶頸。

(2)cpu快取行丟失套接字結構現象嚴重。

2.所有工作執行緒都accept()在同乙個伺服器套接字上呢?一樣存在問題:

(1)多執行緒訪問server socket鎖競爭嚴重。

(2)高負載情況下,執行緒之間的處理不均衡,有時高達3:1。

(3)導致cpu快取行跳躍(cache line bouncing)。

(4)在繁忙cpu上存在較大延遲。

上面兩種方法共同點就是:很難做到cpu之間的負載均衡,隨著核數的提公升,效能並沒有提公升。甚至伺服器的吞吐量cps(connection per second)會隨著核數的增加呈下降趨勢。

下面,我們就來看看so_reuseport解決了什麼問題:

(1)允許多個套接字bind()/listen()同乙個tcp/udp埠。每乙個執行緒擁有自己的伺服器套接字,在伺服器套接字上沒有鎖的競爭。

(2)核心層面實現負載均衡。

(3)安全層面,監聽同乙個埠的套接字只能位於同乙個使用者下面。

(4)處理新建連線時,查詢listener的時候,能夠支援在監聽相同ip和埠的多個socket之間均衡選擇。

當乙個連線到來的時候,系統到底是怎麼決定那個套接字來處理它?

對於不同核心,存在兩種模式,這兩種模式並不共存,一種叫做熱備份模式,另一種叫做負載均衡模式。3.9核心以後,全部改為負載均衡模式。

Nginx中的驚群現象解決方法

什麼是驚群現象?nginx中用了什麼方法來避免這種問題的發生?本篇就解決這兩個問題。驚群現象的定義與危害 nginx中解決驚群現象的方法 原始碼剖析 ngx int t ngx trylock accept mutex ngx cycle t cycle 將所有監聽連線的讀事件新增到當前的epoll...

web中的高併發

併發的問題,我們具體該關心什麼?講真話,高併發是個比較抽象的概念。很難有乙個統一的可衡量的標準。哪麼有一些其它維度的標準指標來衡量系統的效能嗎?搬出以前計算機課程裡邊的一些指標來跟大家聊聊。先宣告幾個概念,別打瞌睡。qps tps 每秒鐘 request 事務 數量,在網際網路領域,指每秒響應請求數...

java 中的併發工具類簡介

1 等待多執行緒完成的countdownlatch countdownlatch允許乙個或多個執行緒等待其他執行緒完成操作。2 同步屏障cyclicbarrier cyclicbarrier的字面意思是可迴圈使用 cyclic 的屏障 barrier 它要做的事情是,讓一組執行緒到達乙個屏障 也可以...