当前位置: 首页 > 后端技术 > Java

线上环境CPU占用率高问题排查说明

时间:2023-04-01 23:25:25 Java

线上环境CPU占用率高问题排查说明故障处理说明现象生产环境中,业务运行容器CPU占用率升高,业务事务执行速度慢,平台日志不打印异常信息。现场资料jvm线程cpu使用率(top-H)线程栈平台配置jdk版本信息出品jvm内存栈分析思路根据现场同事的描述,问题出在rpc服务调用(abs调用afa的rpc服务通过tci),同时为rpc服务配置流量控制功能。对于启用了流量控制的接入服务,当请求进来时,会检查当前流量的大小。这一步会调用concurrentLinkedQueue的size()方法。根据线程cpu使用率和当前线程栈,当发现cpu使用率异常时,cpu主要消耗concurrentLinkedQueue的size()和remove()方法,对应的ConcuureLinkedQueue(jdk为8u40)源码代码行分别为450行和490行(见下图)这两个函数需要遍历队列中的元素,时间复杂度为O(n)。如果队列中的元素达到一定数量级,就会占用比较高的cpu。根据目前掌握的数据,可以推断当前队列的深度可能比较大。根据当时生产环境CPU高时dump出的内存镜像,发现ConcurrenLinkedQueue中有nullitem的节点数高达695万左右。这一步也证实了第二种猜测。结合jdk官网提供的bug列表,发现jdk8u40版本存在ConcurrentLinkedQueuebug:在某些情况下,通过remove()移除对象后,仍然会有item值的节点在队列中为空。也就是说,在队列中存储对象时,会将对象封装为一个Node,放入队列中。Node的项指向对象。该值设置为null,但该节点并未从队列中移除,导致队列大小没有变化。随着对象的添加,队列的大小会越来越大。jdkbug详情请看:https://bugs.java.com/bugdata...从现场线程栈,发现调用了afa平台配置的定时任务(日志清理)每天凌晨1:00,又会一直把事务线程挂掉,导致事务线程一直被占用得不到释放,可用的业务线程每天都在减少,导致线程泄漏。所有的日志清理定时任务都阻塞在readBytes操作中,一共找到了23个条目。根据开始时间和转储线程栈的时间,分析出每天对应一个表项。通过分析jdk源码,发现在另一种情况下,ConcurrentLinkedQueue通过remove()移除对象时,也会导致node不会真正从队列中移除的问题。首先,ConcurrentLinkedQueue从对象中移除节点只有两个地方,一个是调用remove()方法,另一个是调用first()方法。执行remove()时,通过cas操作进行队列移除。在高并发的情况下,删除相邻的两个元素时,可能会有一个item为null的节点没有被删除,导致泄漏。例如:队列中的A、B、C、D4个节点,当同时删除B和C两个节点时,在高并发的情况下,当两个并发线程走到第495行的位置时下图同时,此时线程1获得了A、B、C的引用,线程2获得了B、C、D三个节点的引用,此时继续往前.线程1删除后,A的netxt指向C。线程2删除后,B的next指向D,因为A是头节点,B的节点没有被引用(由于线程1的执行),所以最终队列会变成A->C->D,此时会导致C的item为null,但并没有真正从队列中移除。执行first()时,当head发现有update时,会遍历后面item不为Null的节点作为head节点。该步骤可以从队列中移除项目为空的节点。如果当前头节点没有被更新,队列中可能的后续节点的项为Null的将一直存储在队列中。7、结合第4点和第5点的分析,发现定时清除日志的线程并没有停止。因为只有当事务执行完成,线程被释放时,线程对象才会被从ConcurrentLinkedQueue队列中移除,其他执行时间更短的线程也被从队列中移除,所以才会被更新成为headqueue,但是因为一直挂着不释放,ConcurrentLinkedQueue的队头不会更新。因此,这会导致队列中item为null的节点越来越多,所以在执行size()方法和remove()方法遍历队列时,会造成严重的CPU占用。结论综上所述,由于jdk8u40的bug,ConcurrentLinkedQueue的队列中存在大量null项的节点,恰好日志清理事务还没有退出,所以ConcurrentLinkedQueue的头节点不会被update,导致size()和remove()遍历速度慢,cpu消耗大。复现一个与生产环境相同afa配置、相同jdk版本的测试环境,准备一个长时间挂在业务线程上的事务,几个短时间耗时的事务。平台启动后,首先启动挂在线程上的事务,然后通过loadrunner测试计划继续对短耗时事务进行压力测试。在足够的压力和时间下,可以重现CPU高,业务事务吞吐量低的情况。验证itme为null的节点泄漏条件比较苛刻。在高并发下,每秒只有一两笔交易可能会出现节点泄漏。因此,要完全重现当时的场景,还需要较长时间的压力测试。但是,还有另一种方法。压力测试时每隔十、二十分钟打印一次内存镜像,取出内存镜像进行分析,通过Eclipse的Memoryanalyzetool工具的oql语言搜索itme。比较节点数,可以找出item为null的节点数的增长率。下图是压力测试三天后dump出的内存镜像。发现在压力测试前后的两张图片中,nullitem的节点数量增加了约268万个。压测前和压测后同时,RpcServer-IoWorker-30线程的CPU使用率稳步上升到31.8%。观察当时的线程栈,发现线程正在执行ConcurrentLinkedQueue的size()方法,与生产环境不同。是一致的。解决方法是升级jdk版本(主要)优化事务,保证定时任务日志清理事务正常停止。(主要)优化平台。在流控校验逻辑上,放弃了ConcurrentLinkedQueue的size()方法,改用BlockingQueue或者其他并发队列来实现。(一个小问题,只有事务线程长时间没有释放才会出现)rpc访问服务的流控功能下线;(适用于紧急情况)修复后压测验证jdk版本升级到8u152,优化日志清理事务,按原压测方案进行压测。压测前的内存栈如下:压测5小时后