避免惊群以及负载均衡的原理与具体实现
一、惊群效应
1、发生惊群效应的原因
1)使用accept
主进程(master进程)fork出⼀批⼦进程(worker进程),⼦进程继承了⽗进程的监听端⼝(sockfd),就会出现accept惊群效应。子进程的fd属于同一个文件,若两个⼦进程同时调⽤accept进⾏阻塞监听,两个进程都会被挂起来,内核会在这个socket的等待队列wait queue链表中将两个PID记录下来以便唤醒。Linux2.6版本之后引⼊了⼀个标记为WQ_FLAG_EXCLUSIVE解决了这种惊群效应。这个在内核就已经处理了。
2)使用epoll监听公共的端口
epoll与直接accept不同,epoll需要先调⽤epoll_create在内核中创建⼀个epollfd
epoll会把当前进程挂在fd的等待队列下,但是默认情况下这种挂载不会设置互斥标志,意思着当设备有事情产⽣进⾏等待队列唤醒的时候,如果当前队列有多个进程在等待,则会全部唤醒,当多个进程共享同⼀个监听端⼝并且都使⽤epoll进⾏多路复⽤的监听时,epoll将这些进程都挂在同⼀个等待队列下。
在linux的后续版本中,解决了这个问题,通过设置标志位,使得只会唤醒队列中的⼀个进程。
既然linux内核中都解决了惊群效应,为什么nginx还去实现一下?
因此内核解决比较晚,nginx很早就使用该方法去避免惊群效应了,并且为了适应不同内核版本,一直保留着,用于避免惊群效应。
2、Nginx中惊群惊群效应
每个进程都有一个reactor(fork出来的)也就是epoll,都监听着一个公共的listenfd。如果该端口有一个(连接)事件时,只有一个进程能accept成功,那么其他epoll_wait都要被唤醒,这样多个子进程在 accept 建立新连接时会有争抢,且子进程数量越多问题越明显,从而造成系统性能下降的现象。
3、Nginx中惊群效应的解决方案
多个进程的epoll都监听公共的端口会出现惊群现象,那么在nginx中有采用accept_mutex的办法,轮流去监听epoll中的建立连接的事件,保证同一时刻只有一个进程在监听listenfd(用于建立连接,进行accept)
普通事件不会出现惊群效应吗?
出现惊群效应是由于多个进程都监听同一个fd,如listenfd,在接受新的连接(accept)时候,都是监听listenfd,是同一个文件。但是对于普通的读写事件来说,就是clientfd,多个进程中的epoll不会出现重复监听同一个clientfd。也就是说,每个clientfd只可能在某个进程中的epoll中
4、Nginx中的源码
1)简述避免惊群效应的源码
1.对进程加锁
尝试加锁,加锁成功后会附加上一个标志位NGX_POST_EVENTS
2.通过标志位,将事件加入到队列中
后续执行ngx_epoll_process_events,才会把事件(accept_events或者events)分别加入到队列中(ngx_posted_accept_events和ngx_posted_events),只有加入到队列中的事件,在后续才能执行
3.执行队列中的事件,并解锁
接下去就是将队列中的事件执行了,先执行accept事件,然后解锁,再执行普通事件
4.概括:
如果进程成功上锁,那么会进入NGX_POST_EVENTS状态,那么事件会延迟执行,accept事件和普通事件都会分别加入到各自的队列中,然后再执行
如果进程没有上锁成功,如果检测到普通事件,直接执行普通事件(不可能出现accept事件,只有上锁的进程的epoll才能监听到,是因为在加锁过程中还添加了listen监听事件,没有加锁的进程epoll是没法监听到的)。
下面是源码实现的细节
C++后台开发系统学习视频地址:C/C++Linux服务器开发高级架构师/C++后台开发架构师
以下学习资料,C++后台开发面试题,教学视频,C++后台开发学习路线图,免费分享有需要的可以自行添加:学习资料群720209036 自取
2)ngx_process_events_and_timers
在ngx_worker_process_cycle调用该函数。
该部分主要是对进程之间避免惊群效应和实现负载均衡
让ngx_epoll_process_events去检测事件,加入队列中(也有直接执行的情况)
然后通过ngx_event_process_posted去执行队列中的事件
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{ngx_uint_t flags;ngx_msec_t timer, delta;//timer是用于epoll_wait等待的时间,后面传入的参数//delta 记录处理epoll_process_events花费的时间if (ngx_timer_resolution) {timer = NGX_TIMER_INFINITE;//时间设置无穷大(实际上就是ngx_msec_t的最大值)flags = 0;} else {flags = NGX_UPDATE_TIME;timer = ngx_event_find_timer();//找到最近一个定时器触发所需要的时间#if (NGX_WIN32)/* handle signals from master in case of network inactivity */if (timer == NGX_TIMER_INFINITE || timer > 500) {timer = 500;}#endif}if (ngx_use_accept_mutex) {//用于负载均衡,如果当前进程中连接池中连接数量较多 或者 待连接数比较少,那么进程会采用让出,不执行任务。if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//尝试加锁//1.如果获得锁,会将所有listenfd加入到epoll//2.如果没有获得锁,会删除当前进程epoll中的listenfdif (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {//加锁return;}//如果持有了锁if (ngx_accept_mutex_held) {flags |= NGX_POST_EVENTS;//获得锁后的标志位NGX_POST_EVENTS(网络读写事件延迟处理标志)} else {if (timer == NGX_TIMER_INFINITE|| timer > ngx_accept_mutex_delay){timer = ngx_accept_mutex_delay;}}}}//之前队列中剩下的一些未完成的事件放在队列ngx_posted_next_events中,也就是要优先处理的//把这些事件放入ngx_posted_events队列头部,并清空ngx_posted_next_eventsif (!ngx_queue_empty(&ngx_posted_next_events)) {ngx_event_move_posted_next(cycle);timer = 0;//这些任务是上一轮的,要紧急处理,后续epoll_wait要立刻返回,不延迟}delta = ngx_current_msec;//里面执行epoll_wait//网络连接事件,放入ngx_posted_accept_events队列//网络读写事件,放入ngx_posted_events队列(void) ngx_process_events(cycle, timer, flags);//执行ngx_epoll_process_eventsdelta = ngx_current_msec - delta;ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"timer delta: %M", delta);//如果没有加锁成功,那么ngx_posted_accept_events会是空的 //执行accept事件(ngx_event_accept)ngx_event_process_posted(cycle, &ngx_posted_accept_events);if (ngx_accept_mutex_held) {//解锁ngx_shmtx_unlock(&ngx_accept_mutex);}ngx_event_expire_timers();//执行普通事件ngx_event_process_posted(cycle, &ngx_posted_events);
}
3)ngx_trylock_accept_mutex
尝试加锁,非阻塞,立刻返回结果
如果加锁成功,那么将监听连接listenfd(多个端口)加入到epoll中
如果加锁失败,那么将epoll中的listenfd(多个端口)清空
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//尝试加锁(通过CAS操作,将mtx->lock设置为当前worker进程的pid,没有占用则mtx->lock为0)ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"accept mutex locked");//之前已经持有了锁,那么就直接返回,继续监听端口(ngx_accept_events 在 epoll 里不使用)//标志位 ngx_accept_mutex_held 为 1 表示当前进程已经获取了 ngx_accept_mutex 锁if (ngx_accept_mutex_held && ngx_accept_events == 0) {return NGX_OK;}// 注册accept事件(之前没有持有锁,需要注册 epoll 事件监听端口,遍历监听端口列表,加入 epoll 连接事件,开始接受请求)if (ngx_enable_accept_events(cycle) == NGX_ERROR) {ngx_shmtx_unlock(&ngx_accept_mutex);// 如果监听失败就需要立即解锁,函数结束return NGX_ERROR;}// 已经成功将监听事件加入 epollngx_accept_events = 0;ngx_accept_mutex_held = 1;// 设置已经获得锁的标志return NGX_OK;}// trylock失败,未获得锁ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,"accept mutex lock failed: %ui", ngx_accept_mutex_held);// 若当前进程获取 ngx_accept_mutex 锁失败,并且 ngx_accept_mutex_held 为 1,则为错误情况// 本次未获得锁,但之前持有锁,也就是说之前在监听端口if (ngx_accept_mutex_held) {// 遍历监听端口列表,删除 epoll 监听连接事件,不接受请求if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {return NGX_ERROR;}ngx_accept_mutex_held = 0;//没有获得锁的标志}return NGX_OK;
}
5、补充
进程之间的锁如何实现呢?
通过CAS去设置进程之间共享内存的变量mtx->lock,如果该值是自身pid,表示锁被当前进程占有了,如果该值是0,表示没有进程占有锁.
什么时候会去争夺锁?
通过设置定时器(红黑树),获得下一个定时器触发时间,时间主要用于epoll_wait的等待,时间一到,就执行epoll_wait下面内容,执行完后,在worker process循环中又会回到ngx_process_events_and_timers,获得下一个定时器触发时间,然后去判断锁是否被占用,继续执行,以此循环。
二、负载均衡
在nginx不同进程之间,进程采用让出加锁机会的方式来实现负载均衡,通过当前进程拒绝监听新连接的次数ngx_accept_disabled来控制,需要让出的次数。它取决于,当前进程中的总连接数 和 待连接(空闲连接)的数量
实现方式
在ngx_event.c中
ngx_accept_disabled表示当前进程中拒绝accept新连接的次数,也就是说当通过定时器轮询到当前进程的时候,如果ngx_accept_disabled>0,那么就不会去获取accept_mutex锁(当前进程不会将accept_event加入epoll中去),并且ngx_accept_disabled-1
void
ngx_process_events_and_timers(ngx_cycle_t *cycle){...if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//获取accept_mutex锁,用于后续accept连接}...
}
在ngx_event_accept.c有下面的内容,表示nginx单进程的所有连接总数的八分之一,减去剩下的空闲连接数量(还没连接的数量)。空闲连接数量小了,那么ngx_accept_disable越大,让出机会就更大了。
ngx_accept_disabled = ngx_cycle->connection_n / 8- ngx_cycle->free_connection_n;
默认每个进程,每次只处理一条连接,如果想每次处理多条连接,需要开启multi_accept
实现进程间负载均衡的一些变量:
注意这些变量都是每个进程私有的,非共享内存的变量。
ngx_accept_disabled:当前进程连接拒绝的次数,先让出给其他进程
ngx_cycle->connection_n:当前进程已连接的总连接数,如果当前进程已经连接总数比较多的话,那么让出的情况会变大
ngx_cycle->free_connection_n:空闲连接数(待连接数量),还未连接的数量如果比较多,那么让出的情况就会变少
参考资料
推荐一个零声教育C/C++后台开发的免费公开课程,个人觉得老师讲得不错,分享给大家:C/C++后台开发高级架构师,内容包括Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习
原文链接:Nginx源码阅读:避免惊群以及负载均衡的原理与具体实现_菊头蝙蝠的博客-CSDN博客
避免惊群以及负载均衡的原理与具体实现相关推荐
- Nginx源码实现的细枝末节 11个阶段的实现丨Nginx Filter|中间件开发|惊群效应|负载均衡丨组件丨c/c++linux服务器开发
Nginx源码实现的细枝末节 11个阶段的实现 视频讲解如下,点击观看: Nginx源码实现的细枝末节 11个阶段的实现丨Nginx Filter|中间件开发|惊群效应|负载均衡丨组件丨c/c++li ...
- 搭建服务器集群——Windows7系统中nginx与IIS服务器搭建集群实现负载均衡
转载:https://www.cnblogs.com/xiongze520/p/10308720.html 分布式,集群,云计算机.大数据.负载均衡.高并发······当耳边响起这些词时,做为一个菜鸟 ...
- 使用LVS+TUN搭建集群实现负载均衡
使用LVS+TUN搭建集群实现负载均衡 TUN模式的概述与工作原理 TUN模式服务概述: IP Tunneling(IP隧道) --可以在不同地域,不同网段 Director分配请 ...
- Nginx负载均衡的原理及流程分析
负载均衡的原理及处理流程 系统的扩展可以分为纵向扩展和横向扩展. 纵向扩展是从单机的角度出发,通过增加系统的硬件处理能力来提升服务器的处理能力 横向扩展是通过添加机器来满足大型网站服务的处理能力. 这 ...
- Tomcat服务器集群与负载均衡实现
一.前言 在单一的服务器上执行WEB应用程序有一些重大的问题,当网站成功建成并开始接受大量请求时,单一服务器终究无法满足需要处理的负荷量,所以就有点显得有点力不从心了.另外一个常见的问题是会产生单点故 ...
- 阿里P8架构师谈:分布式、集群、负载均衡、分布式数据一致性的区别与关联
服务器集群: 1.集群概念 集群就是一组相互独立的计算机,通过高速的网络组成一个计算机系统.服务器集群就是指将很多服务器集中起来一起进行同一种服务,在客户端看来就像是只有一个服务器. 2.集群的特点和 ...
- 阿里云MVP乔帮主:五大类型负载均衡的原理场景详解(文末赠书)
乔帮主 读完需要 21 分钟 速读仅需 5 分钟 导读:本文摘自于阿里云 MVP."乔帮主"乔锐杰所撰写的<阿里云运维架构实践秘籍>一书,我们发现常见负载均衡 LVS. ...
- Nginx、Haproxy、LVS负载均衡从原理到部署(一)
先说些题外话,我记得51博客的号早就注册了,之前只是不间断上来看看别人写的技术文章涨涨见识,自己后面开始接触到运维这块,就想到把平时学的一些相关技术记录到博客上来,只是方便自己可以随时上网回顾,由于多 ...
- 10分钟搭建服务器集群——Windows7系统中nginx与IIS服务器搭建集群实现负载均衡...
10分钟搭建服务器集群--Windows7系统中nginx与IIS服务器搭建集群实现负载均衡 原文:10分钟搭建服务器集群--Windows7系统中nginx与IIS服务器搭建集群实现负载均衡 分布式 ...
最新文章
- JAVA基础知识(5)
- linux不能到达网关,linux – 在网关服务器上,netNS可以限制一个...
- 【echarts】echarts开发流程详解
- 当我们扩张时——技术商业策略圆桌第一弹
- Kibana 创建索引 POST 403 (forbidden) on create index
- Visual Studio Code(VScode)设置为中文界面
- Scikit-learn 更新至0.24版,这10个新特性你需要了解
- LeetCode 剑指Offer 64.求1,2到n的和, 不使用循环/判断及乘除
- android工程师专业技能,android开发工程师简历中的专业技能怎么写
- 中文 Uncode 编码范围
- 2019年六项扣除 个税计算器
- 不是你不懂黑盒攻击,而是你还不懂SurFree
- python获取模块的名称_python获取当前模块的名称
- 股票价格在随机漫步吗?用 Python 来告诉你
- html5带倍速功能的视频播放器(加速2倍,1.5倍播放)
- 小狐狸VF的一些命令及技巧
- torch.bmm(a,b)
- 快速排序优化——三数取中法
- 【高等数学】函数与极限
- matlab画立体星星教程,抖音星空画的人怎么画 制作教程完整视频步骤分享