Linux / 惊群效应
一、简介
当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
二、OS惊群简介
在多进程/多线程等待同一资源时,也会出现惊群。即当某一资源可用时,多个进程/线程会惊醒,竞争资源。这就是操作系统中的惊群。
三、坏处
- 惊醒所有进程/线程,导致n-1个进程/线程做了无效的调度,上下文切换,cpu瞬时增高
- 多个进程/线程争抢资源,所以涉及到同步问题,需对资源进行加锁保护,加解锁加大系统CPU开销
四、常见场景
在高并发(多线程/多进程/多连接)中,会产生惊群的情况有:
- accept 惊群
- epoll 惊群
- nginx 惊群
- 线程池惊群
4.1 accept 惊群
以多进程为例,在主进程创建监听描述符 listenfd 后,fork() 多个子进程,多个进程共享 listenfd,accept 是在每个子进程中,当一个新连接来的时候,会发生惊群。
在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。
在内核2.6及之后,解决了惊群问题,办法是在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:
1)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部。若没有这个标志的入口项,则添加到队首。
2)当 wake up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
对于互斥等待的行为,比如对一个 listen 后的socket描述符,多线程阻塞 accept 时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生。这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。
4.2 epoll 惊群
epoll惊群分两种:
1、是在fork之前创建 epollfd,所有进程共用一个epoll。
2、是在fork之后创建 epollfd,每个进程独用一个epoll。
4.2.1 fork之前创建epollfd(新版内核已解决)
- 主进程创建 listenfd,创建 epollfd。
- 主进程 fork 多个子进程
- 每个子进程把 listenfd,加到 epollfd 中。
- 当一个连接进来时,会触发 epoll 惊群,多个子进程的 epoll 同时会触发。
这里的epoll惊群跟 accept 惊群是类似的,共享一个 epollfd,加锁或标记解决,在新版本的epoll中已解决,但在内核2.6及之前是存在的。
4.2.2 fork之后创建epollfd(内核未解决)
- 主进程创建 listendfd 。
- 主进程创建多个子进程
- 每个子进程创建自已的 epollfd。
- 每个子进程把 listenfd 加入到 epollfd 中。
- 当一个连接进来时,会触发 epoll 惊群,多个子进程 epoll 同时会触发。
因为每个子进程的 epoll 是不同的epoll, 虽然 listenfd 是同一个,但新连接过来时, accept 会触发惊群。因为内核不知道该发给哪个监听进程,因为不是同一个 epoll 。所以这种惊群内核并没有处理,惊群还是会出现。
4.3 nginx惊群
这里说的nginx惊群,其实就是上面的问题(fork之后创建epollfd),下面看看 nginx 是怎么处理惊群的。
在nginx中使用的epoll,是在创建进程后创建的 epollfd 。因些会出现上面的惊群问题。即每个子进程worker都会惊醒。
在nginx中,流程。
- 主线程创建 listenfd 。
- 主线程fork多个子进程(根据配置)。
- 子进程创建 epollfd 。
- 获得 accept 锁,只有一个子进程把 listenfd 加到epollfd中。(同一时间只有一个进程会把监听描述符加到 epoll 中)
- 循环监听。
void ngx_process_events_and_timers(ngx_cycle_t *cycle){// 忽略....//ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。//当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1if (ngx_use_accept_mutex) {//ngx_accept_disabled表示此时满负荷,没必要再处理新连接了,//我们在nginx.conf曾经配置了每一个nginx worker进程能够处理的最大连接数,//当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,//将不再去处理新连接,这也是个简单的负载均衡if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//获得accept锁,多个worker仅有一个可以得到这把锁。//获得锁不是阻塞过程,都是立刻返回,获取成功的话ngx_accept_mutex_held被置为1。//拿到锁,意味着监听句柄被放到本进程的epoll中了,//如果没有拿到锁,则监听句柄会被从epoll中取出。if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}//拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,//任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中,// epollin|epollout事件都放到ngx_posted_events链表中if (ngx_accept_mutex_held) {flags |= NGX_POST_EVENTS;} else {//拿不到锁,也就不会处理监听的句柄,//这个timer实际是传给epoll_wait的超时时间,//修改为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,//以免新连接长时间没有得到处理if (timer == NGX_TIMER_INFINITE|| timer > ngx_accept_mutex_delay){timer = ngx_accept_mutex_delay;}}}}// 忽略....//linux下,调用ngx_epoll_process_events函数开始处理(void) ngx_process_events(cycle, timer, flags);// 忽略....//如果ngx_posted_accept_events链表有数据,就开始accept建立新连接if (ngx_posted_accept_events) {ngx_event_process_posted(cycle, &ngx_posted_accept_events);}//释放锁后再处理下面的EPOLLIN EPOLLOUT请求if (ngx_accept_mutex_held) {ngx_shmtx_unlock(&ngx_accept_mutex);}if (delta) {ngx_event_expire_timers();}ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"posted events %p", ngx_posted_events);//然后再处理正常的数据读写请求。因为这些请求耗时久,//所以在ngx_process_events里NGX_POST_EVENTS标志将事件//都放入ngx_posted_events链表中,延迟到锁释放了再处理。if (ngx_posted_events) {if (ngx_threaded) {ngx_wakeup_worker_thread(cycle);} else {ngx_event_process_posted(cycle, &ngx_posted_events);}}}
分析:
- nginx里采用了主动的方法去把监听描述符放到epoll中或从epoll移出(这个是nginx的精髓所在,因为大部份的并发架构都是被动的)
- nginx中用采互斥锁去解决谁来accept问题,保证了同一时刻,只有一个worker接收新连接(所以nginx并没有惊群问题)
- nginx根据自已的载负(最大连接的7/8)情况,决定去不去抢锁,简单方便地解决负载,防止进程因业务太多而导致所有业务都不及时处理
nginx采用互斥锁和主动的方法,避免了惊群,使得nginx中并无惊群。
4.4 线程池惊群
在多线程设计中,经常会用到互斥和条件变量的问题。当一个线程解锁并通知其他线程的时候,就会出现惊群的现象。
pthread_mutex_lock / pthread_mutex_unlock
线程互斥锁的加锁及解锁函数。
pthread_cond_wait
线程池中的消费者线程等待线程条件变量被通知;
pthread_cond_signal / pthread_cond_broadcast
生产者线程通知线程池中的某个或一些消费者线程池,接收处理任务;
这里的惊群现象出现在 3 里,pthread_cond_signal,语义上看是通知一个线程。调用此函数后,系统会唤醒在相同条件变量上等待的一个或多个线程(可参看手册)。如果通知了多个线程,则发生了惊群。
正常的用法:
- 所有线程共用一个锁,共用一个条件变量。
- 当 pthread_cond_signal 通知时,就可能会出现惊群。
解决惊群的方法:
- 所有线程共用一个锁,每个线程有自已的条件变量。
- pthread_cond_signal 通知时,定向通知某个线程的条件变量,不会出现惊群
五、高并发设计
以多线程为例,进程同理
栗子 |
主线程 |
子线程 epoll |
是否有惊群 |
新版本是否已经解决 |
参考 |
1 |
listenfd epollfd |
共用 listenfd 和 epollfd 子线程accept |
epoll 惊群 |
已解决 |
被动 |
2 |
listenfd |
共用 listenfd, 每个线程创建 epollfd listenfd 加入 epoll |
epoll 惊群 |
未解决 |
被动 |
3 |
listenfd 主线程accept并分发connfd |
每个线程创建 epollfd 接收主线程分发的 connfd |
无惊群 accept 瓶颈 |
(无用) |
被动 |
4 |
listenfd |
共用 listenfd, 每个线程创建 epollfd 互斥锁决定加入 / 移出 epoll |
无惊群 |
nginx |
5.1 栗1
分析
主线程创建 listenfd 和 epollfd,子线程共享并把 listenfd 加入到epoll中,旧版中会出现惊群,新版中已解决了惊群。
缺点
应用层并不知道内核会把新连接分给哪个线程,可能平均,也可能不平均如果某个线程已经最大负载了,还分过来,会增加此线程压力甚至崩溃。
总结
因为例1并不是最好的方法,因为没有解决负载和分配问题。
5.2 栗2
分析
主线程创建 listenfd,子线程创建 epollfd,,把 listenfd 加入到 epoll 中, 这种方法是无法避免惊群的问题。每次有新连接时,都会唤醒所有的accept线程,但只有一个 accept 成功,其他的线程 accept 失败 EAGAIN 。
总结
栗 2 解决不了惊群的问题,如果线程超多,惊群越明显。如果真正开发中,可忽略惊群,或者需要用惊群,那么使用此种设计也是可行的。
5.3 栗3
分析
主线程创建 listenfd,每个子线程创建 epollfd,主线程负责accept,并发分新connfd给负载最低的一个线程,然后线程再把connfd 加入到 epoll 中。无惊群现象。
总结:
主线程只用 accept 用,可能会主线程没干,或连接太多处理不过来,accept 瓶颈(一般情况不会产生)。主线程可以很好地根据子线程的连接来分配新连接,有比较好的负载并发量也比较大,自测(单进程十万并发连接QPS十万,四核四G内存,很稳定)
5.4 栗4
这是 nginx 的设计,无疑是目前最优的一种高并发设计,无惊群。
nginx本质
同一时刻只允许一个 nginx worker 在自己的 epoll 中处理监听句柄。它的负载均衡也很简单,当达到最大 connection 的 7/8时,本 worker 不会去试图拿 accept 锁,也不会去处理新连接。这样其他 nginx worker 进程就更有机会去处理监听句柄,建立新连接了。而且,由于 timeout 的设定,使得没有拿到锁的worker进程,去拿锁的频繁更高。
总结
nginx的设计非常巧妙,很好的解决了惊群的产生,所以没有惊群。同时也根据各进程的负载主动去决定要不要接受新连接,负载比较优。
六、总结
研究高并发有一段时间了,总结下我自已的理解,怎么样才算是高并发呢?单进程百万连接,单进程百万 QPS ?
先说说基本概念
6.1 高并发连接
指的是连接的数量,对服务端来说,一个套接字对就是一个连接,连接和本地文件描述符无关,不受本地文件描述符限制,只跟内存有关,假设一个套接字对占用服 务器 8k 内存,那么1G内存=1024*1024/8 = 131072。因此连接数跟内存有关。1G = 10万左右连接,当然这是理论,实际要去除内核占用,其他进程占用,和本进程其他占用。假哪一个机器 32G 内存,那个撑个100万个连接是没有问题的。如果是单个进程100万连,那就更牛B了,但一般都不会这么做,因为如果此进程宕了,那么,所有业务都影响了。所以一般都会分布到不同进程,不同机器,一个进程出问题了,不会影响其他进程的处理。(这也是nginx原理)
6.2 PV
每天的总访问量 pave view, PV = QPS * (24*0.2) * 3600 (二八原则)
6.3 QPS
每秒请求量。假如每秒请求量10万,假如机器为16核,那么启16个线程同时工作, 那么每个线程同时的请求量 = 10万/ 16核 = 6250QPS。
按照二八原则,一天24小时,忙时=24*0.2 = 4.8小时。
则平均一天总请求量 = 4.8 * 3600 *10万QPS = 172亿8千万。
那么每秒请求10万并发量,每天就能达到172亿的PV。这算高并发吗?
6.4 丢包率
如果客端端发10万请求,服务端只处理了8万,那么就丢了2万。丢包率=2/10 = 20%。丢包率是越小越好,最好是没有。去除网络丢包,那么就要考虑内核里的丢包问题,因此要考虑网卡的吞吐量,同一时间发大多请求过来,内核会不会处理不过来, 导致丢包。
6.5 稳定性
一个高并发服务,除了高并发外,最重要的就是稳定了,这是所有服务都必须的。 一千 QPS 能处理,一万QPS 也能处理,十万 QPS 也能处理,当然越多越好。不要因为业务骤增导致业务瘫痪,那失败是不可估量的。因为,要有个度,当业务增加到一定程 度,为了保证现有业务的处理,不处理新请求业务,延时处理等,同时保证代码的可靠。
因此,说到高并发,其实跟机器有并,内存,网卡,CPU核数等有关,一个强大的服务器,比如:32核,64G内存,网卡吞吐很大,那么单个进程,开32个线程,做一个百万连接,百万QPS的服务,是可行的。
本身按栗 3 去做了个高并发的设计,做到了四核4G内存的虚拟机里,十万连接,十万QPS,很稳定,没加业务,每核CPU %sys 15左右 %usr 5%左右。如果加了业务,应该也是比较稳定的,有待测试。当然例3是有自已的缺点的。
同进,也希望研究高并发的同学,一起来讨论高并发服务设计思想。(加微:luoying140131)
(SAW:Game Over!)
Linux / 惊群效应相关推荐
- Linux惊群效应详解(最详细的了吧)
https://blog.csdn.net/lyztyycode/article/details/78648798?locationNum=6&fps=1 linux惊群效应 详细的介绍什么是 ...
- Linux惊群效应之Nginx解决方案
结论 不管还是多进程还是多线程,都存在惊群效应,本篇文章使用多进程分析. 在Linux2.6版本之后,已经解决了系统调用Accept的惊群效应(前提是没有使用select.poll.epoll等事件机 ...
- Linux惊群效应详解
inux惊群效应 详细的介绍什么是惊群,惊群在线程和进程中的具体表现,惊群的系统消耗和惊群的处理方法. 1.惊群效应是什么? 惊群效应也有人叫做雷鸣群体效应,不过叫什么,简言之,惊群现象 ...
- Nginx源码实现的细枝末节 11个阶段的实现丨Nginx Filter|中间件开发|惊群效应|负载均衡丨组件丨c/c++linux服务器开发
Nginx源码实现的细枝末节 11个阶段的实现 视频讲解如下,点击观看: Nginx源码实现的细枝末节 11个阶段的实现丨Nginx Filter|中间件开发|惊群效应|负载均衡丨组件丨c/c++li ...
- Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx丨惊群效应|error|负载均衡|Openresty丨C/C++Linux服务器开发丨中间件
Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx 视频讲解如下,点击观看: Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx丨惊群效应|error|负载均衡|O ...
- 系统通知并发问题_玩转Java高并发?请先说明下并发下的惊群效应
实际项目中,我们有很多高并发的场景需要考虑.设计,在高并发领域有个很有特点的名词叫惊群效应,你了解吗? 一.啥是惊群效应 啥叫惊群效应,有个例子说明的很透彻.当你往一群鸽子中间扔一块食物,虽然最终只有 ...
- 解决多进程模式下引起的“惊群”效应
导语: 对不起,我是标题党,本文解决的不是我们理解的"惊群"效应,先为我们操作系统组的正下名,因为腾讯服务器的内核版本,已经解决epoll模式下的惊群现象(本文描述的现象跟惊群其实 ...
- 一周技术思考(第36期)-缓存踩踏与惊群效应
图自网络 "10年前的那一天Facebook发生了什么",本想用这个题目,但不符合本系列的气质,那,那天到底发生了什么呢. Facebook的事故介绍 2010年9月23日,Fac ...
- Nginx解决惊群效应
1. 惊群效应 1.1 简介 惊群问题又名惊群效应.简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒.唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现 ...
最新文章
- linux deploy ENV 目录,手机安装linux deploy 安装和配置
- Java中常见的几种类型转换
- 浙江理工大学2019年4月赛
- rabitMQ-centos7安装
- 计算机网络基础:网络分类和拓扑结构知识笔记
- Zoom Host可以真正看到您的所有私人消息吗?
- [見好就收]NET 2.0 - WinForm Control - DataGridView 编程36计
- 车联网服务non-RESTful架构改造实践
- 【2017年第2期】专题:大数据管理与分析
- 嘉年华专访 | 国际上智能运维研究
- 使用Photoshop+960 Grid System模板进行网页设计
- 【C语言】16-预处理指令2-条件编译
- 威纶触摸屏485轮询通讯_【威纶】触摸屏 界面制作软件 EBpro使用手册.pdf
- 台式计算机未识别网络,台式机显示未识别网络怎么办
- sourcetree的日常使用
- 【Unity3D脚本】Transform类
- 剪辑视频怎么学?手把手教你自学视频剪辑
- 用c语言编写打猎小游戏,使用c语言编写简单小游戏.docx
- 解决 plt.savefig() 生成空白图片的问题
- gmtime ()函数
热门文章
- [转载]如何编写无法维护的代码(3)
- win11 WSL centos7安装docker命令整理
- Harbor API整理:获取项目下的所有镜像
- kafka2.5.0创建主题topic命令
- git放弃本地文件修改
- Spark弹性式数据集RDDs
- 解决ubuntu16.E: 无法获得锁 /var/lib/dpkg/lock - open (11: 资源暂时不可用) E: 无法锁定管理目录(/var/lib/dpkg/),是否有其他进程正占用它?
- Scala泛型:协变和逆变
- scala整型的类型及取值范围
- Spring Data ElasticSearch示例--查询索引库