1. 惊群效应

1.1 简介

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

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

简单地说:就是扔一块食物,所有鸽子来抢,但最终只一个鸽子抢到了食物。

1.2 引发的问题

惊群效应会占用系统资源,降低系统性能。多进程/线程的唤醒,涉及到的一个问题是上下文切换问题。频繁的上下文切换带来的一个问题是数据将频繁的在寄存器与运行队列中流转。极端情况下,时间更多的消耗在进程/线程的调度上,而不是执行

食物只有一块,最终只有一个鸽子抢到,但是惊动了所有鸽子,每个鸽子都跑过来,消耗了每个鸽子的能量。

2. 常见的惊群效应

在 Linux 下,我们常见的惊群效应发生于我们使用 accept 以及我们 select 、poll 或 epoll 等系统提供的 API 来处理我们的网络链接。

2.1 accept 惊群

以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。

由上图所示:

  1. 主线程创建了监听描述符listenfd = 3
  2. 主线程fork 三个子进程共享listenfd=3
  3. 当有新连接进来时,内核进行处理

在内核2.6之前,所有进程accept都会惊醒,但只有一个可以accept成功,其他返回EGAIN。

在内核2.6及之后,解决了惊群,在内核中增加了一个互斥等待变量。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:

  1. 当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.
  2. 当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。
  3. 对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列 的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

2.2 epoll惊群

epoll惊群分两种:

  • 在fork之前创建epollfd,所有进程共用一个epoll;
  • 在fork之后创建epollfd,每个进程独用一个epoll.

2.2.1 fork之前创建epollfd(内核2.6已解决)

  1. 主进程创建listenfd, 创建epollfd
  2. 主进程fork多个子进程
  3. 每个子进程把listenfd,加到epollfd中
  4. 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发

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

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

  1. 主进程创建listendfd
  2. 主进程创建多个子进程
  3. 每个子进程创建自已的epollfd
  4. 每个子进程把listenfd加入到epollfd中
  5. 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发

分析:因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时, accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。

3. 内核解决惊群问题详解

首先如前面所说,Accept 的惊群问题在 Linux Kernel 2.6 之后就被从内核的层面上解决了。但是 EPOLL 怎么办?在 2016 年一月,Linux 之父 Linus 向内核提交了一个补丁

参见 epoll: add EPOLLEXCLUSIVE flag

其中的关键代码是

if (epi->event.events & EPOLLEXCLUSIVE)add_wait_queue_exclusive(whead, &pwq->wait);elseadd_wait_queue(whead, &pwq->wait);

简而言之,通过增加一个 EPOLLEXCLUSIVE 标志位作为辅助。如果用户开启了 EPOLLEXCLUSIVE ,那么在加入内核等待队列时,使用 add_wait_queue_exclusive 否则则使用 add_wait_queue

至于这两个函数的用法,可以参考这篇文章Handing wait queues

其中有这样一段描述

The add_wait_queue( ) function inserts a nonexclusive process in the first position of a wait queue list. The add_wait_queue_exclusive( ) function inserts an exclusive process in the last position of a wait queue list.

好了,我们现在来改一下我们的代码(内核版本要在 Linux Kernel 4.5)之后

#include <errno.h>
#include <fcntl.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>#define SERVER_ADDRESS "0.0.0.0"
#define SERVER_PORT 10086
#define WORKER_COUNT 4
#define MAXEVENTS 64static int create_and_bind_socket() {int fd = socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in server_address;server_address.sin_family = AF_INET;inet_pton(AF_INET, SERVER_ADDRESS, &server_address.sin_addr);server_address.sin_port = htons(SERVER_PORT);bind(fd, (struct sockaddr *)&server_address, sizeof(server_address));return fd;
}static int make_non_blocking_socket(int sfd) {int flags, s;flags = fcntl(sfd, F_GETFL, 0);if (flags == -1) {perror("fcntl error");return -1;}flags |= O_NONBLOCK;s = fcntl(sfd, F_SETFL, flags);if (s == -1) {perror("fcntl set error");return -1;}return 0;
}int worker_process(int listenfd, int epoll_fd, struct epoll_event *events,int k) {while (1) {int n;n = epoll_wait(epoll_fd, events, MAXEVENTS, -1);printf("Worker %d pid is %d get value from epoll_wait\n", k, getpid());sleep(0.2);for (int i = 0; i < n; i++) {if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) ||(!(events[i].events & EPOLLIN))) {printf("%d\n", i);fprintf(stderr, "epoll err\n");close(events[i].data.fd);continue;} else if (listenfd == events[i].data.fd) {struct sockaddr in_addr;socklen_t in_len;int in_fd;in_len = sizeof(in_addr);in_fd = accept(listenfd, &in_addr, &in_len);if (in_fd == -1) {printf("worker %d accept failed\n", k);break;}printf("worker %d accept success\n", k);close(in_fd);}}}return 0;
}int main() {int listen_fd, s;int epoll_fd;struct epoll_event event;struct epoll_event *events;listen_fd = create_and_bind_socket();if (listen_fd == -1) {abort();}s = make_non_blocking_socket(listen_fd);if (s == -1) {abort();}s = listen(listen_fd, SOMAXCONN);if (s == -1) {abort();}epoll_fd = epoll_create(MAXEVENTS);if (epoll_fd == -1) {abort();}event.data.fd = listen_fd;// add EPOLLEXCLUSIVE supportevent.events = EPOLLIN | EPOLLEXCLUSIVE;s = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);if (s == -1) {abort();}events = calloc(MAXEVENTS, sizeof(event));for (int i = 0; i < WORKER_COUNT; i++) {printf("create worker %d\n", i);int pid = fork();if (pid == 0) {worker_process(listen_fd, epoll_fd, events, i);}}int status;wait(&status);free(events);close(listen_fd);return EXIT_SUCCESS;
}

然后我们来看看效果

诶?为什么还是有两个进程被唤醒了?原因在于 EPOLLEXCLUSIVE 只保证唤醒的进程数小于等于我们开启的进程数,而不是直接唤醒所有进程,也不是只保证唤醒一个进程

我们来看看官方的描述

Sets an exclusive wakeup mode for the epoll file descriptor that is being attached to the target file descriptor, fd. When a wakeup event occurs and multiple epoll file descriptors are attached to the same target file using EPOLLEXCLUSIVE, one or more of the epoll file descriptors will receive an event with epoll_wait(2). The default in this scenario (when EPOLLEXCLUSIVE is not set) is for all epoll file descriptors to receive an event. EPOLLEXCLUSIVE is thus useful for avoid‐ ing thundering herd problems in certain scenarios.

恩,换句话说,就目前而言,系统并不能严格保证惊群问题的解决。很多时候我们还是要依靠应用层自身的设计来解决

4. Nginx解决惊群效应

目前而言,应用解决惊群有两种策略

  1. 这是可以接受的代价,那么我们暂时不管。这是我们大多数的时候的策略
  2. 通过加锁或其余的手段来解决这个问题,最典型的例子是 Nginx

我们来看看 Nginx 怎么解决这样的问题的:

Nginx通过控制争抢处理socket的进程数量抢占ngx_accept_mutex锁解决惊群现象。只有一个ngx_accept_mutex锁,谁拿到锁,谁处理该socket的请求。

同时:如果当前进程的连接数>最大连接数*7/8,则该进程不参与本轮竞争。

//nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件。下面代码是ngx_process_events_and_timers()函数的核心部分。
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{  //ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1         if (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 {  //工作进程抢占锁,抢占成功的进程将ngx_accept_mutex_held变量置为1。拿到锁,意味着socket被放到本进程的epoll中了,如果没有拿到锁,则socket会被从epoll中取出。  //此处trylock是非阻塞锁,如果没有抢占到锁,进程会立刻返回,处理自己监听的描述符上的读写事件。if(pthread_mutex_trylock(&ngx_accept_mutex)){ngx_accept_mutex_held = 1;}else{//设置time时间,500ms后就去争抢锁,使得没有拿到锁的worker进程,去拿锁的频繁更高,确保每个进程可以处理几乎相同数量的fd的读写。timer = 500;ngx_accept_mutex_held = 0;}//拿到锁的话,置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;}}//继续epoll_wait等待处理事件int num = epoll_wait(epollfd, events, length, timer);for(int i=0; i<num; ++i){......//如果是读事件if (revents & EPOLLIN){//有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理//新连接事件队列ngx_posted_accept_events//用户读写事件队列ngx_posted_eventsif (flags & NGX_POST_EVENTS){queue = rev->accept ? &ngx_posted_accept_events:&ngx_posted_events;ngx_post_event(rev, queue);}else//处理{rev->handler(rev);}}//如果是写事件if (revents & EPOLLOUT){//同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中 if (flags & NGX_POST_EVENTS) {ngx_post_event(rev, &ngx_posted_events);}else//处理{rev->handler(rev);}}}//先处理新用户的连接事件ngx_event_process_posted(cycle, &ngx_posted_accept_events);//释放处理新连接的锁if(ngx_accept_mutex_held){pthread_mutex_unlock(&ngx_accept_mutex);}//再处理已建立连接的用户读写事件ngx_event_process_posted(cycle, &ngx_posted_events);
}

nginx从抢锁、释放锁到处理事件的整个过程,我已经结合代码做了注释,相信大家对整个过程应该已经不陌生了。至于pthread_mutex_trylock()中进程是如何抢占锁的,这就有赖于实现抢占的算法了,此处只是解释处理过程,并不关心抢占实现原理。感兴趣的同学可以自己搜索相关资料。

  1. 先处理新用户的连接事件,再释放处理新连接的锁:如果刚释放锁,就有新连接,刚获得锁的进程要给等待队列中添加sockfd时,此时原获得锁的进程也要从等待队列中删除sockfd,TCP的三次握手的连接是非线程安全的。为了避免产生错误,使得将sockfd从等待队列中删除后,再让新的进程抢占锁,处理新连接。
  2. 拿到锁,将任务放在任务队列中,不是立刻去处理:每个进程要处理新连接事件,必须拿到锁,当前进程将新连接事件的sokect添加到任务队列中,立即释放锁,让其他进程尽快获得锁,处理用户的连接。

你可能有个疑问,如果没有加锁,有新事件连接时,所有的进程都会被唤醒执行accept,有且仅有一个进程会accept返回成功,其他进程都重新进入睡眠状态。现在有了锁,在发生accept之前,进程们要去抢占锁,也是有且仅有一个进程会抢到锁,其他进程也是重新进入睡眠状态。即:不论是否有accept锁,都会有很多进程被唤醒再重新进入睡眠状态的过程,那惊群现象如何解释

其实,锁不能解决惊群现象,惊群现象是没办法解决的,很多进程被同时唤醒是一个必然的过程。Nginx中通过检查当前进程的连接数是否>最大连接数*7/8来判断当前进程是否能处理新连接,减少被唤醒的进程数量,也实现了简单的负载均衡。锁只能保证不让所有的进程去调用accept函数,解决了很多进程调用accept返回错误,锁解决的是惊群现象的错误,并不是解决了惊群现象!

Nginx解决惊群效应相关推荐

  1. nginx如何解决惊群效应

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

  2. Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx丨惊群效应|error|负载均衡|Openresty丨C/C++Linux服务器开发丨中间件

    Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx 视频讲解如下,点击观看: Linux环境,手把手带你实现一个Nginx模块,深入了解Nginx丨惊群效应|error|负载均衡|O ...

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

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

  4. Linux惊群效应之Nginx解决方案

    结论 不管还是多进程还是多线程,都存在惊群效应,本篇文章使用多进程分析. 在Linux2.6版本之后,已经解决了系统调用Accept的惊群效应(前提是没有使用select.poll.epoll等事件机 ...

  5. Nginx真的消除了惊群效应么?不

    想必看这边文章的人都了解什么是惊群效应以及在Nginx中惊群效应的解决方案,如果不了解,百度一下比比皆是. 接下来直入主题,nginx中的这套解决方案真的不会出现惊群效应么? 答案为否.我们看下具体情 ...

  6. Linux惊群效应详解(最详细的了吧)

    https://blog.csdn.net/lyztyycode/article/details/78648798?locationNum=6&fps=1 linux惊群效应 详细的介绍什么是 ...

  7. epoll惊群效应深度剖析

    epoll惊群效应深度剖析 前情提要 我们一个基于Nginx+uWSGI+python的服务最近在高峰期经常会遇到负载高导致一些请求报错的情况,在单机qps只有差不多2000-3000左右的时候内核的 ...

  8. Linux惊群效应详解

    inux惊群效应 详细的介绍什么是惊群,惊群在线程和进程中的具体表现,惊群的系统消耗和惊群的处理方法. 1.惊群效应是什么?        惊群效应也有人叫做雷鸣群体效应,不过叫什么,简言之,惊群现象 ...

  9. tcp 端口复用与惊群效应(REUSEADDR、REUSEPORT)

    我在之前的一篇文章中,介绍了我在之前的项目中遇到的端口复用,windows 的udp里端口复用导致了一个bug,具体的链接参考如下: 关于Socket中端口复用_zhc的博客-CSDN博客_socke ...

最新文章

  1. 程序员学历低,该被歧视吗? | 每日趣闻
  2. JUnit4单元测试报错问题:method initializationerror not found
  3. Django环境安装和创建工程
  4. delphi trichviewedit 设置一行的段落_HTML中的文本与段落(3)
  5. 关于Android的自动化测试,你需要了解的5个测试框架
  6. maven项目中引用jave
  7. golang程序员前景怎么样?Python、Java、go语言的优势互比
  8. star面试法则面试案例_案例面试技巧
  9. 对象存储BOS服务介绍
  10. 5.3.3—二叉查找树—Validate Binary Sear Tree
  11. 计算机网络(五):IPv6
  12. Spark的任务调度
  13. android 显示表情符号,吓一跳 同一表情符号iPhone和Android机显示效果差别这么大...
  14. vue.js执行if语句后程序终止甚至后面的else语句也不会执行
  15. WORD2003相关问题
  16. python的pyaudio教程入门_Python PyAudio 安装使用
  17. 根据用户IP地址来判断用户所在城市
  18. avformat_open_input()
  19. 基于改进区域生长算法的PET-CT成像自动肺实质分割方法(笔记六)
  20. Java开发人员必知必会的20种常用类库和API

热门文章

  1. PHP检测及判断手机登录用户是安卓或爱疯(iPhone)客户端
  2. c语言send发送结构体,Socket编程中用send发送结构体
  3. 安装好UTAU后没法使用
  4. 如何打开html的文档结构图,如何在Word中设置文档结构图
  5. matlab圆周率计算,matlab代码求圆周率的简单算法
  6. SAP系统和微信集成的系列教程之二:如何通过微信公众号消费API
  7. cf edu #132 Div.2
  8. kotlin的wifi获取列表和连接指定wifi
  9. Microsoft Excel 直方图
  10. 【工具类】Themeleaf 模板中 strings 的常用方法整理