Linux網絡編程“驚群”問題總結

http://www.cnblogs.com/Anker/p/7071849.html

1、前言

  我從事Linux系統下網絡開發將近4年了,經常還是遇到一些問題,只是知其然而不知其所以然,有時候和其他人交流,搞得非常尷尬。如今計算機都是多核了,網絡編程框架也逐步豐富多了,我所知道的有多進程、多線程、異步事件驅動常用的三種模型。最經典的模型就是Nginx中所用的Master-Worker多進程異步驅動模型。今天和大家一起討論一下網絡開發中遇到的“驚群”現象。之前只是聽說過這個現象,網上查資料也了解了基本概念,在實際的工作中還真沒有遇到過。今天周末,結合自己的理解和網上的資料,徹底將“驚群”弄明白。需要弄清楚如下幾個問題:

(1)什么是“驚群”,會產生什么問題?

(2)“驚群”的現象怎么用代碼模擬出來?

(3)如何處理“驚群”問題,處理“驚群”后的現象又是怎么樣呢?

2、何為驚群

  如今網絡編程中經常用到多進程或多線程模型,大概的思路是父進程創建socket,bind、listen后,通過fork創建多個子進程,每個子進程繼承了父進程的socket,調用accpet開始監聽等待網絡連接。這個時候有多個進程同時等待網絡的連接事件,當這個事件發生時,這些進程被同時喚醒,就是“驚群”。這樣會導致什么問題呢?我們知道進程被喚醒,需要進行內核重新調度,這樣每個進程同時去響應這一個事件,而最終只有一個進程能處理事件成功,其他的進程在處理該事件失敗后重新休眠或其他。網絡模型如下圖所示:

簡而言之,驚群現象(thundering herd)就是當多個進程和線程在同時阻塞等待同一個事件時,如果這個事件發生,會喚醒所有的進程,但最終只可能有一個進程/線程對該事件進行處理,其他進程/線程會在失敗后重新休眠,這種性能浪費就是驚群。

3、編碼模擬“驚群”現象

  我們已經知道了“驚群”是怎么回事,那么就按照上面的圖編碼實現看一下效果。我嘗試使用多進程模型,創建一個父進程綁定一個端口監聽socket,然后fork出多個子進程,子進程們開始循環處理(比如accept)這個socket。測試代碼如下所示:

復制代碼
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/types.h>  
 4 #include <sys/socket.h>  
 5 #include <netinet/in.h>  
 6 #include <arpa/inet.h>  
 7 #include <assert.h>  
 8 #include <sys/wait.h>
 9 #include <string.h>
10 #include <errno.h>
11 
12 #define IP   "127.0.0.1"
13 #define PORT  8888
14 #define WORKER 4
15 
16 int worker(int listenfd, int i)
17 {
18     while (1) {
19         printf("I am worker %d, begin to accept connection.\n", i);
20         struct sockaddr_in client_addr;  
21         socklen_t client_addrlen = sizeof( client_addr );  
22         int connfd = accept( listenfd, ( struct sockaddr* )&client_addr, &client_addrlen );  
23         if (connfd != -1) {
24             printf("worker %d accept a connection success.\t", i);
25             printf("ip :%s\t",inet_ntoa(client_addr.sin_addr));
26             printf("port: %d \n",client_addr.sin_port);
27         } else {
28             printf("worker %d accept a connection failed,error:%s", i, strerror(errno));
         close(connfd);
29 } 30 } 31 return 0; 32 } 33 34 int main() 35 { 36 int i = 0; 37 struct sockaddr_in address; 38 bzero(&address, sizeof(address)); 39 address.sin_family = AF_INET; 40 inet_pton( AF_INET, IP, &address.sin_addr); 41 address.sin_port = htons(PORT); 42 int listenfd = socket(PF_INET, SOCK_STREAM, 0); 43 assert(listenfd >= 0); 44 45 int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address)); 46 assert(ret != -1); 47 48 ret = listen(listenfd, 5); 49 assert(ret != -1); 50 51 for (i = 0; i < WORKER; i++) { 52 printf("Create worker %d\n", i+1); 53 pid_t pid = fork(); 54 /*child process */ 55 if (pid == 0) { 56 worker(listenfd, i); 57 } 58 59 if (pid < 0) { 60 printf("fork error"); 61 } 62 } 63 64 /*wait child process*/ 65 int status; 66 wait(&status); 67 return 0; 68 }
復制代碼

編譯執行,在本機上使用telnet 127.0.0.1 8888測試,結果如下所示:

按照“驚群"現象,期望結果應該是4個子進程都會accpet到請求,其中只有一個成功,另外三個失敗的情況。而實際的結果顯示,父進程開始創建4個子進程,每個子進程開始等待accept連接。當telnet連接來的時候,只有worker2 子進程accpet到請求,而其他的三個進程并沒有接收到請求。

這是什么原因呢?難道驚群現象是假的嗎?于是趕緊google查一下,驚群到底是怎么出現的。

其實在Linux2.6版本以后,內核內核已經解決了accept()函數的“驚群”問題,大概的處理方式就是,當內核接收到一個客戶連接后,只會喚醒等待隊列上的第一個進程或線程。所以,如果服務器采用accept阻塞調用方式,在最新的Linux系統上,已經沒有“驚群”的問題了。

但是,對于實際工程中常見的服務器程序,大都使用select、poll或epoll機制,此時,服務器不是阻塞在accept,而是阻塞在select、poll或epoll_wait,這種情況下的“驚群”仍然需要考慮。接下來以epoll為例分析:

使用epoll非阻塞實現代碼如下所示:

復制代碼
  1 #include <sys/types.h>
  2 #include <sys/socket.h>
  3 #include <sys/epoll.h>
  4 #include <netdb.h>
  5 #include <string.h>
  6 #include <stdio.h>
  7 #include <unistd.h>
  8 #include <fcntl.h>
  9 #include <stdlib.h>
 10 #include <errno.h>
 11 #include <sys/wait.h>
 12 #include <unistd.h>
 13 
 14 #define IP   "127.0.0.1"
 15 #define PORT  8888
 16 #define PROCESS_NUM 4
 17 #define MAXEVENTS 64
 18 
 19 static int create_and_bind ()
 20 {
 21     int fd = socket(PF_INET, SOCK_STREAM, 0);
 22     struct sockaddr_in serveraddr;
 23     serveraddr.sin_family = AF_INET;
 24     inet_pton( AF_INET, IP, &serveraddr.sin_addr);  
 25     serveraddr.sin_port = htons(PORT);
 26     bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
 27     return fd;
 28 }
 29 
 30 static int make_socket_non_blocking (int sfd)
 31 {
 32     int flags, s;
 33     flags = fcntl (sfd, F_GETFL, 0);
 34     if (flags == -1) {
 35         perror ("fcntl");
 36         return -1;
 37     }
 38     flags |= O_NONBLOCK;
 39     s = fcntl (sfd, F_SETFL, flags);
 40     if (s == -1) {
 41         perror ("fcntl");
 42         return -1;
 43     }
 44     return 0;
 45 }
 46 
 47 void worker(int sfd, int efd, struct epoll_event *events, int k) {
 48     /* The event loop */
 49     while (1) {
 50         int n, i;
 51         n = epoll_wait(efd, events, MAXEVENTS, -1);
 52         printf("worker  %d return from epoll_wait!\n", k);
 53         for (i = 0; i < n; i++) {
 54             if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events &EPOLLIN))) {
 55                 /* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */
 56                 fprintf (stderr, "epoll error\n");
 57                 close (events[i].data.fd);
 58                 continue;
 59             } else if (sfd == events[i].data.fd) {
 60                 /* We have a notification on the listening socket, which means one or more incoming connections. */
 61                 struct sockaddr in_addr;
 62                 socklen_t in_len;
 63                 int infd;
 64                 char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
 65                 in_len = sizeof in_addr;
 66                 infd = accept(sfd, &in_addr, &in_len);
 67                 if (infd == -1) {
 68                     printf("worker %d accept failed!\n", k);
 69                     break;
 70                 }
 71                 printf("worker %d accept successed!\n", k);
 72                 /* Make the incoming socket non-blocking and add it to the list of fds to monitor. */
 73                 close(infd); 
 74             }
 75         }
 76     }
 77 }
 78 
 79 int main (int argc, char *argv[])
 80 {
 81     int sfd, s;
 82     int efd;
 83     struct epoll_event event;
 84     struct epoll_event *events;
 85     sfd = create_and_bind();
 86     if (sfd == -1) {
 87         abort ();
 88     }
 89     s = make_socket_non_blocking (sfd);
 90     if (s == -1) {
 91         abort ();
 92     }
 93     s = listen(sfd, SOMAXCONN);
 94     if (s == -1) {
 95         perror ("listen");
 96         abort ();
 97     }
 98     efd = epoll_create(MAXEVENTS);
 99     if (efd == -1) {
100         perror("epoll_create");
101         abort();
102     }
103     event.data.fd = sfd;
104     event.events = EPOLLIN;
105     s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
106     if (s == -1) {
107         perror("epoll_ctl");
108         abort();
109     }
110 
111     /* Buffer where events are returned */
112     events = calloc(MAXEVENTS, sizeof event);
113     int k;
114     for(k = 0; k < PROCESS_NUM; k++) {
115         printf("Create worker %d\n", k+1);
116         int pid = fork();
117         if(pid == 0) {
118             worker(sfd, efd, events, k);
119         }
120     }
121     int status;
122     wait(&status);
123     free (events);
124     close (sfd);
125     return EXIT_SUCCESS;
126 }
復制代碼

父進程中創建套接字,并設置為非阻塞,開始listen。然后fork出4個子進程,在worker中調用epoll_wait開始accpet連接。使用telnet測試結果如下:

從結果看出,與上面是一樣的,只有一個進程接收到連接,其他三個沒有收到,說明沒有發生驚群現象。這又是為什么呢?

在早期的Linux版本中,內核對于阻塞在epoll_wait的進程,也是采用全部喚醒的機制,所以存在和accept相似的“驚群”問題。新版本的的解決方案也是只會喚醒等待隊列上的第一個進程或線程,所以,新版本Linux?部分的解決了epoll的“驚群”問題。所謂部分的解決,意思就是:對于部分特殊場景,使用epoll機制,已經不存在“驚群”的問題了,但是對于大多數場景,epoll機制仍然存在“驚群”。

epoll存在驚群的場景如下:在worker保持工作的狀態下,都會被喚醒,例如在epoll_wait后調用sleep一次。改寫woker函數如下:

復制代碼
void worker(int sfd, int efd, struct epoll_event *events, int k) {/* The event loop */while (1) {int n, i;n = epoll_wait(efd, events, MAXEVENTS, -1);/*keep running*/sleep(2);printf("worker  %d return from epoll_wait!\n", k); for (i = 0; i < n; i++) {if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events &EPOLLIN))) {/* An error has occured on this fd, or the socket is not ready for reading (why were we notified then?) */fprintf (stderr, "epoll error\n");close (events[i].data.fd);continue;} else if (sfd == events[i].data.fd) {/* We have a notification on the listening socket, which means one or more incoming connections. */struct sockaddr in_addr;socklen_t in_len;int infd;char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];in_len = sizeof in_addr;infd = accept(sfd, &in_addr, &in_len);if (infd == -1) {printf("worker %d accept failed,error:%s\n", k, strerror(errno));break;}   printf("worker %d accept successed!\n", k); /* Make the incoming socket non-blocking and add it to the list of fds to monitor. */close(infd); }   }   }   
}
復制代碼

測試結果如下所示:

終于看到驚群現象的出現了。

4、解決驚群問題

  Nginx中使用mutex互斥鎖解決這個問題,具體措施有使用全局互斥鎖,每個子進程在epoll_wait()之前先去申請鎖,申請到則繼續處理,獲取不到則等待,并設置了一個負載均衡的算法(當某一個子進程的任務量達到總設置量的7/8時,則不會再嘗試去申請鎖)來均衡各個進程的任務量。后面深入學習一下Nginx的驚群處理過程。

5、參考網址

http://blog.csdn.net/russell_tao/article/details/7204260

http://pureage.info/2015/12/22/thundering-herd.html

http://blog.chinaunix.net/uid-20671208-id-4935141.html

冷靜思考,勇敢面對,把握未來!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/383808.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/383808.shtml
英文地址,請注明出處:http://en.pswp.cn/news/383808.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Java學習筆記六】常用數據對象之String

字符串 在Java中系統定義了兩種類型的字符串類&#xff1a;String和StringBuffer String類對象的值和長度都不能改變&#xff0c;稱為常量字符串類&#xff0c;其中每個值稱為常量字符串。 StringBuffer類對象的值和長度都可以改變&#xff0c;稱為變量字符串類&#xff0c;其…

【Java學習筆記七】常用數據對象之數組

同一般的對象創建和定義一樣&#xff0c;數組的定義和創建可以分開進行也可以合并一起進行。 一維數組定義格式&#xff1a; <元素類型>[] <數組名>;//[]也可以放在數組名的后面一維數組創建格式&#xff1a; new <元素類型>[<元素個數>];執行new運…

yfan.qiu linux硬鏈接與軟鏈接

http://www.cnblogs.com/yfanqiu/archive/2012/06/11/2545556.html Linux 系統中有軟鏈接和硬鏈接兩種特殊的“文件”。 軟鏈接可以看作是Windows中的快捷方式&#xff0c;可以讓你快速鏈接到目標檔案或目錄。 硬鏈接則透過文件系統的inode來產生新檔名&#xff0c;而不是產生…

【Java學習筆記八】包裝類和vector

包裝類 在Java語言中&#xff0c;每一種基本的數據類型都有相應的對象類型&#xff0c;稱為他們基本類型的包裝類&#xff08;包裹類&#xff09;。 字節byte&#xff1a;Byte、短整數型short&#xff1a;Short 標準整數型int&#xff1a;Integer、長整數型long&#xff1a;Lo…

Linux C++線程池實例

http://www.cnblogs.com/danxi/p/6636095.html 想做一個多線程服務器測試程序&#xff0c;因此參考了github的一些實例&#xff0c;然后自己動手寫了類似的代碼來加深理解。 目前了解的線程池實現有2種思路&#xff1a; 第一種&#xff1a; 主進程創建一定數量的線程&#xff0…

Java編寫簡單的自定義異常類

除了系統中自己帶的異常&#xff0c;我們也可以自己寫一些簡單的異常類來幫助我們處理問題。 所有的異常命名都是以Exception結尾&#xff0c;并且都是Exception的子類。 假設我們要編寫一個人類的類&#xff0c;為了判斷年齡的輸入是否合法&#xff0c;我們編寫了一個名為Il…

shared_ptr簡介以及常見問題

http://blog.csdn.net/stelalala/article/details/19993425 本文中的shared_ptr以vs2010中的std::tr1::shared_ptr作為研究對象。可能和boost中的有些許差異&#xff0c;特此說明。 基本功能 shared_ptr提供了一個管理內存的簡單有效的方法。shared_ptr能在以下方面給開發提供便…

【Java學習筆記九】多線程

程序&#xff1a;計算機指令的集合&#xff0c;它以文件的形式存儲在磁盤上&#xff0c;是應用程序執行的藍本。 進程&#xff1a;是一個程序在其自身的地址空間中的一次執行活動。進程是資源申請、調度和獨立運行的單位&#xff0c;因此&#xff0c;它使用系統中的運行資源。而…

【C++11新特性】 C++11智能指針之weak_ptr

http://blog.csdn.net/xiejingfa/article/details/50772571 原創作品&#xff0c;轉載請標明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50772571 如題&#xff0c;我們今天要講的是C11引入的三種智能指針中的最后一個&#xff1a;weak_ptr。在學習weak_ptr之…

【C++學習筆記四】運算符重載

當調用一個重載函數和重載運算符時&#xff0c;編譯器通過把您所使用的參數類型和定義中的參數類型相比較&#xff0c;巨鼎選用最合適的定義。&#xff08;重載決策&#xff09; 重載運算符時帶有特殊名稱的函數&#xff0c;函數名是由關鍵字operator和其后要重載的運算符符號…

【C++11新特性】 C++11智能指針之unique_ptr

原創作品&#xff0c;轉載請標明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50759210 在前面一篇文章中&#xff0c;我們了解了C11中引入的智能指針之一shared_ptr&#xff0c;今天&#xff0c;我們來介紹一下另一種智能指針unique_ptr。 unique_ptr介紹 uni…

C++派生類對象和基類對象賦值

在C中&#xff0c;我們允許 將派生類對象賦給基類對象。&#xff08;不允許將基類對象賦給派生類對象&#xff09; 只會將基類對象成員賦值用基類指針指向派生類對象。&#xff08;不允許用派生類指針指向基類對象&#xff09; 基類指針只能操作基類中的成員基類引用作為派生類…

【C++11新特性】 C++11智能指針之shared_ptr

http://blog.csdn.net/Xiejingfa/article/details/50750037 原創作品&#xff0c;轉載請標明&#xff1a;http://blog.csdn.net/Xiejingfa/article/details/50750037 C中的智能指針首先出現在“準”標準庫boost中。隨著使用的人越來越多&#xff0c;為了讓開發人員更方便、更安…

C++(純)虛函數重寫時訪問權限更改問題

我們知道在Java中是自動實現多態的&#xff0c;Java中規定重寫的方法的訪問權限不能縮小。那么在C中我們實現多態的時候是否可以更改&#xff08;縮小&#xff09;訪問權限呢&#xff1f; 經過測試&#xff0c;得到的答案如下&#xff1a;如果用基類指針指向派生類對象實現多態…

C++ — 智能指針的簡單實現以及循環引用問題

http://blog.csdn.net/dawn_sf/article/details/70168930 智能指針 ____________________________________________________ 今天我們來看一個高大上的東西&#xff0c;它叫智能指針。 哇這個名字聽起來都智能的不得了&#xff0c;其實等你了解它你一定會有一點失望的。。。。因…

C++(靜態)(常量)數據進行初始化問題以及靜態變量析構

在C11標準以前我們都不可以在類中對數據成員初始化&#xff0c;僅能在構造函數中進行初始化&#xff1a; class A {int a,b; double c; string d;A():a(1),b(2),c(3),d(""){} };在C11標準以后我們可以在類中對非靜態成員進行初始化。實際上的機制是在調用構造函數的…

C++this指針的用法

參考博客&#xff1a;https://www.cnblogs.com/zhengfa-af/p/8082959.html 在 訪問對象的非靜態成員時會隱式傳遞一個參數&#xff0c;即對象本身的指針&#xff0c;這個指針名為this。 例如&#xff1a; class A {int a1;public:A(){}void GetA(int a){cout<<this-&g…

C++開發者都應該使用的10個C++11特性

http://blog.jobbole.com/44015/ 感謝馮上&#xff08;治不好你我就不是獸醫 &#xff09;的熱心翻譯。如果其他朋友也有不錯的原創或譯文&#xff0c;可以嘗試推薦給伯樂在線。】 在C11新標準中&#xff0c;語言本身和標準庫都增加了很多新內容&#xff0c;本文只涉及了一些皮…

C++不能被聲明為虛函數

虛函數是為了實現多態&#xff0c;但是顯然并不是所有函數都可以聲明為虛函數的。 不能被聲明為虛函數的函數有兩類&#xff1a; 不能被繼承的函數不能被重寫的函數 因此&#xff0c;這些函數都不能被聲明為虛函數 普通函數構造函數 如果構造函數定義為虛函數&#xff0c;則…

類的聲明與定義

類的前向聲明&#xff1a; class A;在聲明之后&#xff0c;定義之前&#xff0c;類A是一個不完全類型&#xff0c;即知道A是一個類&#xff0c;但是不知道包含哪些成員。不完全類型只能以有限方式使用&#xff0c;不能定義該類型的對象&#xff0c;不完全類型只能用于定義指向…