利用 Arthas 精准定位 Java 应用 CPU 负载过高问题

简介: 云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 最近我们线上有个应用服务器有点上头,CPU总能跑到99%,我寻思着它流量也不大啊,为啥能把自己整这么累?于是我登上这台服务器,看看它到底在干啥! 以前碰到类似问题,可能会考虑使用 top -Hp 加 jstack 命令去排查,虽然能大致定位到问题范围,但有效信息还是太少了,多数时候还是要靠猜。

云栖号资讯:【点击查看更多行业资讯
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!


最近我们线上有个应用服务器有点上头,CPU总能跑到99%,我寻思着它流量也不大啊,为啥能把自己整这么累?于是我登上这台服务器,看看它到底在干啥!

以前碰到类似问题,可能会考虑使用 top -Hp 加 jstack 命令去排查,虽然能大致定位到问题范围,但有效信息还是太少了,多数时候还是要靠猜。今天向大家推荐一款更高效更精准的工具:Arthas!Arthas 是 Alibaba 开源的 Java 诊断工具,能够帮助我们快速定位线上问题。基本的安装使用可以参考官方文档:https://alibaba.github.io/arthas

这次我们利用它来排查 CPU 负载高的问题。CPU 负载过高一般是某个或某几个线程有问题,所以我们尝试使用第一个命令:thread,这个命令会显示所有线程的信息,并且把 CPU 使用率高的线程排在前面。


[arthas@384]$ thread Threads Total: 112, NEW: 0, RUNNABLE: 26, BLOCKED: 0, WAITING: 31, TIMED_WAITING: 55, TERMINATED: 0
ID NAME STATE %CPU TIME
108 h..ec-0 RUNNABLE 51 4011:48
100 h..ec-2 RUNNABLE 48 4011:51
...

为了方便阅读,删掉了一些不重要的信息

可以看到,CPU 资源几乎被前两个线程占满,并且已经执行了 4000 多分钟,我们服务器也就启动了两天,可见这两天它们是一刻也没闲着!那它们究竟在干什么呢?我们可以使用命令:thread id,查看线程堆栈。


[arthas@384]$ thread 108 "http-nio-7001-exec-10" Id=108 cpuUsage=51% RUNNABLE
at c.g.c.c.HashBiMap.seekByKey(HashBiMap.java)
at c.g.c.c.HashBiMap.put(HashBiMap.java:270)
at c.g.c.c.HashBiMap.forcePut(HashBiMap.java:263)
at c.y.r.j.o.OaInfoManager.syncUserCache(OaInfoManager.java:159)

也可以使用 thread -n 3 命令打印出 CPU 占比最高的前三个线程,这差不多是 > top -Hp> & > printf> & > jstack> 三令合一的效果了>

可以看到,这个线程一直在执行 HashBiMap.seekByKey 方法(可以重复执行几次 thread id 确保该线程执行的方法没有时刻在变化),造成这个问题一般有两个原因:

  • seekByKey 方法被循环调用
  • seekByKey 内部有死循环

先看一下是不是第一种,我们使用 tt 命令监听一下这个方法的调用情况:

tt -t com.google.common.collect.HashBiMap seekByKey -n 100

注意:在线上执行这个命令的时候,一定要记得加上 -n 参数,否则线上巨大的流量可能会瞬间撑爆你的 JVM 内存执行结果显示,seekByKey 方法并没有被一直调用,那大概率是 seekByKey 方法内部有死循环。看下这个方法内部的逻辑,我们可以使用 jad com.google.common.collect.HashBiMap seekByKey 命令反编译这个方法,这样做的好处是显得比较高端,不过我还是打算直接找到源码,说不定还有注释。源码如下:


private BiEntry<K, V> seekByKey(@Nullable Object key, int keyHash) { for (BiEntry<K, V> entry = hashTableKToV[keyHash & mask];
entry != null;
entry = entry.nextInKToVBucket) {
if (keyHash == entry.keyHash && Objects.equal(key, entry.key)) {
return entry;
}
}
return null;
}

然后并没有注释,还好这个方法逻辑比较简单,也很容易看懂。

1.通过 hash 找到 bucket,每个 bucket 是一个链表;

2.遍历链表,找到这个 key 对应的 entry。这里要留意下 entry 的下一个节点是 nextInKToVBucket,后文中会用到。

发生了死循环,我们猜想可能是因为这个链表有环路。那么有没有办法验证这个猜想呢?答案是有!那么如何验证呢?首先我们要获得这个 HashBiMap 对象,以便于查询对象里的数据。获得这个对象有很多办法,比如监听这个对象的某个方法,然后主动触发这个方法。这里向大家介绍一种更为通用的方法,这个方法在 SpringMVC 程序里非常好用。因为我们是 SpringMVC 应用,所有请求都会被 RequestMappingHandlerAdapter 拦截,我们通过 tt 命令,监听 invokeHandlerMethod 的执行,然后在页面随便点点,就会得到以下内容:

[arthas@384]$ tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 10 Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 622 ms.
INDEX COST(ms) OBJECT CLASS METHOD
1000 481.203383 0x481eb705 RequestMappingHandlerAdapter invokeHandlerMethod
1001 3.432024 0x481eb705 RequestMappingHandlerAdapter invokeHandlerMethod
...

tt 命令会记录方法调用时的所有入参和返回值、抛出的异常、对象本身等数据。INDEX 字段代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作。

我们可以通过 -i 参数后边跟着对应的 INDEX 编号查看这条记录的详细信息。再通过 -w 参数,指定一个 OGNL 表达式,查找相关对象:

 
[arthas@384]$ tt -i 1000 -w 'target.getApplicationContext()' @AnnotationConfigServletWebServerApplicationContext[
reader=@AnnotatedBeanDefinitionReader[org.springframework.context.annotation.AnnotatedBeanDefinitionReader@50294e97],
scanner=@ClassPathBeanDefinitionScanner[org.springframework.context.annotation.ClassPathBeanDefinitionScanner@5eeeaae2],
annotatedClasses=@LinkedHashSet[isEmpty=true;size=0],
basePackages=null,

Arthas 会把当前执行的对象放到 target 变量中,通过 target.getApplicationContext() 就得到了 SpringContext 对象,然后,我们就可以为所欲为了!

接下来我们需要用 OGNL 写一个函数,来实现链表的环路检测,在 OGNL 里写一段环路检测代码里是不太容易的,这里我用了一个取巧的伪实现。

1

因为我知道一个 bucket 不太可能有 50 个以上的节点,所以就通过遍历次数是否大于 50 来判断是否有环路。

完整的命令:


tt -i 1000 -w 'target.getApplicationContext().getBean("oaInfoManager").userCache.entrySet().{delegate}.{^ #loopCnt = 0,#foundCycle = :[ #this == null ? false : #loopCnt > 50 ? true : (#loopCnt = #loopCnt + 1, #foundCycle(#this.nextInKToVBucket))], #foundCycle(#this)}.get(0)' -x 2

命令解析:

获取 HashBiMap 对象:target.getApplicationContext().getBean("oaInfoManager").userCache
遍历所有 entry,取出第一个有环路的 entry
-x 参数指定展开层级,我们需要将这个参数设置的比环要大一些,才能确保可以发现环路。这里我们的环路非常小,所以设置成了 2

执行结果如下:


@BiEntry[ key=@String[张三],
value=@Long[1111],
nextInKToVBucket=@BiEntry[
key=@String[李四],
value=@Long[2222],
nextInKToVBucket=@BiEntry[张三=1111]
]
]

可以看到是有 张三->李四->张三 这样一个环路。至此,造成死循环的原因确定了下来。结合两个线程几乎同时启动,又同时在执行 HashBiMap.forcePut 方法,容易想到是因为并发导致了数据的不一致,这一点也可以验证,不过由于篇幅有限,这里就不再赘述。找到了问题,就成功了 99%,解决这个问题的方法非常简单,就是对 syncUserCache 方法加一个 synchronized 关键字!

结语

这次遇到的问题并不复杂,用 jstack 命令也可以解决的了。但我们希望通过这样一个案例,向大家展示 Arthas 一些强大的功能,帮助大家打开思路,未来在遇到更复杂场景时,可以多一些趁手的工具!

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/live

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-06-11
本文作者:张云翔
本文来自:“dockone”,了解相关信息可以关注“dockone”

相关文章
|
1天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
13 0
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
6天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
6天前
|
设计模式 算法 Java
Java中的设计模式及其应用
【4月更文挑战第18天】本文介绍了Java设计模式的重要性及分类,包括创建型、结构型和行为型模式。创建型模式如单例、工厂方法用于对象创建;结构型模式如适配器、组合关注对象组合;行为型模式如策略、观察者关注对象交互。文中还举例说明了单例模式在配置管理器中的应用,工厂方法在图形编辑器中的使用,以及策略模式在电商折扣计算中的实践。设计模式能提升代码可读性、可维护性和可扩展性,是Java开发者的必备知识。
|
SQL 存储 Java
Java 应用与数据库的关系| 学习笔记
快速学习 Java 应用与数据库的关系。
169 0
Java 应用与数据库的关系| 学习笔记
|
SQL 存储 Java
Java 应用与数据库的关系| 学习笔记
快速学习 Java 应用与数据库的关系。
165 0
Java 应用与数据库的关系| 学习笔记
|
SQL 存储 关系型数据库
Java应用与数据库的关系|学习笔记
快速学习Java应用与数据库的关系
Java应用与数据库的关系|学习笔记
|
5天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
6天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
6天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。