[框架]高并发中的惊群效应
高并發中的驚群效應
second60 20180726
目錄
高并發中的驚群效應
1.驚群效應簡介
2. 操作系統的驚群
3. 驚群的壞處
3.1 壞處
3.2 其他
4 驚群的幾種情況
4.1 accept驚群(新版內核已解決)
4.2 epoll驚群
4.2.1 fork之前創建epollfd(新版內核已解決)
4.2.2 fork之后創建epollfd(內核未解決)
4.3 nginx驚群的解決
4.4 線程池驚群
5 高并發設計
5.1 例1
5.2 例2
5.3 例3
5.4 例4
6 總結
1.驚群效應簡介
當你往一群鴿子中間扔一塊食物,雖然最終只有一個鴿子搶到食物,但所有鴿子都會被驚動來爭奪,沒有搶到食物的鴿子只好回去繼續睡覺, 等待下一塊食物到來。這樣,每扔一塊食物,都會驚動所有的鴿子,即為驚群。
?
簡單地說:就是扔一塊食物,所有鴿子來搶,但最終只一個鴿子搶到了食物。
語義分析:食物只有一塊,最終只有一個鴿子搶到,但是驚動了所有鴿子,每個鴿子都跑過來,消耗了每個鴿子的能量。(這個很符合達爾文的進化論,物種之間的競爭,適者生存。)
?
2. 操作系統的驚群
在多進程/多線程等待同一資源時,也會出現驚群。即當某一資源可用時,多個進程/線程會驚醒,競爭資源。這就是操作系統中的驚群。
?
3. 驚群的壞處
3.1 壞處
3.2 其他
1. 在某些情況:驚群次數少/進(線)程負載不高,驚群可以忽略不計
?
4 驚群的幾種情況
在高并發(多線程/多進程/多連接)中,會產生驚群的情況有:
4.1 accept驚群(新版內核已解決)
以多進程為例,在主進程創建監聽描述符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描述符時的驚群問題。
?
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中,解決驚群的方法,使用了互斥鎖還解決。
void ngx_process_events_and_timers(ngx_cycle_t *cycle){// 忽略....//ngx_use_accept_mutex表示是否需要通過對accept加鎖來解決驚群問題。//當nginx worker進程數>1時且配置文件中打開accept_mutex時,這個標志置為1if (ngx_use_accept_mutex) {//ngx_accept_disabled表示此時滿負荷,沒必要再處理新連接了,//我們在nginx.conf曾經配置了每一個nginx worker進程能夠處理的最大連接數,//當達到最大數的7/8時,ngx_accept_disabled為正,說明本nginx worker進程非常繁忙,//將不再去處理新連接,這也是個簡單的負載均衡if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//獲得accept鎖,多個worker僅有一個可以得到這把鎖。//獲得鎖不是阻塞過程,都是立刻返回,獲取成功的話ngx_accept_mutex_held被置為1。//拿到鎖,意味著監聽句柄被放到本進程的epoll中了,//如果沒有拿到鎖,則監聽句柄會被從epoll中取出。if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}//拿到鎖的話,置flag為NGX_POST_EVENTS,這意味著ngx_process_events函數中,//任何事件都將延后處理,會把accept事件都放到ngx_posted_accept_events鏈表中,// epollin|epollout事件都放到ngx_posted_events鏈表中if (ngx_accept_mutex_held) {flags |= NGX_POST_EVENTS;} else {//拿不到鎖,也就不會處理監聽的句柄,//這個timer實際是傳給epoll_wait的超時時間,//修改為最大ngx_accept_mutex_delay意味著epoll_wait更短的超時返回,//以免新連接長時間沒有得到處理if (timer == NGX_TIMER_INFINITE|| timer > ngx_accept_mutex_delay){timer = ngx_accept_mutex_delay;}}}}// 忽略....//linux下,調用ngx_epoll_process_events函數開始處理(void) ngx_process_events(cycle, timer, flags);// 忽略....//如果ngx_posted_accept_events鏈表有數據,就開始accept建立新連接if (ngx_posted_accept_events) {ngx_event_process_posted(cycle, &ngx_posted_accept_events);}//釋放鎖后再處理下面的EPOLLIN EPOLLOUT請求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);//然后再處理正常的數據讀寫請求。因為這些請求耗時久,//所以在ngx_process_events里NGX_POST_EVENTS標志將事件//都放入ngx_posted_events鏈表中,延遲到鎖釋放了再處理。if (ngx_posted_events) {if (ngx_threaded) {ngx_wakeup_worker_thread(cycle);} else {ngx_event_process_posted(cycle, &ngx_posted_events);}}}?
| 步驟 | nginx主動解決驚群流程 | |
| 1 | 子進程loop | |
| 2 | 判斷是否使用accept加鎖 | |
| 3 | 判斷是否滿負荷最大連接數的7/8(是不處理) | |
| 4 | 多個worker競爭accept_mutex鎖(主動精髓) | |
| 5 | 獲得鎖成功 | 獲得鎖失敗 |
| 6 | (監聽句柄加到本進程的epoll) | 監聽句柄會被從epoll中取出 |
| 7 | 事件加入到鏈表中 (accept事件放到ngx_posted_accept_events鏈表 epollin|out事件放到ngx_posted_events鏈表) | 修改epoll_wait的超時時間 (為了下次更早搶鎖) |
| 8 | 如果有accept_event就處理新連接 | ? |
| 9 | 釋放鎖accept_mutex | ? |
| 10 | 處理正常的數據讀寫請求 | ? |
| 11 | 子進程繼續loop | |
| 12 | ? | |
(注:上面的總結如果有不對的地方,麻煩大牛提出來,謝謝)
?
分析:
?
總結: nginx采用互斥鎖和主動的方法,避免了驚群,使得nginx中并無驚群
4.4 線程池驚群
在多線程設計中,經常會用到互斥和條件變量的問題。當一個線程解鎖并通知其他線程的時候,就會出現驚群的現象。
這里的驚群現象出現在3里,pthread_cond_signal,語義上看,是通知一個線程。調用此函數后,系統會喚醒在相同條件變量上等待的一個或多個線程(可參看手冊)。如果通知了多個線程,則發生了驚群。
?
正常的用法:
解決驚群的方法:
?
5 高并發設計
以多線程為例,進程同理
| 例 | 主線程 | 子線程epoll | 是否有驚群 | 參考 |
| 1 | listenfd/epollfd | 共用listenfd/epollfd 子線程accept | epoll驚群 ? | 被動 |
| 2 | listenfd | 共用listenfd, 每個線程創建epollfd listenfd加入epoll | epoll驚群 | 被動 |
| 3 | listenfd 主線程accept并分發connfd | 每個線程創建epollfd 接收主線程分發的connfd | 無驚群 accept瓶頸 | 被動 |
| 4 | listenfd | 共用listenfd, 每個線程創建epollfd 互斥鎖決定加入/移出epoll | 無驚群 ? | nginx |
| ? | ? | ? | ? | ? |
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中。無驚群現象。
?
總結:
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是有自已的缺點的。
?
同進,也希望研究高并發的同學,一起來討論高并發服務設計思想。(加微:luoying140131)
總結
以上是生活随笔為你收集整理的[框架]高并发中的惊群效应的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MD5对文件进行加密,可以支持大文件
- 下一篇: 给定n个整数,请统计出每个整数出现的次数