历史原因,当前有一个服务专门用于处理mq消息,mq使用的阿里云rocketmq,sdk版本1.2.6(2016年)。随着业务的发展,该应用上的consumer越来越多,接近200+,导致该应用所在的ecs长时间load高,频繁报警。
二. 现象分析
该应用所在的ecs服务器load长期飙高(该ecs上只有一个服务),但cpu、io、内存等资源利用率较低,系统负载参考下图:
ECS配置:4核8G
物理cpu个数=4
单个物理CPU中核(core)的个数=1
单核多处理器
在系统负荷方面,多核CPU与多CPU效果类似,考虑系统负荷的时候,把系统负荷除以总的核心数,只要每个核心的负荷不超过1.0,就表明正常运行。 通常,n核cpu时,load<n,系统负载都属于正常情况。
三. 原因定位
tips:系统load高,不代表cpu资源不足。Load高只是代表需要运行的队列累计过多。但队列中的任务实际可能是耗cpu的,也可能是耗i/0及其他因素的。
用户进程=8.6% 内核进程 =9.7% 空闲=80% I/O等待所占用的cpu时间百分比=0.3%
通过上图CPU、内存、IO使用情况,发现三者都不高,
CPU使用率低负载高,排除cpu资源不足导致load高的可能性。
再通过vmstat查看进程、内存、I/O等系统整体运行状态,如下图:
从结果上看,io的block in和block out 并不频繁,但是system的中断数(in)、上下文切换(cs)特别频繁,进程上下文切换次数较多的情况下,很容易导致CPU将大量的时间耗费在寄存器、内核栈、以及虚拟内存等资源的保存和恢复上,进而缩短了真正运行进程的时间造成load高。
CPU寄存器,是CPU内置的容量小、但速度极快的内存。程序计数器,则是用来存储CPU正在执行的指令的位置,或者即将执行的下一条指令的位置。
他们都是CPU在运行任何任务前,必须依赖的环境,因此也被叫做CPU上下文。
CPU上下文切换,就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文,到这些寄存器和程序计数器,
最后再跳转到程序计数器所指的新位置,运行新任务。
第一个是java线程特别多 第二个是很有规律的出现每秒上下文切换100+次的线程。(具体原因后面分析)
线程数9749(非高峰)
线程数过多 部分线程每秒上下文切换次数过高
四. 代码分析
线程数过多代码层面排查,从上面代码截图可以看到,ConsumeMessageThread_由线程池进行管理,再看一下线程池的关键参数,核心线程数this.defaultMQPushConsumer.getConsumeThreadMin()、最大线程数this.defaultMQPushConsumer.getConsumeThreadMax()、无界队列LinkedBlockingQueue。
ps:由于线程池队列用的LinkedBlockingQueue无界队列,LinkedBlockingQueue的容量默认大小是Integer.Max,在任务没有填满这个容量之前不会创建新的工作线程,因此最大线程数没有任何作用。
再看一下message-consumer对核心线程数以及最大线程数的配置,发现代码层面没有特殊配置,因此使用系统默认值,即下图
由于未指定消费线程数量(ConsumeThreadNums),采用系统默认核心线程数20,最大线程数64.每个consumer初始化的时候都会创建一个核心线程数等于20的线程池,即大概率每个consumer都会存在20个线程消费消息, 导致线程数飙升(20*consumer个数),但发现这些消费线程大部分都处于sleep/wait状态,对上下文切换影响不大。
线程上下文切换次数过高代码层面排查:在rocketmq源码中无法搜索到该段代码,该应用使用阿里云sdk,在sdk中检索,查看上下文以及调用链路,会发现这段代码属于轨迹回传模块。
从这段代码以及堆栈信息可以看到问题出现在traceContextQueue.poll(5,TimeUnit.MILLISECONDS);其中traceContextQueue为有界阻塞队列,poll时,如果队列为空,会阻塞一定时间,因此会导致线程在running和time_wait之间进行频繁切换。
至于为什么要用poll(5,TimeUnit.MILLISECONDS)而不是take(),个人认为可能是为了减少网络io,5ms批量取一次丢到线程池批量上报,避免单个轨迹频繁上报?
线程;出队的逻辑调用的是 dequeue()方法,此外,它还有一个重载的方法,poll(long timeout, TimeUnit unit),如果队列为空,则会等待一段时间
所谓的公平是指阻塞的线程,按照阻塞的先后顺序访问队列,非公平是指当队列可用的时候,阻塞的线程都可以有争夺线程访问的资格,有可能先阻塞的线程最后才能访问队列。
与park对应的unpark执行或已经执行时。 线程被中断时 等待完time参数指定的毫秒数时 异常现象发生
阿里云sdk中轨迹回发模块,一个consumer有一个分发线程和一个轨迹队列以及一个轨迹数据回发线程池,分发线程从轨迹队列中取,取不到则阻塞5ms,取到塞到轨迹数据回发线程池,然后数据上报。过多的分发线程频繁在running和time_wait状态进行切换,导致系统load高。
由于代码层面未设置每个consumer消息消费的最大最小线程数,导致每个consumer都会开20个核心线程进程消息消费,导致线程数量过多消耗系统资源以及空跑。
五. 优化方案
代码层面针对每个consumer设置线程数配置项,consumer可根据承载的业务等实际情况设置核心线程数,减少整体的线程数目,避免大量线程空跑。 以上分析用的是阿里云ons 1.2.6的版本,当前已经迭代到了1.8.5版本,通过分析1.8.5版本的轨迹回传模块的源码,发现对轨迹回传增加了开关,配置轨迹回传使用单个线程(单例),即全部consumer使用一个分发线程、一个轨迹有界队列、一个轨迹上报线程池来处理,可以考虑验证通过后升版本。
作者:年糕妈妈技术团队
链接:https://juejin.cn/post/6844904173843005447
来源:稀土掘金
------------------ END ------------------
关注公众号,获取更多精彩内容