高并发中的惊群效应
second60 20180726
目录
高并发中的惊群效应
<https://blog.csdn.net/second60/article/details/81252106#%E9%AB%98%E5%B9%B6%E5%8F%91%E4%B8%AD%E7%9A%84%E6%83%8A%E7%BE%A4%E6%95%88%E5%BA%94>
1.惊群效应简介
<https://blog.csdn.net/second60/article/details/81252106#1.%E6%83%8A%E7%BE%A4%E6%95%88%E5%BA%94%E7%AE%80%E4%BB%8B>
2. 操作系统的惊群
<https://blog.csdn.net/second60/article/details/81252106#2.%20%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%9A%84%E6%83%8A%E7%BE%A4>
3. 惊群的坏处
<https://blog.csdn.net/second60/article/details/81252106#3.%20%E6%83%8A%E7%BE%A4%E7%9A%84%E5%9D%8F%E5%A4%84>
3.1 坏处
<https://blog.csdn.net/second60/article/details/81252106#3.1%20%E5%9D%8F%E5%A4%84>
3.2 其他
<https://blog.csdn.net/second60/article/details/81252106#3.2%20%E5%85%B6%E4%BB%96>
4 惊群的几种情况
<https://blog.csdn.net/second60/article/details/81252106#4%20%E6%83%8A%E7%BE%A4%E7%9A%84%E5%87%A0%E7%A7%8D%E6%83%85%E5%86%B5>
4.1 accept惊群(新版内核已解决)
<https://blog.csdn.net/second60/article/details/81252106#4.1%20accept%E6%83%8A%E7%BE%A4(%E6%96%B0%E7%89%88%E5%86%85%E6%A0%B8%E5%B7%B2%E8%A7%A3%E5%86%B3)>
4.2 epoll惊群
<https://blog.csdn.net/second60/article/details/81252106#4.2%20epoll%E6%83%8A%E7%BE%A4>
4.2.1 fork之前创建epollfd(新版内核已解决)
<https://blog.csdn.net/second60/article/details/81252106#4.2.1%20fork%E4%B9%8B%E5%89%8D%E5%88%9B%E5%BB%BAepollfd(%E6%96%B0%E7%89%88%E5%86%85%E6%A0%B8%E5%B7%B2%E8%A7%A3%E5%86%B3)>
4.2.2 fork之后创建epollfd(内核未解决)
<https://blog.csdn.net/second60/article/details/81252106#4.2.2%20fork%E4%B9%8B%E5%90%8E%E5%88%9B%E5%BB%BAepollfd(%E5%86%85%E6%A0%B8%E6%9C%AA%E8%A7%A3%E5%86%B3)>
4.3 nginx惊群的解决
<https://blog.csdn.net/second60/article/details/81252106#4.3%20nginx%E6%83%8A%E7%BE%A4%E7%9A%84%E8%A7%A3%E5%86%B3>
4.4 线程池惊群
<https://blog.csdn.net/second60/article/details/81252106#4.4%20%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%83%8A%E7%BE%A4>
5 高并发设计
<https://blog.csdn.net/second60/article/details/81252106#5%20%E9%AB%98%E5%B9%B6%E5%8F%91%E8%AE%BE%E8%AE%A1>
5.1 例1
<https://blog.csdn.net/second60/article/details/81252106#5.1%20%E4%BE%8B1>
5.2 例2
<https://blog.csdn.net/second60/article/details/81252106#5.2%20%E4%BE%8B2>
5.3 例3
<https://blog.csdn.net/second60/article/details/81252106#5.3%20%E4%BE%8B3>
5.4 例4
<https://blog.csdn.net/second60/article/details/81252106#5.4%20%E4%BE%8B4>
6 总结
<https://blog.csdn.net/second60/article/details/81252106#6%20%E6%80%BB%E7%BB%93>
1.惊群效应简介
当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉,
等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。
简单地说:就是扔一块食物,所有鸽子来抢,但最终只一个鸽子抢到了食物。
语义分析:食物只有一块,最终只有一个鸽子抢到,但是惊动了所有鸽子,每个鸽子都跑过来,消耗了每个鸽子的能量。(这个很符合达尔文的进化论,物种之间的竞争,适者生存。)
2. 操作系统的惊群
在多进程/多线程等待同一资源时,也会出现惊群。即当某一资源可用时,多个进程/线程会惊醒,竞争资源。这就是操作系统中的惊群。
3. 惊群的坏处
3.1 坏处
* 惊醒所有进程/线程,导致n-1个进程/线程做了无效的调度,上下文切换,cpu瞬时增高
* 多个进程/线程争抢资源,所以涉及到同步问题,需对资源进行加锁保护,加解锁加大系统CPU开销
3.2 其他
1. 在某些情况:惊群次数少/进(线)程负载不高,惊群可以忽略不计
4 惊群的几种情况
在高并发(多线程/多进程/多连接)中,会产生惊群的情况有:
* accept惊群
* epoll惊群
* nginx惊群
* 线程池惊群
4.1 accept惊群(新版内核已解决)
以多进程为例,在主进程创建监听描述符listenfd后,fork()多个子进程,多个进程共享listenfd,accept是在每个子进程中,当一个新连接来的时候,会发生惊群。
由上图所示:
* 主线程创建了监听描述符listenfd = 3
* 主线程fork 三个子进程共享listenfd=3
* 当有新连接进来时,内核进行处理
在内核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(新版内核已解决)
1. 主进程创建listenfd, 创建epollfd
2. 主进程fork多个子进程
3. 每个子进程把listenfd,加到epollfd中
4. 当一个连接进来时,会触发epoll惊群,多个子进程的epoll同时会触发
分析:
这里的epoll惊群跟accept惊群是类似的,共享一个epollfd, 加锁或标记解决。在新版本的epoll中已解决。但在内核2.6及之前是存在的。
4.2.2 fork之后创建epollfd(内核未解决)
1. 主进程创建listendfd
2. 主进程创建多个子进程
3. 每个子进程创建自已的epollfd
4. 每个子进程把listenfd加入到epollfd中
5. 当一个连接进来时,会触发epoll惊群,多个子进程epoll同时会触发
分析:
因为每个子进程的epoll是不同的epoll, 虽然listenfd是同一个,但新连接过来时,
accept会触发惊群,但内核不知道该发给哪个监听进程,因为不是同一个epoll。所以这种惊群内核并没有处理。惊群还是会出现。
4.3 nginx惊群的解决
这里说的nginx惊群,其实就是上面的问题(fork之后创建epollfd),下面看看nginx是怎么处理惊群的。
在nginx中使用的epoll,是在创建进程后创建的epollfd。因些会出现上面的惊群问题。即每个子进程worker都会惊醒。
在nginx中,流程。
1
主线程创建listenfd
2
主线程fork多个子进程(根据配置)
3
子进程创建epollfd
4
获到accept锁,只有一个子进程把listenfd加到epollfd中
同一时间只有一个进程会把监听描述符加到epoll中
5
循环监听
在nginx中,解决惊群的方法,使用了互斥锁还解决。
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 {
//获得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主动解决惊群流程
1
子进程loop
2
判断是否使用accept加锁
3
判断是否满负荷最大连接数的7/8(是不处理)
4
多个worker竞争accept_mutex锁(主动精髓)
5
获得锁成功
获得锁失败
6
(监听句柄加到本进程的epoll)
监听句柄会被从epoll中取出
7
事件加入到链表中
(accept事件放到ngx_posted_accept_events链表
epollin|out事件放到ngx_posted_events链表)
修改epoll_wait的超时时间
(为了下次更早抢锁)
8
如果有accept_event就处理新连接
9
释放锁accept_mutex
10
处理正常的数据读写请求
11
子进程继续loop
12
(注:上面的总结如果有不对的地方,麻烦大牛提出来,谢谢)
分析:
* 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通知时,定向通知某个线程的条件变量,不会出现惊群
5 高并发设计
以多线程为例,进程同理
例
主线程
子线程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的设计非常巧妙,很好的解决了惊群的产生,所以没有惊群,同时也根据各进程的负载主动去决定要不要接受新连接,负载比较优。
6 总结
高并发设计,仁者见仁,智者见智,如果要求不高,随便拿个常用的开源库,就可能支撑。如果对业务有特殊要求,那么根据业务去选择,如网关服(可用高并发连接的开源库libevent/libev),消息队列(zmq/RabbitMQ/ActiveMQ/Kafka),数据缓存(redis/memcached),分布式等。
研究高并发有一段时间了,总结下我自已的理解,怎么样才算是高并发呢?单进程百万连接,单进程百万QPS?
先说说基本概念
高并发连接:指的是连接的数量,对服务端来说,一个套接字对就是一个连接,连接和本地
文件描述符无关,不受本地文件描述符限制,只跟内存有关,假设一个套接字对占用服 务器8k内存,那么1G内存=1024*1024/8 =
131072。因此连接数跟内存有关。
1G = 10万左右连接,当然这是理论,实际要去除内核占用,其他进程占用,和本进程其他占用。
假哪一个机器32G内存,那个撑个100万个连接是没有问题的。
如果是单个进程100万连,那就更牛B了,但一般都不会这么做,因为如果此进程宕了,那么,所有业务都影响了。所以一般都会分布到不同进程,不同机器,一个进程出问题了,不会影响其他进程的处理。(这也是nginx原理)
PV : 每天的总访问量pave view, PV = QPS * (24*0.2) * 3600 (二八原则)
QPS: 每秒请求量。假如每秒请求量10万,假如机器为16核,那么启16个线程同时工作, 那么每个线程同时的请求量= 10万/ 16核 = 6250QPS。
按照二八原则,一天24小时,忙时=24*0.2 = 4.8小时。
则平均一天总请求量=4.8 * 3600 *10万QPS = 172亿8千万。
那么每秒请求10万并发量,每天就能达到172亿的PV。这算高并发吗?
丢包率: 如果客端端发10万请求,服务端只处理了8万,那么就丢了2万。丢包率=2/10 =
20%。丢包率是越小越好,最好是没有。去除,网络丢包,那么就要考虑内核里的丢包 问题,因此要考虑网卡的吞吐量,同一时间发大多请求过来,内核会不会处理不过来,
导致丢包。
稳定性:一个高并发服务,除了高并发外,最重要的就是稳定了,这是所有服务都必须的。
一千QPS能处理,一万QPS也能处理,十万QPS也能处理,当然越多越好。不要因为 业务骤增导致业务瘫痪,那失败是不可估量的。因为,要有个度,当业务增加到一定程
度,为了保证现有业务的处理,不处理新请求业务,延时处理等。同时保证代码的可靠。
因此,说到高并发,其实跟机器有并,内存,网卡,CPU核数等有关,一个强大的服务器,比如:32核,64G内存,网卡吞吐很大,那么单个进程,开32个线程,做一个百万连接,百万QPS的服务,是可行的。
本身 按例3去做了个高并发的设计,做到了四核4G内存的虚拟机里,十万连接,十万QPS,很稳定,没加业务,每核CPU %sys 15左右 %usr
5%左右。如果加了业务,应该也是比较稳定的。有待测试。当然例3是有自已的缺点的。
同进,也希望研究高并发的同学,一起来讨论高并发服务设计思想。(加微:luoying140131)
热门工具 换一换