概述

12月30号,天气晴,阳光明媚
今天下午项目就要上线了,我正在美滋滋的规划上线之后的生活。但是偏偏手贱,无意中打开了测试环境的服务器,发现了一个令人恐惧的事情,CPU总利用率最高爆到了150%。
宛如一道晴天霹雳击中我的天灵盖,瞬间冷汗直冒。明明昨天服务器还是正常的,没理由突然爆表啊?代码已经停止更新了呀,这不科学!

分析问题

下午就要上线了,这要是发到生产还得了?本着狗命要紧的态度,赶紧定位问题。
先看top的利用率分布

CPU性能问题分析定位_系统调用

CPU性能问题分析定位_测试环境_02

细看CPU单核的利用率,其实并不是很高,50%左右。但是现在测试环境明明没有很多业务在执行。而且利用率几乎都分布在内核空间,这很明显不科学。

打印cpu热点
执行 perf top,看一下占用cpu最好的函数是什么

CPU性能问题分析定位_内核空间_03

排名最高的是schedule,其次是system call after swapgs,第三是sched_yield。我们来分析一下这前三个排名的函数是做什么的
1.schedule:函数schedule()实现调度程序。它的任务是从运行队中找到一个进程,并随后将CPU分配给这个进程
2.system call after swapgs:这个函数是实现系统调用的。通常用户空间向内核空间发起请求时,必然会经历系统调用
3.sched_yield:在资源竞争情况很严重时,执行这个函数会主动放弃当前线程的CPU使用权,然后一个新的线程会占用CPU

综上所述,可以大致猜测出问题的原因。一定是有大批量的任务在向内核请求系统调度,但是资源已经不足了,然后好死不死的又偏偏调用了sched_yield这个自毁函数,导致cpu的使用权被让出来。cpu通过schedule函数玩命的调度切换。于是cpu就炸了。

定位问题

为了验证刚刚的猜测,这里做两步操作。
第一步:打印内核日志。结果发现内核里面全部在执行sched_yield,放弃cpu。

CPU性能问题分析定位_测试环境_04

稍加思索之后,觉得应该改一个配置** /proc/sys/kernel/sched_compat_yield**
把这个配置从0改成1,可以让sched_yield()系统调用更加有效,让它使用更少的cpu。美滋滋的改完之后发现,cpu利用率一点也没降下来。稍加思索之后想明白了,我的目的是阻断cpu调用sched_yield()而自爆,而不是让他调用的更加丝滑啊!这个方法行不通

第二步:打印线程堆栈

CPU性能问题分析定位_百度_05

线程堆栈里面打印结果如下:

  •  
java.lang.Thread.State: RUNNABLEat java.lang.Thread.yield(Native Method)at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)at java.lang.Thread.run(Thread.java:745)

中间那个lmaxYieldingWaitStrategy服务是个什么玩意?在脑海里面检索了五分钟也没想到到底是个什么服务。然后百度了一下,百度给出了下面这个结论

com.lmax.disruptor是个高效的的消息队列,YieldingWaitStrategy是它的三种策略之一。哪三种策略?如下所述

com.lmax.disruptor.BlockingWaitStrategy
最低效的策略,但其对CPU的消耗最小,并且在各种部署环境中能提供更加一致的性能表现;

com.lmax.disruptor.SleepingWaitStrategy
性能表现和com.lmax.disruptor.BlockingWaitStrategy差不多,对CPU的消耗也类似,但其对生产者线程的影响最小,适合用于异步日志类似的场景;

com.lmax.disruptor.YieldingWaitStrategy
性能最好,适合用于低延迟的系统;在要求极高性能且事件处理线程数小于CPU逻辑核心树的场景中,推荐使用此策略;

but,这个消息中间件完全脱离了产品需求,从测试到产品没人知道有这么一回事。。。

由此可以判断出,开发应该是偷偷加了一个消息中间件,并且选用了第三种看起来性能最好的策略。但是忽略了一点,这种策略对cpu的压榨极高,尤其是cpu性能不好的时候,几乎能把cpu榨的一滴不剩。官网描述如下

CPU性能问题分析定位_系统调用_06

中间那个thread.yield就可以解释我们之前在内核空间看到的sched_yield()函数调用。因为资源不够,所以调用这个函数去反复循环排队,导致死循环。
最后让开发先把代码注释掉了,自己本地慢慢调试策略吧。

结尾

这件事给了我们几个教训
1:直到上线前一分钟,都要对开发的一举一动保持高度警惕
2:开发同学不要自作聪明往代码里面加一些未经验证的小发明创造,然后美滋滋的等上线。上了就炸
3:性能测试人员要学会抽丝剥茧,保留证据,同时做好和开发死磕的准备