导语: 对不起,我是标题党,本文解决的不是我们理解的“惊群”效应,先为我们操作系统组的正下名,因为腾讯服务器的内核版本,已经解决epoll模式下的惊群现象(本文描述的现象跟惊群其实基本一致)。接下来容我详细道来这个是什么形式的“惊群”效应并如何解决。
缘起
最近很无聊,突然登录线上的某台机器,发现服务进程的CPU占用率很不一样,详细如下图:
为什么会出现这种情况呢?那到底会不会造成线上服务不稳定。然后自己通过webbench压测了一波,发现所有请求都很正常,一个请求都没丢失,而且时延也非常完成。好吧,心头大石放下,还以为还要背个事故呢。
目前调度不均衡的情况,是请求量比较少导致的。猜想,如果我把请求量压上来,进程的调度均衡情况会不会被改善呢?再压测了一波,把所有进程都压到一个相对高的CPU占用率,竟然发现各个进程的调度情况均衡了,但还是有一些差异。
猜测
然后自己一直纠结着是不是因为Linux的惊群导致。先分析下系统的内核版本,本来测试的机器是前段时间才重装的系统,应该已经解决了惊群的了啊。咋一看,内核版本已经是修复了惊群现象版本(3.10>2.6)。
惊群简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上降低了系统性能。
好吧,本来想甩下锅给操作系统组的,直接去问操作系统组为什么会出现惊群的,看来还是保留点自尊吧。
那,到底是什么原因导致的呢?先strace看看某个进程什么情况:
怎么进程会accept失败了……这不科学啊,压测工具的响应也是正常的,线上也没人反馈过出问题,应该这个是正常的逻辑。
分析
进程为啥会出现竞争效应呢?先看看所有进程的情况:
同一个设备id?所有进程都在同一个队列竞争资源。分析了下服务的代码创建多进程,具体流程是这样的:
这种方式创建多进程,在父进程创建完socket以后才fork出来,内核肯定clone同一个设备id啊。那为什么同一个设备id就会导致资源分配不均衡呢?下面我们分析下:
首先,进程epoll模式是设置了LT模式,LT模式下,每接收一个请求,内核都会唤醒进程进行接收。然而,所有子进程都是共享一个设备id,换句话来说,只能由一个进程把请求读出并处理,其他进程只是一个空转状态。因此,就会出现上面各个子进程的调度不均衡的情况,其实,这种情况我自己认为也是惊群效应,所有服务进程都被惊醒,但是accept出来是EAGAIN。但具体为什么都是某个进程占用的CPU更高,这个应该是由内核决定,具体原因我也不太清楚。
好吧,稍微修改一下:
fork();
create_listen_socket();
loop();
不幸的是,这样是启动不起来的!
详细分析了下:socket创建的时候,设置了REUSEADDR,所以原来创建多进程的,是能启动起来,因为端口公用了一个设备id。但是先fork在createsocket,端口被占用了,那能不能设置REUSEPORT。两者的区别,网上稍微搜一下就有相关资料了,具体差别是在于:SO_REUSEADDR主要改变了系统对待通配符IP地址冲突的方式,而SO_REUSEPORT允许将任意数目的socket绑定到完全相同的源地址端口对上。
然后尝试设置REUSEPORT参数,结果也是不尽人意,编译出错。
原来,REUSEPORT是只有在3.9以上的内核版本才支持,我的开发机是2.6,应该不支持这次编译。
继续深挖
好吧,问题还是不能解决,请教了一些操作系统组的高手,建议使用ET模式去解决一下这个惊群效应。修改了框架代码,编译了一个ET模式的服务进程,进行压测,webbench丢包非常严重,1000/s的包只能处理几个。ET模式是不是不通用呢?我们先看下ET的具体说明:
ET模式下accept存在的问题,考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
这样就明了了,多个请求同时唤醒一个进程,而我的accept是没有循环处理的, ET边缘触发导致多个请求操作系统只通知了一次,而逻辑才进行了一次处理,所以导致队列的包没被接收处理,从而导致丢包。
接下来改造系统,ET模式下,循环accept。OK,请求都被accept成功,但是,还是会触发EAGAIN,而且多进程之间也是调度不均衡的。(https://blog.csdn.net/dog250/article/details/80837278里面是某大神总结的,是可以通过ET模式解决LT模式的惊群现象,但是我把代码编译测试了一遍,确实还是会触发惊群,但频率没那么高,估计大神的测试代码是因为业务逻辑是空的原因导致)。
峰回路转
什么ET/LT模式都尝试过了,还是解决不了多进程之前调度不均衡的问题,想着反正不影响(原理上来说,空转情况下确实是会浪费点CPU),准备想放弃。但最后还是想尝试一下,把socket句柄设置成15(SO_REUSEPORT=15),自己定义了一个宏,然后修改了fork逻辑:
fork();
socket = create_listen_socket();
set_sock_opt(socket,SO_REUSERPORT);
loop();
启动进程,成功了!但是具体原因是为啥,机器的操作系统是不支持SO_REUSEPORT的,问下了操作系统的同事,给到的答复是目前的操作系统是打了上游的patch。再细问一下,标准库的头文件也没有SO_REUSEPORT的定义。给到的答复是头文件和内核不同步。好吧,其实我很不愿意接受了这个答复。最后的一个问题,那这样我如何确保我的所有机器是否支持SO_REUSEPORT,给到的答复是只能测试了。
经过一轮发布,发现所有机器都支持这个参数,而且进程已经支持了多进程之间的调度均衡。
另外,通过fork的顺序,也确认了每个进程管理自身的设备id,也不会出现惊群现象(不会再出现accept EAGAIN),原因是REUSEPORT,侦听同一个IP地址端口对的多个socket本身在socket层就是相互隔离的,在它们之间的事件分发是TCP/IP协议栈完成的,所以不会再有惊群发生。
写在最后
多进程情况下,都建议使用REUSEPORT,就不会出现那么多不稳定的问题。