Linux網(wǎng)絡(luò)編程驚群問題總結(jié)

1孽鸡、前言

我從事Linux系統(tǒng)下網(wǎng)絡(luò)開發(fā)將近4年了,經(jīng)常還是遇到一些問題栏豺,只是知其然而不知其所以然彬碱,有時(shí)候和其他人交流,搞得非常尷尬奥洼。如今計(jì)算機(jī)都是多核了巷疼,網(wǎng)絡(luò)編程框架也逐步豐富多了,我所知道的有多進(jìn)程灵奖、多線程嚼沿、異步事件驅(qū)動(dòng)常用的三種模型。最經(jīng)典的模型就是Nginx中所用的Master-Worker多進(jìn)程異步驅(qū)動(dòng)模型桑寨。今天和大家一起討論一下網(wǎng)絡(luò)開發(fā)中遇到的“驚群”現(xiàn)象伏尼。之前只是聽說過這個(gè)現(xiàn)象忿檩,網(wǎng)上查資料也了解了基本概念尉尾,在實(shí)際的工作中還真沒有遇到過。今天周末燥透,結(jié)合自己的理解和網(wǎng)上的資料沙咏,徹底將“驚群”弄明白。需要弄清楚如下幾個(gè)問題:

(1)什么是“驚群”班套,會(huì)產(chǎn)生什么問題肢藐?

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

(3)如何處理“驚群”問題吱韭,處理“驚群”后的現(xiàn)象又是怎么樣呢吆豹?

2鱼的、何為驚群

如今網(wǎng)絡(luò)編程中經(jīng)常用到多進(jìn)程或多線程模型,大概的思路是父進(jìn)程創(chuàng)建socket痘煤,bind凑阶、listen后,通過fork創(chuàng)建多個(gè)子進(jìn)程衷快,每個(gè)子進(jìn)程繼承了父進(jìn)程的socket宙橱,調(diào)用accpet開始監(jiān)聽等待網(wǎng)絡(luò)連接。這個(gè)時(shí)候有多個(gè)進(jìn)程同時(shí)等待網(wǎng)絡(luò)的連接事件蘸拔,當(dāng)這個(gè)事件發(fā)生時(shí)师郑,這些進(jìn)程被同時(shí)喚醒,就是“驚群”调窍。這樣會(huì)導(dǎo)致什么問題呢宝冕?我們知道進(jìn)程被喚醒,需要進(jìn)行內(nèi)核重新調(diào)度陨晶,這樣每個(gè)進(jìn)程同時(shí)去響應(yīng)這一個(gè)事件猬仁,而最終只有一個(gè)進(jìn)程能處理事件成功,其他的進(jìn)程在處理該事件失敗后重新休眠或其他先誉。網(wǎng)絡(luò)模型如下圖所示:

image

簡而言之湿刽,驚群現(xiàn)象(thundering herd)就是當(dāng)多個(gè)進(jìn)程和線程在同時(shí)阻塞等待同一個(gè)事件時(shí),如果這個(gè)事件發(fā)生褐耳,會(huì)喚醒所有的進(jìn)程诈闺,但最終只可能有一個(gè)進(jìn)程/線程對該事件進(jìn)行處理,其他進(jìn)程/線程會(huì)在失敗后重新休眠铃芦,這種性能浪費(fèi)就是驚群雅镊。

3、編碼模擬“驚群”現(xiàn)象

我們已經(jīng)知道了“驚群”是怎么回事刃滓,那么就按照上面的圖編碼實(shí)現(xiàn)看一下效果仁烹。我嘗試使用多進(jìn)程模型,創(chuàng)建一個(gè)父進(jìn)程綁定一個(gè)端口監(jiān)聽socket咧虎,然后fork出多個(gè)子進(jìn)程卓缰,子進(jìn)程們開始循環(huán)處理(比如accept)這個(gè)socket。測試代碼如下所示:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>

#define IP   "127.0.0.1"
#define PORT  8888
#define WORKER 4

int worker(int listenfd, int i)
{
    while (1) {
        printf("I am worker %d, begin to accept connection.\n", i);
        struct sockaddr_in client_addr;
        socklen_t client_addrlen = sizeof( client_addr );
        int connfd = accept( listenfd, ( struct sockaddr* )&client_addr, &client_addrlen );
        if (connfd != -1) {
            printf("worker %d accept a connection success.\t", i);
            printf("ip :%s\t",inet_ntoa(client_addr.sin_addr));
            printf("port: %d \n",client_addr.sin_port);
        } else {
            printf("worker %d accept a connection failed,error:%s", i, strerror(errno));         close(connfd);
        }
    }
    return 0;
}

int main()
{
    int i = 0;
    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton( AF_INET, IP, &address.sin_addr);
    address.sin_port = htons(PORT);
    int listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenfd >= 0);

    int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(listenfd, 5);
    assert(ret != -1);

    for (i = 0; i < WORKER; i++) {
        printf("Create worker %d\n", i+1);
        pid_t pid = fork();
        /*child  process */
        if (pid == 0) {
            worker(listenfd, i);
        }

        if (pid < 0) {
            printf("fork error");
        }
    }

    /*wait child process*/
    int status;
    wait(&status);
    return 0;
}

編譯執(zhí)行砰诵,在本機(jī)上使用telnet 127.0.0.1 8888測試征唬,結(jié)果如下所示:

image

按照“驚群"現(xiàn)象,期望結(jié)果應(yīng)該是4個(gè)子進(jìn)程都會(huì)accpet到請求茁彭,其中只有一個(gè)成功总寒,另外三個(gè)失敗的情況。而實(shí)際的結(jié)果顯示理肺,父進(jìn)程開始創(chuàng)建4個(gè)子進(jìn)程摄闸,每個(gè)子進(jìn)程開始等待accept連接善镰。當(dāng)telnet連接來的時(shí)候,只有worker2 子進(jìn)程accpet到請求年枕,而其他的三個(gè)進(jìn)程并沒有接收到請求媳禁。

這是什么原因呢?難道驚群現(xiàn)象是假的嗎画切?于是趕緊google查一下竣稽,驚群到底是怎么出現(xiàn)的。

其實(shí)在Linux2.6版本以后霍弹,內(nèi)核內(nèi)核已經(jīng)解決了accept()函數(shù)的“驚群”問題毫别,大概的處理方式就是,當(dāng)內(nèi)核接收到一個(gè)客戶連接后典格,只會(huì)喚醒等待隊(duì)列上的第一個(gè)進(jìn)程或線程岛宦。所以,如果服務(wù)器采用accept阻塞調(diào)用方式耍缴,在最新的Linux系統(tǒng)上砾肺,已經(jīng)沒有“驚群”的問題了。

但是防嗡,對于實(shí)際工程中常見的服務(wù)器程序变汪,大都使用select、poll或epoll機(jī)制蚁趁,此時(shí)裙盾,服務(wù)器不是阻塞在accept,而是阻塞在select他嫡、poll或epoll_wait番官,這種情況下的“驚群”仍然需要考慮。接下來以epoll為例分析:

使用epoll非阻塞實(shí)現(xiàn)代碼如下所示:

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netdb.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>

#define IP   "127.0.0.1"
#define PORT  8888
#define PROCESS_NUM 4
#define MAXEVENTS 64

static int create_and_bind ()
{
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton( AF_INET, IP, &serveraddr.sin_addr);
    serveraddr.sin_port = htons(PORT);
    bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    return fd;
}

static int make_socket_non_blocking (int sfd)
{
    int flags, s;
    flags = fcntl (sfd, F_GETFL, 0);
    if (flags == -1) {
        perror ("fcntl");
        return -1;
    }
    flags |= O_NONBLOCK;
    s = fcntl (sfd, F_SETFL, flags);
    if (s == -1) {
        perror ("fcntl");
        return -1;
    }
    return 0;
}

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);
        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!\n", k);
                    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);
            }
        }
    }
}

int main (int argc, char *argv[])
{
    int sfd, s;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;
    sfd = create_and_bind();
    if (sfd == -1) {
        abort ();
    }
    s = make_socket_non_blocking (sfd);
    if (s == -1) {
        abort ();
    }
    s = listen(sfd, SOMAXCONN);
    if (s == -1) {
        perror ("listen");
        abort ();
    }
    efd = epoll_create(MAXEVENTS);
    if (efd == -1) {
        perror("epoll_create");
        abort();
    }
    event.data.fd = sfd;
    event.events = EPOLLIN;
    s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
    if (s == -1) {
        perror("epoll_ctl");
        abort();
    }

    /* Buffer where events are returned */
    events = calloc(MAXEVENTS, sizeof event);
    int k;
    for(k = 0; k < PROCESS_NUM; k++) {
        printf("Create worker %d\n", k+1);
        int pid = fork();
        if(pid == 0) {
            worker(sfd, efd, events, k);
        }
    }
    int status;
    wait(&status);
    free (events);
    close (sfd);
    return EXIT_SUCCESS;
}

父進(jìn)程中創(chuàng)建套接字钢属,并設(shè)置為非阻塞徘熔,開始listen。然后fork出4個(gè)子進(jìn)程淆党,在worker中調(diào)用epoll_wait開始accpet連接酷师。使用telnet測試結(jié)果如下:

image

從結(jié)果看出,與上面是一樣的宁否,只有一個(gè)進(jìn)程接收到連接窒升,其他三個(gè)沒有收到缀遍,說明沒有發(fā)生驚群現(xiàn)象慕匠。這又是為什么呢?

在早期的Linux版本中域醇,內(nèi)核對于阻塞在epoll_wait的進(jìn)程台谊,也是采用全部喚醒的機(jī)制蓉媳,所以存在和accept相似的“驚群”問題。新版本的的解決方案也是只會(huì)喚醒等待隊(duì)列上的第一個(gè)進(jìn)程或線程锅铅,所以酪呻,新版本Linux 部分的解決了epoll的“驚群”問題。所謂部分的解決盐须,意思就是:對于部分特殊場景玩荠,使用epoll機(jī)制,已經(jīng)不存在“驚群”的問題了贼邓,但是對于大多數(shù)場景阶冈,epoll機(jī)制仍然存在“驚群”。

epoll存在驚群的場景如下:在worker保持工作的狀態(tài)下塑径,都會(huì)被喚醒女坑,例如在epoll_wait后調(diào)用sleep一次。改寫woker函數(shù)如下:

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); 
            }   
        }   
    }   
}

測試結(jié)果如下所示:

image

終于看到驚群現(xiàn)象的出現(xiàn)了统舀。

4匆骗、解決驚群問題

Nginx中使用mutex互斥鎖解決這個(gè)問題,具體措施有使用全局互斥鎖誉简,每個(gè)子進(jìn)程在epoll_wait()之前先去申請鎖碉就,申請到則繼續(xù)處理,獲取不到則等待闷串,并設(shè)置了一個(gè)負(fù)載均衡的算法(當(dāng)某一個(gè)子進(jìn)程的任務(wù)量達(dá)到總設(shè)置量的7/8時(shí)铝噩,則不會(huì)再嘗試去申請鎖)來均衡各個(gè)進(jìn)程的任務(wù)量。后面深入學(xué)習(xí)一下Nginx的驚群處理過程窿克。
5骏庸、參考網(wǎng)址

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市年叮,隨后出現(xiàn)的幾起案子具被,更是在濱河造成了極大的恐慌,老刑警劉巖只损,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件一姿,死亡現(xiàn)場離奇詭異,居然都是意外死亡跃惫,警方通過查閱死者的電腦和手機(jī)叮叹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爆存,“玉大人蛉顽,你說我怎么就攤上這事∠冉希” “怎么了携冤?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵悼粮,是天一觀的道長。 經(jīng)常有香客問我曾棕,道長扣猫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任翘地,我火速辦了婚禮申尤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衙耕。我一直安慰自己瀑凝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布臭杰。 她就那樣靜靜地躺著粤咪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渴杆。 梳的紋絲不亂的頭發(fā)上寥枝,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音磁奖,去河邊找鬼囊拜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛比搭,可吹牛的內(nèi)容都是我干的冠跷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼身诺,長吁一口氣:“原來是場噩夢啊……” “哼蜜托!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霉赡,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤橄务,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后穴亏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜂挪,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年嗓化,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了棠涮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡刺覆,死狀恐怖严肪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤诬垂,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站伦仍,受9級(jí)特大地震影響结窘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜充蓝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一隧枫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谓苟,春花似錦官脓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至仑撞,卻和暖如春赤兴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隧哮。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工桶良, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人沮翔。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓陨帆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親采蚀。 傳聞我的和親對象是個(gè)殘疾皇子疲牵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359