一.惊群效应简介

所谓惊群效应,就是多个进程或者线程在等待同一个事件,当事件发生时,所有进程或者线程都会被内核唤醒。然后,通常只有一个进程获得了该事件,并进行处理;其他进程在发现获取事件失败后,又继续进入了等待状态。这在一定程度上降低了系统性能。

具体来说,惊群通常发生在服务器的监听等待调用上。服务器创建监听socket,然后fork多个进程,在每个进程中调用accept或者epoll_wait等待终端的连接。

二.惊群的坏处

惊醒所有进程/线程,导致n-1个进程/线程做了无效的调度、上下文切换,cpu瞬时增高。
多个进程/线程争抢资源,所以涉及到同步问题,需对资源进行加锁保护,加解锁加大系统CPU开销。

三.惊群的几种情况

在高并发(多线程/多进程/多连接)中,会产生惊群的情况有:

  • accept惊群
  • epoll惊群
  • nginx惊群
  • 线程池惊群

3.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描述符时的惊群问题。

3.2 epoll惊群

epoll惊群分两种:

一是在fork之前创建epollfd,所有进程共用一个epoll。

二是在fork之后创建epollfd,每个进程独用一个epoll。

3.2.1 fork之前创建epollfd(新版内核已解决)

1. 主进程创建listenfd,创建epollfd。

2. 主进程fork多个子进程。

3. 每个子进程把listenfd加到epollfd中。

4. 当一个新连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发。

分析:这里的epoll惊群跟accept惊群是类似的,共享一个epollfd,加锁或标记解决。在新版本的epoll中已解决,但在内核2.6及之前是存在的。

epoll_wait的惊群问题在这个patch epoll: add EPOLLEXCLUSIVE flag 21 Jan 2016已经解决了。通过添加EPOLLEXCLUSIVE标志标识,在唤醒时只唤醒一个等待进程。相比而言,内核在解决accept的惊群时是作为一个问题进行了修复,即无需设置标志;而对于epoll_wait,则作为添加一个功能选项。这主要是因为accept等待的是一个socket,并且这个socket的连接只能被一个进程处理,内核可以很明确的进行这个预设,因此,accept只唤醒一个进程才是更优的选择。而对于epoll_wait,等待的是多个socket上的事件,有连接事件、读写事件等,这些事件可以同时被一个进程处理,也可以同时被多个进程分别处理,内核不能进行唯一进程处理的假定,因此,提供一个设置标志让用户决定。

3.2.2 fork之后创建epollfd(内核未解决)

1. 主进程创建listendfd。

2. 主进程创建多个子进程。

3. 每个子进程创建自已的epollfd。

4. 每个子进程把listenfd加入到epollfd中。

5. 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发。

分析:虽然listenfd是同一个,但每个子进程的epollfd是不同的epollfd,当新连接过来时,但内核不知道该发给哪个监听进程,accept会触发惊群。所以,对于这种情况,惊群还是会出现。

3.3 nginx惊群的解决

这里说的nginx惊群,其实就是上面的问题(fork之后创建epollfd),下面看看nginx是怎么处理惊群的。

在nginx中使用的epoll,是在创建子进程后创建的epollfd。因此,会出现上面的惊群问题,即每个子进程worker都会惊醒。

在nginx中,整个流程如下:

1 主线程创建listenfd。

2 主线程fork多个子进程(根据配置)。

3 子进程创建epollfd。

4 获得accept锁,只有一个子进程把listenfd加到epollfd中,同一时间只有一个进程会把监听描述符加到epoll中。

5 循环监听。

在nginx中,解决惊群的方法,使用了互斥锁还解决。nginx的每个worker进程,都会在函数ngx_process_events_and_timers()中处理不同的事件,然后通过ngx_process_events()封装了不同的事件处理机制,在Linux上默认采用epoll_wait()。
nginx主要在ngx_process_events_and_timers()函数中解决惊群现象。

 void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{... ...// 是否通过对accept加锁来解决惊群问题,需要工作线程数>1且配置文件打开accetp_mutexif (ngx_use_accept_mutex) {// 超过配置文件中最大连接数的7/8时,该值大于0,此时满负荷不会再处理新连接,简单负载均衡if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {// 多个worker仅有一个可以得到这把锁。获取锁不会阻塞过程,而是立刻返回,获取成功的话,// ngx_accept_mutex_held被置为1。拿到锁意味着监听句柄被放到本进程的epoll中了,如果// 没有拿到锁,则监听句柄会被从epoll中取出。if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}if (ngx_accept_mutex_held) {// 此时意味着ngx_process_events()函数中,任何事件都将延后处理,会把accept事件放到// ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中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;}}}}... ...(void) ngx_process_events(cycle, timer, flags);   // 实际调用ngx_epoll_process_events函数开始处理... ...if (ngx_posted_accept_events) { //如果ngx_posted_accept_events链表有数据,就开始accept建立新连接ngx_event_process_posted(cycle, &ngx_posted_accept_events);}if (ngx_accept_mutex_held) { //释放锁后,再处理下面的EPOLLIN EPOLLOUT请求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链表中,延迟到锁释放了再处理。
}

分析:nginx里采用了主动轮询的方法,去把监听描述符放到epoll中或从epoll移出(这个是nginx的精髓所在,因为大部分的并发架构都是被动的)。nginx中采用互斥锁去解决谁来accept问题,保证了同一时刻只有一个worker接收新连接(所以nginx并没有惊群问题)。nginx根据自已的载负(最大连接的7/8)情况,决定去不去抢锁,简单方便地解决负载,防止进程因业务太多而导致所有业务都不及时处理。

总结: nginx采用互斥锁和主动轮询的方法,使得nginx中并无惊群。

3.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通知时,定向通知某个线程的条件变量,不会出现惊群。

四. linux Os最新的避免和解决惊群的办法

Linux内核的3.9版本带来了SO_REUSEPORT特性,该特性支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,允许多个套接字bind()以及listen()同一个TCP或UDP端口,并且在内核层面实现负载均衡。
在未开启SO_REUSEPORT的时候,由一个监听socket将新接收的连接请求交给各个工作者处理。
在使用SO_REUSEPORT后,多个进程可以同时监听同一个IP和端口,然后由内核决定将新连接发送给哪个进程,显然会降低每个worker接收新连接时的锁竞争。
下面,让我们好好比较一下多进程(线程)服务器编程传统方法和使用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和端口的多个socket之间均衡选择。
当一个连接到来的时候,系统到底是怎么决定那个套接字来处理它?
对于不同内核,存在两种模式,这两种模式并不共存,一种叫做热备份模式,另一种叫做负载均衡模式。3.9内核以后,全部改为负载均衡模式。
热备份模式:一般而言,会将所有的reuseport同一个IP地址/端口的套接字挂在一个链表上,取第一个即可,工作的只有一个,其他的作为备份存在。如果该套接字挂了,它会被从链表删除,然后第二个便会成为第一个。
负载均衡模式:和热备份模式一样,所有reuseport同一个IP地址/端口的套接字会挂在一个链表上,你也可以认为是一个数组,这样会更加方便。当有连接到来时,用数据包的源IP/源端口作为一个HASH函数的输入,将结果对reuseport套接字数量取模,得到一个索引,该索引指示的数组位置对应的套接字便是工作套接字。这样就可以达到负载均衡的目的,从而降低某个服务的压力。

高并发中的惊群效应简介相关推荐

  1. nginx中的惊群效应

    文章目录 (1)什么是惊群效应 (2)惊群问题(thundering herd)的产生 (3)惊群效应影响 (4)常见的惊群效应 A.accept 惊群 B.epoll惊群 引言: 随便写写 (1)什 ...

  2. nginx如何解决惊群效应

    本文主要内容包括惊群效应简介.nginx如何解决惊群和一个仿照nginx避免惊群效应的简单例子 惊群效应 惊群简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒. ...

  3. 系统通知并发问题_玩转Java高并发?请先说明下并发下的惊群效应

    实际项目中,我们有很多高并发的场景需要考虑.设计,在高并发领域有个很有特点的名词叫惊群效应,你了解吗? 一.啥是惊群效应 啥叫惊群效应,有个例子说明的很透彻.当你往一群鸽子中间扔一块食物,虽然最终只有 ...

  4. 一个解决了惊群效应的高并发主动式服务模型

    网络框架,服务端必不可少的listen监听socket描述符,监听是否有新连接请求,讨论一个问题,多进程或者多线程模型中,listen到底该如何放置?以多线程为例,一般情况下分为mainThread( ...

  5. 玩转Java高并发?请先说明下并发下的惊群效应

    实际项目中,我们有很多高并发的场景需要考虑.设计,在高并发领域有个很有特点的名词叫惊群效应,你了解吗? 一.啥是惊群效应 啥叫惊群效应,有个例子说明的很透彻.当你往一群鸽子中间扔一块食物,虽然最终只有 ...

  6. 线程池设计中的惊群问题

    2019独角兽企业重金招聘Python工程师标准>>> 多线程编程已经是现在网络编程中常用的编程技术,设计一个良好的线程池库显得尤为重要.在 UNIX(WIN32下可以采用类似的方法 ...

  7. Linux / 惊群效应

    一.简介 当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉, 等待下一块食物到来.这样,每扔一块食物,都会惊动所有的鸽子,即为 ...

  8. 一周技术思考(第36期)-缓存踩踏与惊群效应

    图自网络 "10年前的那一天Facebook发生了什么",本想用这个题目,但不符合本系列的气质,那,那天到底发生了什么呢. Facebook的事故介绍 2010年9月23日,Fac ...

  9. Nginx解决惊群效应

    1. 惊群效应 1.1 简介 惊群问题又名惊群效应.简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒.唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现 ...

最新文章

  1. 计算机运行游戏慢怎么办,电脑运行太慢了太卡了怎么办,台式电脑运行速度慢的解决方法...
  2. 【Arduino】斑马线指示灯Zabra_crossing_WS2812
  3. C++模板基本概念及语法
  4. 为什么使用HashMap需要重写hashcode和equals方法_为什么要重写hashcode和equals方法?你能说清楚了吗...
  5. webpack 4.0 配置文件 webpack.config.js文件的放置位置
  6. 丢弃commit_git丢弃本地修改的所有文件(新增、删除、修改)
  7. 10无法勾选隐藏的项目_3ds max一直卡在启动页面无法进入
  8. java中四种修饰符
  9. L3-006 迎风一刀斩 (30 分)-PAT 团体程序设计天梯赛 GPLT
  10. linux通过rpm和yum安装包
  11. p46_IPv4地址
  12. 如何选择梯度下降法中的学习速率α(Gradient Descent Learning Rate Alpha)
  13. python-socket模块基本用法收集
  14. java怎么查看jar包_怎么查看Jar包源码?如何打开Jar文件?
  15. 电磁流量计的工作原理
  16. 如何修改服务器的dns,如何修改服务器dns地址
  17. 【云原生】Docker高级篇之网络、compose、可视化、监控
  18. PHP利用qq邮箱发邮件
  19. Find My产品|苹果Find My技术助力儿童鞋发展
  20. Linux CentOS7 VMware LAMP架构Apache用户认证、域名跳转、Apache访问日志

热门文章

  1. 理解box-sizing属性border-box,content-box
  2. CommonView for wifi抓包破解WPA无线网络
  3. 【闯王的宝藏】 水题 链表
  4. udig生成geoserver样式sld文件
  5. 如果你已经裸辞了,找不到工作,可以试试我的这个方法
  6. html选项卡切换代码,js实现简单的可切换选项卡效果
  7. Linux/Unix系统编程 五:进程
  8. 《你最美》换发型应用项目源码
  9. Linux下SSH代理
  10. Xcode12 iOS14踩坑