驚羣彙總(含epoll驚羣)

原文查看https://www.cnblogs.com/Anker/p/7071849.html

         https://blog.csdn.net/lyztyycode/article/details/78648798

https://blog.csdn.net/dog250/article/details/80837278

彙總一下他們的博客供自己複習使用,我就不自己寫了,他們的可以彙總一下。

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

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

 

 主進程創建了socket、bind、listen之後,fork()出來多個進程,每個子進程都開始循環處理(accept)這個listen_fd。每個進程都阻塞在accept上,當一個新的連接到來時候,所有的進程都會被喚醒,但是其中只有一個進程會接受成功,其餘皆失敗,重新休眠。
       那麼這個問題真的存在嗎?
       歷史上,Linux的accpet確實存在驚羣問題,但現在的內核都解決該問題了。即,當多個進程/線程都阻塞在對同一個socket的接受調用上時,當有一個新的連接到來,內核只會喚醒一個進程,其他進程保持休眠,壓根就不會被喚醒。
 

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<string.h>
#include<netinet/in.h>
#include<unistd.h>
 
#define PROCESS_NUM 10
int main()
{
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    int connfd;
    int pid;
 
    char sendbuff[1024];
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(1234);
    bind(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(fd, 1024);
    int i;
    for(i = 0; i < PROCESS_NUM; ++i){
        pid = fork();
        if(pid == 0){
            while(1){
                connfd = accept(fd, (struct sockaddr *)NULL, NULL);
                snprintf(sendbuff, sizeof(sendbuff), "接收到accept事件的進程PID = %d\n", getpid());
 
                send(connfd, sendbuff, strlen(sendbuff)+1, 0);
                printf("process %d accept success\n", getpid());
                close(connfd);
            }
        }
    }
    //int status;
    wait(0);
    return 0;
}

按邏輯會引起阻塞,但是linux內核已經解決了accept()函數的“驚羣”現象,大概的處理方式就是,當內核接收到一個客戶連接後,只會喚醒等待隊列上的第一個進程(線程),所以如果服務器採用accept阻塞調用方式,在最新的linux系統中已經沒有“驚羣效應”了。

當一個連接到來的時候,系統到底是怎麼決定那個套接字來處理它?

對於不同內核,存在兩種模式,這兩種模式並不共存,一種叫做熱備份模式,另一種叫做負載均衡模式,3.9內核以後,全部改爲負載均衡模式。

熱備份模式:一般而言,會將所有的reuseport同一個IP地址/端口的套接字掛在一個鏈表上,取第一個即可,工作的只有一個,其他的作爲備份存在,如果該套接字掛了,它會被從鏈表刪除,然後第二個便會成爲第一個。
負載均衡模式:和熱備份模式一樣,所有reuseport同一個IP地址/端口的套接字會掛在一個鏈表上,你也可以認爲是一個數組,這樣會更加方便,當有連接到來時,用數據包的源IP/源端口作爲一個HASH函數的輸入,將結果對reuseport套接字數量取模,得到一個索引,該索引指示的數組位置對應的套接字便是工作套接字。這樣就可以達到負載均衡的目的,從而降低某個服務的壓力。
但是但是epoll的部分問題Linux沒有"解決”,需要自己使用上注意一些。

  1. 創建epoll句柄,初始化相關數據結構
  2. 爲epoll句柄添加文件句柄,註冊睡眠entry的回調
  3. 事件發生,喚醒相關文件句柄睡眠隊列的entry,調用其回調
  4. 喚醒epoll睡眠隊列的task,蒐集並上報數據

Linux內核的3.9版本帶來了SO_REUSEPORT特性,該特性支持多個進程或者線程綁定到同一端口,提高服務器程序的性能,允許多個套接字bind()以及listen()同一個TCP或UDP端口,並且在內核層面實現負載均衡。

在未開啓SO_REUSEPORT的時候,由一個監聽socket將新接收的連接請求交給各個工作者處理,看圖示:

在使用SO_REUSEPORT後,多個進程可以同時監聽同一個IP:端口,然後由內核決定將新鏈接發送給哪個進程,顯然會降低每個工人接收新鏈接時鎖競爭


下面讓我們好好比較一下多進程(線程)服務器編程傳統方法和使用SO_REUSEPORT的區別
運行在Linux系統上的網絡應用程序,爲了利用多核的優勢,一般使用以下典型的多進程(多線程)服務器模型:

1.單線程listener/accept,多個工作線程接受任務分發,雖然CPU工作負載不再成爲問題,但是仍然存在問題:

       (1)、單線程listener(圖一),在處理高速率海量連接的時候,一樣會成爲瓶頸

        (2)、cpu緩存行丟失套接字結構現象嚴重。

2.所有工作線程都accept()在同一個服務器套接字上呢?一樣存在問題:

        (1)、多線程訪問server socket鎖競爭嚴重。

        (2)、高負載情況下,線程之間的處理不均衡,有時高達3:1。

        (3)、導致cpu緩存行跳躍(cache line bouncing)。

        (4)、在繁忙cpu上存在較大延遲。

上面兩種方法共同點就是很難做到cpu之間的負載均衡,隨着核數的提升,性能並沒有提升。甚至服務器的吞吐量CPS(Connection Per Second)會隨着核數的增加呈下降趨勢。

下面我們就來看看SO_REUSEPORT解決了什麼問題:

        (1)、允許多個套接字bind()/listen()同一個tcp/udp端口。每一個線程擁有自己的服務器套接字,在服務器套接字上沒有鎖的競爭。

        (2)、內核層面實現負載均衡

        (3)、安全層面,監聽同一個端口的套接字只能位於同一個用戶下面。

        (4)、處理新建連接時,查找listener的時候,能夠支持在監聽相同IP和端口的多個sock之間均衡選擇。
 

上結論:於是乎,使用了reuseport,一切都變得明朗了:

 

不再依賴mem模型
不再擔心驚羣
爲什麼reuseport沒有驚羣?首先我們要知道驚羣發生的原因,就是同時喚醒了多個進程處理一個事件,導致了不必要的CPU空轉。爲什麼會喚醒多個進程,因爲發生事件的文件描述符在多個進程之間是共享的。而reuseport呢,偵聽同一個IP地址端口對的多個socket本身在socket層就是相互隔離的,在它們之間的事件分發是TCP/IP協議棧完成的,所以不會再有驚羣發生。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章