ConcurrentLinkedQueue 引发的现网问题

问题排查

​ 在公司某次重大活动的保障中,某个组件突然收到了CPU过高的预警(超过80%),收到预警以后,立刻查看监控,发现现网一共10个节点,其中3个节点cpu超过80%,剩余节点cpu正常(cpu不超过60%),并且问题节点的cpu中断数,突然陡增。‘

疑点一:在同样级别的请求量下,只有3台服务器

疑点二:CPU中断数,在某个时间点突然陡增,但上下文切换没有明显变化 (如果上下文切换数,和中断数两个同时上升,很有可能是GC的原因导致的)

在这里插入图片描述

​ 这边首先怀疑是不是6个服务节点,收到的请求量,不一致。但是,在查了日志后,发现各节点的请求量,基本上是一致的,甚至正常节点的请求量要略高于问题节点(分析,由于cpu高了,导致处理能力下降了)。

在这里插入图片描述

第一个怀疑的是GC问题,通过 jstat 命令查看:

jstat -gcutil [PID] 2000

结果发现 GC 是正常的,full gc次数为0 , yong gc 的频率也不是很高。

继续分析问题,先到问题节点上查看占用cpu比较高的线程:

top -Hp [PID]

这时候发现新的疑点

疑点三:占用cpu较高的线程不止一个,并且是轮流变化的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7kYCjHB-1624524310333)(https://raw.githubusercontent.com/WengyXu/oss/master/uPic/2021-06-24/image-20210624131636075.png)]、

​ 不管怎么样,有了占用cpu较高的线程,就可以结合栈快照进行分析,但这里碰到一个问题,就是top5的线程,一直在变化,通过jstack打出的快照,由于是手动打得,可能要晚于top上显示线程运行的时刻。

​ 这时候,就用到了排查神器 Arthas 启动 arthas,并attach 上服务进程以后, 输入命令:

thread -n 3 -i 5000

查看到一个堆栈信息非常频繁的出现:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rh6JJIgr-1624524310334)(https://raw.githubusercontent.com/WengyXu/oss/master/uPic/2021-06-24/image-20210624134350806.png)]

终于找到方向了,继续分析,这个堆栈信息是发生在 Spring MVC 对请求进行拦截的时候。起初怀疑是filter中的代码,导致的,但对比了10台服务器的代码,md5值是一摸一样的。如果是这段代码的问题,那在同样级别的请求量下,应该是都会有问题。

经过网上查证,找到了相同的问题:

https://github.com/spring-projects/spring-framework/issues/24886
https://github.com/spring-projects/spring-framework/issues/24671
https://github.com/spring-projects/spring-framework/commit/39536acf9f28ddcae59bf89ae2c31b8e6a615494

原来 Spring MVC 在处理请求时,会缓存请求头中的 Content-Type , Spring 构造了一个 LRU 缓冲结构(内部采用ConcurrentLinkedQueue 实现),按作者正常的设计,该队列,最多只能存储64个元素,超过64个,就会删除最不常用的,但由于jdk1.8低版本的bug, 导致在高并发的情况下,会产生内存泄露的问题,导致链表变长。而remove由于需要遍历链表,所以当链表长度达到一定程度以后,遍历就会非常耗费CPU

主要逻辑代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uos4LR9y-1624524310335)(https://raw.githubusercontent.com/WengyXu/oss/master/uPic/2021-06-24/image-20210624152230916.png)]

这里就解释了疑问一为什么只在3台服务器上,出现问题,由于该bug只在频繁的调用add和remove方法时,才会导致链表变长(实际没有删掉),所以说,这里存在一个阈值,只有当已缓存的数量达到32个时候,才会触发。

这里要说一个后面的反思,由于当时是cpu过高,并且没有gc问题, 所以在保留现场的时候,只做了栈快照,没有留堆快照,这样对于后续故障定位,和现场复原就失去了第一手的证据。如果有堆快照的话,就可以对象进一步佐证(问题节点的content-type,超过了32个)

参考:

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8137185
https://blog.51cto.com/u_15127641/2874815

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hro82gdX-1624524310336)(https://raw.githubusercontent.com/WengyXu/oss/master/uPic/2021-06-24/image-20210624144754238.png)]

测试环境复现

  • 在测试环境,我们使用jemter,进行压测,并且在请求中,随机添加40个不同的content-type,成功复现问题,压了30分钟以后,cpu飙高到95%,后续,即使减少了并发量,cpu也很难降下来

image-20210624155830566

  • 在将jdk 升级到升级到1.8.0_201 后,问题解决,无法复现

疑问点解答

疑问一

为什么只在3台服务器上,出现问题?

由于该bug只在频繁的调用add和remove方法时,才会导致链表变长(实际没有删掉),所以说,这里存在一个阈值,只有当已缓存的数量达到32个时候,才会触发。

疑问二

CPU中断数,在某个时间点突然陡增,但上下文切换没有明显变化

问题主要是由于,多个线程一直在遍历链表,查找到元素remove, 一直占用着cpu时间,本身上下文切换,不多,但由于长期占用cpu的时间,当别的进程需要cpu资源时,只能中断cpu, 所以现象就是,CPU中断数,在某个时间点突然陡增,但上下文切换没有明显变化。

疑问三

占用cpu较高的线程不止一个,并且是轮流变化的

根据代码可知,需要LRU Cache中缓存的元素达到,32个以上,才会触发add() 和 remove 方法,来移动元素位置,将操作频繁的元素移动到头部,淘汰在尾部的预算。

基于堆栈的思考

在分析堆栈的时候,发现一个问题,代码中读取和更新逻辑,采用的是读锁,按道理是可以并发执行的,但从栈快照中却发现,也有很多线程wait 在435行
在这里插入图片描述在这里插入图片描述

ReentrantReadWriteLock 底层使用的是AQS, 于是我抱疑问去查了一下资料

ReentrantReadWriteLock.ReadLock 的实现机制 :

获取共享锁的时候 先去同步队列看看 有没有阻塞的写锁 如果有就阻塞自己 把自己也加入到同步队列

当第一个获取共享锁的线程不释放共享锁时 后续的 写锁和 排在写锁后面的线程 都会被阻塞

写锁阻塞是因为 有读锁获取到了锁

后续的读锁阻塞是 因为前面有了写锁在等待锁

在这里插入图片描述

启示

  • 在使用ConcurrentLinkedQueue队列时,如果长度非常大的话,需要谨慎使用remove,size等需要遍历元素的方法,还有就是,在采用ConcurrentLinkedQueue的迭代器时,要注意他是弱一致性的。
  • 在使用任何一个新的框架或者api时,一定要仔细阅读,官方的文档,和相关的issue bug, 避免睬坑。
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值