版權聲明:本文為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是有自已的缺點的。