高並發中的驚群效應


版權聲明:本文為CSDN博主「second60」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/second60/article/details/81252106

 

1.驚群效應簡介

當你往一群鴿子中間扔一塊食物,雖然最終只有一個鴿子搶到食物,但所有鴿子都會被驚動來爭奪,沒有搶到食物的鴿子只好回去繼續睡覺, 等待下一塊食物到來。這樣,每扔一塊食物,都會驚動所有的鴿子,即為驚群。

 

簡單地說:就是扔一塊食物,所有鴿子來搶,但最終只一個鴿子搶到了食物。

語義分析:食物只有一塊,最終只有一個鴿子搶到,但是驚動了所有鴿子,每個鴿子都跑過來,消耗了每個鴿子的能量。(這個很符合達爾文的進化論,物種之間的競爭,適者生存。)

 

2. 操作系統的驚群

在多進程/多線程等待同一資源時,也會出現驚群。即當某一資源可用時,多個進程/線程會驚醒,競爭資源。這就是操作系統中的驚群。

3. 驚群的壞處  

  3.1 壞處

驚醒所有進程/線程,導致n-1個進程/線程做了無效的調度,上下文切換,cpu瞬時增高
多個進程/線程爭搶資源,所以涉及到同步問題,需對資源進行加鎖保護,加解鎖加大系統CPU開銷
3.2 其他
1. 在某些情況:驚群次數少/進(線)程負載不高,驚群可以忽略不計

 

 

4 驚群的幾種情況


在高並發(多線程/多進程/多連接)中,會產生驚群的情況有:

accept驚群
epoll驚群
nginx驚群
線程池驚群

  4.1 accept驚群(新版內核已解決)


以多進程為例,在主進程創建監聽描述符listenfd后,fork()多個子進程,多個進程共享listenfd,accept是在每個子進程中,當一個新連接來的時候,會發生驚群。

 

由上圖所示:

主線程創建了監聽描述符listenfd = 3
主線程fork 三個子進程共享listenfd=3
當有新連接進來時,內核進行處理
在內核2.6之前,所有進程accept都會驚醒,但只有一個可以accept成功,其他返回EGAIN。

 

在內核2.6及之后,解決了驚群,在內核中增加了一個互斥等待變量。一個互斥等待的行為與睡眠基本類似,主要的不同點在於:
        1)當一個等待隊列入口有 WQ_FLAG_EXCLUSEVE 標志置位, 它被添加到等待隊列的尾部. 沒有這個標志的入口項, 相反, 添加到開始.
        2)當 wake_up 被在一個等待隊列上調用時, 它在喚醒第一個有 WQ_FLAG_EXCLUSIVE 標志的進程后停止。
        對於互斥等待的行為,比如如對一個listen后的socket描述符,多線程阻塞accept時,系統內核只會喚醒所有正在等待此時間的隊列 的第一個,隊列中的其他人則繼續等待下一次事件的發生,這樣就避免的多個線程同時監聽同一個socket描述符時的驚群問題。

4.2 epoll驚群

epoll驚群分兩種:

1 是在fork之前創建epollfd,所有進程共用一個epoll;

2 是在fork之后創建epollfd,每個進程獨用一個epoll.

4.2.1 fork之前創建epollfd(新版內核已解決)

1. 主進程創建listenfd, 創建epollfd

2. 主進程fork多個子進程

3. 每個子進程把listenfd,加到epollfd中

4. 當一個連接進來時,會觸發epoll驚群,多個子進程的epoll同時會觸發

 

分析:

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

4.2.2 fork之后創建epollfd(內核未解決)

1. 主進程創建listendfd

2. 主進程創建多個子進程

3. 每個子進程創建自已的epollfd

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

5. 當一個連接進來時,會觸發epoll驚群,多個子進程epoll同時會觸發

 

分析:

因為每個子進程的epoll是不同的epoll, 雖然listenfd是同一個,但新連接過來時, accept會觸發驚群,但內核不知道該發給哪個監聽進程,因為不是同一個epoll。所以這種驚群內核並沒有處理。驚群還是會出現。

4.3 nginx驚群的解決

這里說的nginx驚群,其實就是上面的問題(fork之后創建epollfd),下面看看nginx是怎么處理驚群的。

 

在nginx中使用的epoll,是在創建進程后創建的epollfd。因些會出現上面的驚群問題。即每個子進程worker都會驚醒。

在nginx中,流程。

1

主線程創建listenfd  
2 主線程fork多個子進程(根據配置  
3 子進程創建epollfd  
4 獲到accept鎖,只有一個子進程把listenfd加到epollfd中 同一時間只有一個進程會把監聽描述符加到epoll中
5 循環監聽  

在nginx中,解決驚群的方法,使用了互斥鎖還解決。

 1 void ngx_process_events_and_timers(ngx_cycle_t *cycle)  2  
 3 {  4  
 5 // 忽略....  6  
 7 //ngx_use_accept_mutex表示是否需要通過對accept加鎖來解決驚群問題。  8  
 9 //當nginx worker進程數>1時且配置文件中打開accept_mutex時,這個標志置為1
 10  
 11     if (ngx_use_accept_mutex) {  12  
 13      //ngx_accept_disabled表示此時滿負荷,沒必要再處理新連接了,  14  
 15 //我們在nginx.conf曾經配置了每一個nginx worker進程能夠處理的最大連接數,  16  
 17 //當達到最大數的7/8時,ngx_accept_disabled為正,說明本nginx worker進程非常繁忙,  18  
 19 //將不再去處理新連接,這也是個簡單的負載均衡
 20  
 21         if (ngx_accept_disabled > 0) {  22  
 23             ngx_accept_disabled--;  24  
 25         } else {  26  
 27          //獲得accept鎖,多個worker僅有一個可以得到這把鎖。  28  
 29 //獲得鎖不是阻塞過程,都是立刻返回,獲取成功的話ngx_accept_mutex_held被置為1。  30  
 31 //拿到鎖,意味着監聽句柄被放到本進程的epoll中了,  32  
 33 //如果沒有拿到鎖,則監聽句柄會被從epoll中取出。
 34  
 35             if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  36  
 37                 return;  38  
 39             }  40  
 41  
 42  
 43 //拿到鎖的話,置flag為NGX_POST_EVENTS,這意味着ngx_process_events函數中,  44  
 45 //任何事件都將延后處理,會把accept事件都放到ngx_posted_accept_events鏈表中,  46  
 47 // epollin|epollout事件都放到ngx_posted_events鏈表中
 48  
 49             if (ngx_accept_mutex_held) {  50  
 51                 flags |= NGX_POST_EVENTS;  52  
 53             } else {  54  
 55              //拿不到鎖,也就不會處理監聽的句柄,  56  
 57 //這個timer實際是傳給epoll_wait的超時時間,  58  
 59 //修改為最大ngx_accept_mutex_delay意味着epoll_wait更短的超時返回,  60  
 61 //以免新連接長時間沒有得到處理
 62  
 63                 if (timer == NGX_TIMER_INFINITE  64  
 65                     || timer > ngx_accept_mutex_delay)  66  
 67                 {  68  
 69                     timer = ngx_accept_mutex_delay;  70  
 71                 }  72  
 73             }  74  
 75         }  76  
 77     }  78  
 79 // 忽略....  80  
 81 //linux下,調用ngx_epoll_process_events函數開始處理
 82  
 83     (void) ngx_process_events(cycle, timer, flags);  84  
 85 // 忽略....  86  
 87 //如果ngx_posted_accept_events鏈表有數據,就開始accept建立新連接
 88  
 89     if (ngx_posted_accept_events) {  90  
 91         ngx_event_process_posted(cycle, &ngx_posted_accept_events);  92  
 93     }  94  
 95  
 96  
 97 //釋放鎖后再處理下面的EPOLLIN EPOLLOUT請求
 98  
 99     if (ngx_accept_mutex_held) { 100  
101         ngx_shmtx_unlock(&ngx_accept_mutex); 102  
103     } 104  
105  
106  
107     if (delta) { 108  
109         ngx_event_expire_timers(); 110  
111     } 112  
113  
114  
115     ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, 116  
117                    "posted events %p", ngx_posted_events); 118  
119 //然后再處理正常的數據讀寫請求。因為這些請求耗時久, 120  
121 //所以在ngx_process_events里NGX_POST_EVENTS標志將事件 122  
123 //都放入ngx_posted_events鏈表中,延遲到鎖釋放了再處理。
124  
125     if (ngx_posted_events) { 126  
127         if (ngx_threaded) { 128  
129             ngx_wakeup_worker_thread(cycle); 130  
131         } else { 132  
133             ngx_event_process_posted(cycle, &ngx_posted_events); 134  
135         } 136  
137     } 138  
139

 

分析:

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

總結: nginx采用互斥鎖和主動的方法,避免了驚群,使得nginx中並無驚群

4.4 線程池驚群

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

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通知時,定向通知某個線程的條件變量,不會出現驚群

5 高並發設計

以多線程為例,進程同理

 

5.1 例1

分析

主線程創建listenfd和epollfd, 子線程共享並把listenfd加入到epoll中,舊版中會出現驚群,新版中已解決了驚群。

缺點:

應用層並不知道內核會把新連接分給哪個線程,可能平均,也可能不平均
如果某個線程已經最大負載了,還分過來,會增加此線程壓力甚至崩潰
總結:因為例1並不是最好的方法,因為沒有解決負載和分配問題

5.2 例2

分析

主線程創建listenfd, 子線程創建epollfd, 把listenfd加入到epoll中, 這種方法是無法避免驚群的問題。每次有新連接時,都會喚醒所有的accept線程,但只有一個accept成功,其他的線程accept失敗EAGAIN。

總結:例2 解決不了驚群的問題,如果線程超多,驚群越明顯,如果真正開發中,可忽略驚群,或者需要用驚群,那么使用此種設計也是可行的。

5.3 例3

分析:

主線程創建listenfd, 每個子線程創建epollfd,主線程負責accept,並發分新connfd給負載最低的一個線程,然后線程再把connfd加入到epoll中。無驚群現象。

 

總結:

主線程只用accept用,可能會主線程沒干,或連接太多處理不過來,accept瓶頸(一般情況不會產生)
主線程可以很好地根據子線程的連接來分配新連接,有比較好的負載
並發量也比較大,自測(單進程十萬並發連接QPS十萬,四核四G內存,很穩定)

5.4 例4

這是nginx的設計,無疑是目前最優的一種高並發設計,無驚群。

nginx本質:

同一時刻只允許一個nginx worker在自己的epoll中處理監聽句柄。它的負載均衡也很簡單,當達到最大connection的7/8時,本worker不會去試圖拿accept鎖,也不會去處理新連接,這樣其他nginx worker進程就更有機會去處理監聽句柄,建立新連接了。而且,由於timeout的設定,使得沒有拿到鎖的worker進程,去拿鎖的頻繁更高。

 

總結:

nginx的設計非常巧妙,很好的解決了驚群的產生,所以沒有驚群,同時也根據各進程的負載主動去決定要不要接受新連接,負載比較優。

6 總結

高並發設計,仁者見仁,智者見智,如果要求不高,隨便拿個常用的開源庫,就可能支撐。如果對業務有特殊要求,那么根據業務去選擇,如網關服(可用高並發連接的開源庫libevent/libev),消息隊列(zmq/RabbitMQ/ActiveMQ/Kafka),數據緩存(redis/memcached),分布式等。

 

研究高並發有一段時間了,總結下我自已的理解,怎么樣才算是高並發呢?單進程百萬連接,單進程百萬QPS?

 

先說說基本概念

高並發連接:指的是連接的數量,對服務端來說,一個套接字對就是一個連接,連接和本地 文件描述符無關,不受本地文件描述符限制,只跟內存有關,假設一個套接字對占用服 務器8k內存,那么1G內存=1024*1024/8 = 131072。因此連接數跟內存有關。

1G = 10萬左右連接,當然這是理論,實際要去除內核占用,其他進程占用,和本進程其他占用。

假哪一個機器32G內存,那個撐個100萬個連接是沒有問題的。

如果是單個進程100萬連,那就更牛B了,但一般都不會這么做,因為如果此進程宕了,那么,所有業務都影響了。所以一般都會分布到不同進程,不同機器,一個進程出問題了,不會影響其他進程的處理。(這也是nginx原理)

 

PV : 每天的總訪問量pave view, PV = QPS * (24*0.2) * 3600 (二八原則)

 

QPS: 每秒請求量。假如每秒請求量10萬,假如機器為16核,那么啟16個線程同時工作, 那么每個線程同時的請求量= 10萬/ 16核 =  6250QPS。

按照二八原則,一天24小時,忙時=24*0.2 = 4.8小時。

則平均一天總請求量=4.8 * 3600 *10萬QPS = 172億8千萬。

那么每秒請求10萬並發量,每天就能達到172億的PV。這算高並發嗎?

 

丟包率: 如果客端端發10萬請求,服務端只處理了8萬,那么就丟了2萬。丟包率=2/10 = 20%。丟包率是越小越好,最好是沒有。去除,網絡丟包,那么就要考慮內核里的丟包 問題,因此要考慮網卡的吞吐量,同一時間發大多請求過來,內核會不會處理不過來, 導致丟包。

 

穩定性:一個高並發服務,除了高並發外,最重要的就是穩定了,這是所有服務都必須的。 一千QPS能處理,一萬QPS也能處理,十萬QPS也能處理,當然越多越好。不要因為 業務驟增導致業務癱瘓,那失敗是不可估量的。因為,要有個度,當業務增加到一定程 度,為了保證現有業務的處理,不處理新請求業務,延時處理等。同時保證代碼的可靠。

 

因此,說到高並發,其實跟機器有並,內存,網卡,CPU核數等有關,一個強大的服務器,比如:32核,64G內存,網卡吞吐很大,那么單個進程,開32個線程,做一個百萬連接,百萬QPS的服務,是可行的。

 

本身 按例3去做了個高並發的設計,做到了四核4G內存的虛擬機里,十萬連接,十萬QPS,很穩定,沒加業務,每核CPU %sys 15左右 %usr 5%左右。如果加了業務,應該也是比較穩定的。有待測試。當然例3是有自已的缺點的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM