避免惊群以及负载均衡的原理与具体实现
一、惊群效应
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 去执行队列中的事件
3)ngx_trylock_accept_mutex
尝试加锁,非阻塞,立刻返回结果
如果加锁成功,那么将监听连接 listenfd(多个端口)加入到 epoll 中
如果加锁失败,那么将 epoll 中的 listenfd(多个端口)清空
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
在 ngx_event_accept.c 有下面的内容,表示 nginx 单进程的所有连接总数的八分之一,减去剩下的空闲连接数量(还没连接的数量)。空闲连接数量小了,那么 ngx_accept_disable 越大,让出机会就更大了。
默认每个进程,每次只处理一条连接,如果想每次处理多条连接,需要开启 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等技术内容,立即学习
评论