一次应用CPU飙高排查过程--HashMap多线程下引发的血案
案件背景
一个应用集群里,时不时会有几台机器出现cpu打满现象,开始没有引起重视,后来连续出现报警,开始着手对其中一台进行排查,现将破案记录如下。
cpu飙升这类案件,一般来说有几个对象嫌疑重大:
- 嫌犯A:内存泄漏,导致大量full GC
- 嫌犯B:宿主机cpu超卖
- 嫌犯C:代码存在死循环
锁定嫌犯
嫌犯A:内存泄漏?
从monitor上看到,这台机器cpu占用达到300%多,而GC一览并没有出现full GC,只是出现了一些常规的YGC。再观察堆内存使用情况,也属正常,先排出了oom的嫌疑。
ps:同样cpu问题,因为内存泄漏导致gc的一次案例:一次应用CPU飙高-GC频繁问题排查过程
嫌犯B:cpu超卖?
虚拟机和容器技术突飞猛进,一台宿主机上跑多个vm带来了很多便利,vm间大多时候都能和谐共处,但偶尔也会出现某个问题vm大量占用宿主机资源,导致其他vm受到影响,也是超卖问题
到底是不是超卖在搞鬼呢?登上机器top一把,一探究竟
top
这里看到Cpu(s)一栏,cpu占用主要来自us,而st(Steal Time)并不高,这说明cpu的消耗并非来自宿主机的超卖,而是应用自身的消耗。所以排出超卖的嫌疑。
锁定嫌犯C:死循环
排出了上面两位的嫌疑,看来只能继续深入应用内部,对犯案现场勘察,查明哪些线程在消耗cpu资源。
前面通过top命令拿到java应用的pid是2143,通过top -Hp pid 命令,查看进程内的线程情况:
top -Hp 2143
不看不知道,一看吓一跳,犯罪现场触目惊心!前几个线程都占用了大量cpu,并且占用cpu时间最长的一个线程(tid=32421),已经存活了5个多小时。
继续进行追查,这货到底在干啥?
printf "%x" 32421 -- 拿到十六进制
jstack pid | grep tid -- 查看线程情况
原来这个线程在HashMap.getEntry()这,线程状态显示是RUNNABLE,说明并没有出现死锁(Blocked),而是不停run了5个多小时,看来凶犯已经找到:死循环非他莫属了!
为了进一步确认,用类似方法一一盘查其他几个高cpu占用的线程,从招供来看都是类似的堆栈。同时,在psp上进行了一把dump,用Zprofiler分析了一把,除去一些正常的线程,还有不少共犯混迹其中。
作案手法
凶手已经找到,但它是如何作案的呢?也就是这个死循环是如何产生的?
HashMap的并发问题
上面的堆栈告诉我们,线程在HashMap.java:465行不停的run,从jdk7的源码(应用使用的版本)可以看到
原来问题出在e.next这个地方。
看过源码的同学都知道,jdk(6)7的HashMap是数组+链表的存储结构(jdk8优化加入了红黑树)。
为了在查询效率方面达到平衡,HashMap的size是动态变化的,size初始值是16(未指定情况下)。一般来说,Hash表这个容器当有数据要插入(put->addEntry)时,会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,这一过程称为resize。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // 动态扩容一倍 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
resize()源码如下:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
可见,在多线程同时调用put方法时,多个线程也会同时进入transfer(),也就到了并发问题的核心地带
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
这段代码会重新构建数组和链表,这单线程下安全,但多个线程同时去操作链表,会出现意想不到的结果,比如A线程操作到一半被挂起,B线程对A正在操作的链表进行了挪动,然后A获得cpu资源继续操作,原先的链表元素可能已经被挪到其他位置。
这会造成部分数据丢失,有一定几率出现更糟的情况:环链表
那么回到之前的getEntry方法,出现环链表的情况下,e.next会出现无限循环,无法跳出的情况。
总结下,多线程同时put时,有一定几率导致环链表产生,导致get方法进入无限循环,进而导致了cpu飙高。
结案
到这里,真相已经浮出水面:二方包的一个工具类(静态类),使用了一个static的HashMap进行了并发操作,导致了并发问题。
多线程环境中,使用ConcurrentHashMap代替HashMap。
后续找提供二方包的同学确认,的确是使用了一个问题版本,此问题已经在新版本中已经修复,升级到新版即可。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
超详细动手搭建一个Vuepress站点及开启PWA与自动部署
Vuepress介绍 官网:https://vuepress.vuejs.org/ 类似hexo一个极简的静态网站生成器,用来写技术文档不能在爽。当然搭建成博客也不成问题。 Vuepress特点 响应式,也可以自定义主题与hexo类似 内置markdown(还增加了一些扩展),并且可以在其使用Vue组件 Google Analytics 集成 PWA 自动生成Service Worker 快速上手 安装 初始化项目 yarn init -y # 或者 npm init -y 安装vuepress yarn add -D vuepress # 或者 npm install -D vuepress 全局安装vuepress yarn global add vuepress # 或者 npm install -g vuepress 新建一个docs文件夹 mkdir docs 设置下package.json { "scripts": { "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs" } } 写作 ya...
- 下一篇
Metaspace泄漏排查
原创文章,转载请注明 一、案件背景 近日,一个线上应用开始频繁报警:异常日志、接口rt超时、load高、tcp重传率高等等。现场监控如下: 从基础监控来看,cpu使用率不算特别异常,而load高说明等待cpu资源的线程队列长,配合rt上涨来看,推测是线程出现了堆积,而线程堆积一般有两种情况: 线程内部处理耗时变长:比如缓存未命中、被下游请求block、慢sql、循环逻辑耗时等。 JVM因GC、锁擦除等jvm操作原因触发stop the world,导致线程等待。 下面进一步定位问题。 二、问题定位 线程耗时变长? 因为应用重度依赖缓存,一起排查问题的同学发现tair成功率也有下降,于是找tair同学开始排查。 tair的同学表示集群正常,观察同机房其他应用读写tair也正常,推测问题还是出在应用自身。(其实应用自身负载高时,会引起tair超时和序列化失败,表象来看都像tair有问题,心疼下tair的同学。。。) 另外从db监控来看也没有慢sql出现,近期也没有逻辑改动较大的发布,暂时从其他方向看看。 STW 接下来比较醒目的就是GC监控了,监控如下: 可以看到问题期间GC次数和耗时明显...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS6,CentOS7官方镜像安装Oracle11G
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作