Linux accept()/epoll_wait()惊群问题与解决方案

2023-12-01 23:58

本文主要是介绍Linux accept()/epoll_wait()惊群问题与解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

问题的来源:

参考《UNP 第三版》第30章“客户/服务器设计范式”中“30.6 TCP预先派生子进程服务器程序”

// 为便于说明问题,代码已简化
int main(int argc, char **argv)
{int listenfd = Tcp_Listen();for (int i = 0; i < nchildren; i++){if ((pid = fork()) == 0){child_main(i, listenfd, addrlen);}}for(;;){pause();}
}pid_t child_main(int i, int listenfd, int addrlen)
{struct sockaddr clientAddr;socklen_t clientAddrLen;for(;;){int connfd = accept(listenfd, &clientAddr, &clientAddrLen);web_child(connfd);close(connfd);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

在如上所示的代码中,主进程创建了用于监听的socket描述符listenfd,每个子进程阻塞调用accept(listenfd, ),等待获取客户端的新建连接connfd,并放入web_child(connfd)中执行。 
因为多个子进程被同时阻塞在同一个监听socket上,当有客户端的新建连接接入时,所有被阻塞的子进程都被唤醒,但是只有执行最快的那个子进程调用accept(listenfd, )能够得到客户连接connfd,其它子进程调用accept(listenfd, )只会等到EGAIN。这种多个子进程被唤醒后又无事可做的状态被称为“惊群”,因为每次进程调度的系统开销相对较大,所以对于高并发服务器,频繁的“惊群”必然导致的服务器性能低下。

针对30.6样例中存在的问题,30.7和30.8给出了两种方案分别使用“文件锁”和“线程互斥锁”保护accept()。简单的说,就是所有的子进程在调用accept()之前,先获取一个,只有得到的进程才能继续执行accept()函数,其它进程都被锁阻塞了。并且,通过验证,这种加锁的方式对于解决“惊群”问题是有效的。

for(;;) 
{    lock(); // 文件锁或互斥锁    int client = accept(...);    unlock();    if (client < 0) continue;    ...  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

另外,30.9中还给出了一个“主进程aceept客户连接,再将客户连接描述符传递给子进程处理”的方案。不过,通过实验证明:和相比,进程间传递描述符的效率更低。这算是题外话吧。

其实,如果只考虑Linux系统,在Linux 2.6版本以后,内核内核已经解决了accept()函数的“惊群”问题,大概的处理方式就是,当内核接收到一个客户连接后,只会唤醒等待队列上的第一个进程或线程。所以,如果服务器采用accept阻塞调用方式,在最新的Linux系统上,已经没有“惊群”的问题了。

但是,对于实际工程中常见的服务器程序,大都使用select、poll或epoll机制,此时,服务器不是阻塞在accept,而是阻塞在select、poll或epoll_wait,这种情况下的“惊群”仍然需要考虑。接下来以epoll为例分析:

在早期的Linux版本中,内核对于阻塞在epoll_wait的进程,也是采用全部唤醒的机制,所以存在和accept相似的“惊群”问题。新版本的的解决方案也是只会唤醒等待队列上的第一个进程或线程,所以,新版本Linux 部分的解决了epoll的“惊群”问题。所谓部分的解决,意思就是:对于部分特殊场景,使用epoll机制,已经不存在“惊群”的问题了,但是对于大多数场景,epoll机制仍然存在“惊群”。

1、场景一:epoll_create()在fork子进程之前:

如果epoll_create()调用在fork子进程之前,那么epoll_create()创建的epfd 会被所有子进程继承。接下来,所有子进程阻塞调用epoll_wait(),等待被监控的描述符(包括用于监听客户连接的监听描述符)出现新事件。如果监听描述符发生可读事件,内核将阻塞队列上排在第一位的进程/线程唤醒,被唤醒的进程/线程继续执行accept()函数,得到新建立的客户连接描述符connfd。这种情况下,任何一个子进程被唤醒并执行accept()函数都是没有问题的。

但是,接下来,子进程的工作如果是调用epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);将新建连接的描述符connfd加入到epfd中统一监控的话,因为,当前的epfd是在fork之前创建的,此时系统中只有一个epoll监控文件,即所有子进程共享一个epoll监控文件。任何一个进程(父进程或子进程)向epoll监控文件添加、修改和删除文件描述符时,都会影响到其它进程的epoll_wait。 
后续,当connfd描述符上接收到客户端信息时,内核也无法保证每次都是唤醒同一个进程/线程,来处理这个连接描述符connfd上的读写信息(其它进程可能根本就不认识connfd;或者在不同进程中,相同的描述符对应不同的客户端连接),最终导致连接处理错误。(另外,不同的线程处理同一个连接描述符,也会导致发送的信息乱序)

所以,应该避免epoll_create()在fork子进程之前。关于这一点,据说libevent的文档中有专门的描述。

2、场景二:epoll_create()在fork子进程之后:

如果epoll_create()在fork子进程之后,则每个进程都有自己的epoll监控文件(当某个进程将新建连接的描述符connfd加入到本进程的epfd中统一监控,不会影响其它进程的epoll_wait),但是为了实现并发监听,所有的子进程都会调用 
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); 
监听描述符加入到监控文件中,也就是说所有子进程都在通过epoll机制轮询同一个监听描述符。如果有新的客户端请求接入,监听描述符出现POLLIN事件(表示描述符可读,有新连接接入),此时内核会唤醒所有的进程,所以“惊群”的问题依然存在。

对于这种情况下的“惊群”问题,Nginx的解决方案和《UNP 第三版》第30章中30.7和30.8给出的加锁方案类似,大概就是通过互斥锁对每个进程从epoll_wait到accept之间的处理通过互斥量保护。需要注意的是,对于这种加锁操作,每次只有一个子进程能执行epoll_wait和accept,具体哪个进程得到执行,要看内核调度。所以,为了解决负载均衡的问题,Nginx的解决方案中,每个进程有一个当前连接计数,如果当前连接计数超过最大连接的7/8,该进程就停止接收新的连接。

lock()    
epoll_wait(...);   
accept(...);    
unlock(...);   
  • 1
  • 2
  • 3
  • 4

另外,我在想,如果不考虑多进程,而是用多线程实现,因为,线程调度的开销比进程要小很多,那么在多线程下,是否就可以不用考虑惊群的问题,当然这个结论需要具体的测试数据,后面有空准备测试一下。

3、利用SO_REUSEPORT解决epoll的惊群问题

网上这方面的内容也非常多,大家可以自行搜索。因为这是利用内核新版本新特性的解决方案,应该算是终极解决方案吧。 
利用SO_REUSEPORT解决epoll的惊群

这篇关于Linux accept()/epoll_wait()惊群问题与解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/443188

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

缓存雪崩问题

缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。 解决方案: 1、使用锁进行控制 2、对同一类型信息的key设置不同的过期时间 3、缓存预热 1. 什么是缓存雪崩 缓存雪崩是指在短时间内,大量缓存数据同时失效,导致所有请求直接涌向数据库,瞬间增加数据库的负载压力,可能导致数据库性能下降甚至崩溃。这种情况往往发生在缓存中大量 k

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta