惊群汇总(含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协议栈完成的,所以不会再有惊群发生。

猜你喜欢

转载自blog.csdn.net/weixin_41143631/article/details/88560377
今日推荐