线上诊断神器──Arthas

Thread

Thread 命令可以查看当前线程信息,查看线程的堆栈.

定位 CPU 问题:thread

直接使用thread命令将默认按照 CPU 增量时间降序排列,只显示第一页数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ thread
Threads Total: 33, NEW: 0, RUNNABLE: 9, BLOCKED: 0, WAITING: 3, TIMED_WAITING: 4, TERMINATED: 0, Internal threads: 17
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPT DAEMON
-1 C2 CompilerThread0 - -1 - 5.06 0.010 0:0.973 false true
-1 C1 CompilerThread0 - -1 - 0.95 0.001 0:0.603 false true
23 arthas-command-execute system 5 RUNNABLE 0.17 0.000 0:0.226 false true
-1 VM Periodic Task Thread - -1 - 0.05 0.000 0:0.094 false true
-1 Sweeper thread - -1 - 0.04 0.000 0:0.011 false true
-1 G1 Young RemSet Sampling - -1 - 0.02 0.000 0:0.025 false true
12 Attach Listener system 9 RUNNABLE 0.0 0.000 0:0.022 false true
11 Common-Cleaner InnocuousThrea 8 TIMED_WAI 0.0 0.000 0:0.000 false true
3 Finalizer system 8 WAITING 0.0 0.000 0:0.000 false true
2 Reference Handler system 10 RUNNABLE 0.0 0.000 0:0.000 false true
4 Signal Dispatcher system 9 RUNNABLE 0.0 0.000 0:0.000 false true
15 arthas-NettyHttpTelnetBootstra system 5 RUNNABLE 0.0 0.000 0:0.029 false true
22 arthas-NettyHttpTelnetBootstra system 5 RUNNABLE 0.0 0.000 0:0.196 false true
24 arthas-NettyHttpTelnetBootstra system 5 RUNNABLE 0.0 0.000 0:0.038 false true
16 arthas-NettyWebsocketTtyBootst system 5 RUNNABLE 0.0 0.000 0:0.001 false true
17 arthas-NettyWebsocketTtyBootst system 5 RUNNABLE 0.0 0.000 0:0.001 false true

展示当前最忙的前 N 个线程:thread -n

展示当前最忙的前 N 个线程并打印堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ thread -n 3
"C1 CompilerThread0" [Internal] cpuUsage=1.63% deltaTime=3ms time=1170ms


"arthas-command-execute" Id=23 cpuUsage=0.11% deltaTime=0ms time=401ms RUNNABLE
at java.management@11.0.7/sun.management.ThreadImpl.dumpThreads0(Native Method)
at java.management@11.0.7/sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:466)
at com.taobao.arthas.core.command.monitor200.ThreadCommand.processTopBusyThreads(ThreadCommand.java:199)
at com.taobao.arthas.core.command.monitor200.ThreadCommand.process(ThreadCommand.java:122)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.process(AnnotatedCommandImpl.java:82)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.access$100(AnnotatedCommandImpl.java:18)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:111)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:108)
at com.taobao.arthas.core.shell.system.impl.ProcessImpl$CommandProcessTask.run(ProcessImpl.java:385)
at java.base@11.0.7/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base@11.0.7/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base@11.0.7/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base@11.0.7/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base@11.0.7/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base@11.0.7/java.lang.Thread.run(Thread.java:834)


"VM Periodic Task Thread" [Internal] cpuUsage=0.07% deltaTime=0ms time=584ms
  • 没有线程 ID,包含[Internal]表示为 JVM 内部线程,参考dashboard命令的介绍。
  • cpuUsage为采样间隔时间内线程的 CPU 使用率,与dashboard命令的数据一致。
  • deltaTime为采样间隔时间内线程的增量 CPU 时间,小于 1ms 时被取整显示为 0ms。
  • time 线程运行总 CPU 时间。

注意:线程栈为第二采样结束时获取,不能表明采样间隔时间内该线程都是在处理相同的任务。建议间隔时间不要太长,可能间隔时间越大越不准确。 可以根据具体情况尝试指定不同的间隔时间,观察输出结果。

thread –all, 显示所有匹配的线程

显示所有匹配线程信息,有时需要获取全部 JVM 的线程数据进行分析。

显示指定线程的运行堆栈:thread id

1
2
3
4
5
6
7
8
9
$ thread 1
"main" Id=1 WAITING on java.util.concurrent.CountDownLatch$Sync@29fafb28
at sun.misc.Unsafe.park(Native Method)
- waiting on java.util.concurrent.CountDownLatch$Sync@29fafb28
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:997)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1304)
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:231)

线程死锁:thread -b

有时候我们发现应用卡住了, 通常是由于某个线程拿住了某个锁, 并且其他线程都在等待这把锁造成的。 为了排查这类问题,可以通过thread -b命令 一键找出罪魁祸首。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ thread -b
"http-bio-8080-exec-4" Id=27 TIMED_WAITING
at java.lang.Thread.sleep(Native Method)
at test.arthas.TestThreadBlocking.doGet(TestThreadBlocking.java:22)
- locked java.lang.Object@725be470 <---- but blocks 4 other threads!
at javax.servlet.http.HttpServlet.service(HttpServlet.java:624)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at test.filter.TestDurexFilter.doFilter(TestDurexFilter.java:46)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
at com.taobao.tomcat.valves.ContextLoadFilterValve$FilterChainAdapter.doFilter(ContextLoadFilterValve.java:191)
at com.taobao.eagleeye.EagleEyeFilter.doFilter(EagleEyeFilter.java:81)
at com.taobao.tomcat.valves.ContextLoadFilterValve.invoke(ContextLoadFilterValve.java:150)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:429)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1085)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:625)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318)
- locked org.apache.tomcat.util.net.SocketWrapper@7127ee12
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

Number of locked synchronizers = 1
- java.util.concurrent.ThreadPoolExecutor$Worker@31a6493e

注意, 目前只支持找出 synchronized 关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持。

其他

指定采样时间间隔:thread -i,

  • thread -i 1000 : 统计最近 1000ms 内的线程 CPU 时间。
  • thread -n 3 -i 1000 : 列出 1000ms 内最忙的 3 个线程栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ thread -n 3 -i 1000
"as-command-execute-daemon" Id=4759 cpuUsage=23% RUNNABLE
at sun.management.ThreadImpl.dumpThreads0(Native Method)
at sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:440)
at com.taobao.arthas.core.command.monitor200.ThreadCommand.processTopBusyThreads(ThreadCommand.java:133)
at com.taobao.arthas.core.command.monitor200.ThreadCommand.process(ThreadCommand.java:79)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.process(AnnotatedCommandImpl.java:96)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl.access$100(AnnotatedCommandImpl.java:27)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:125)
at com.taobao.arthas.core.shell.command.impl.AnnotatedCommandImpl$ProcessHandler.handle(AnnotatedCommandImpl.java:122)
at com.taobao.arthas.core.shell.system.impl.ProcessImpl$CommandProcessTask.run(ProcessImpl.java:332)
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:756)

Number of locked synchronizers = 1
- java.util.concurrent.ThreadPoolExecutor$Worker@546aeec1
...

查看指定状态的线程:thread –state

1
2
3
4
5
6
[arthas@28114]$ thread --state WAITING
Threads Total: 16, NEW: 0, RUNNABLE: 9, BLOCKED: 0, WAITING: 3, TIMED_WAITING: 4, TERMINATED: 0
ID NAME GROUP PRIORITY STATE %CPU DELTA_TIME TIME INTERRUPTE DAEMON
3 Finalizer system 8 WAITING 0.0 0.000 0:0.000 false true
20 arthas-UserStat system 9 WAITING 0.0 0.000 0:0.001 false true
14 arthas-timer system 9 WAITING 0.0 0.000 0:0.000 false true

生成火焰图

通过命令开启采样,观察一段事件后停止,输出一个 html 火焰图文件打开观察即可。

1
profiler start

1
profiler status
1
profiler stop

火焰图的效果大致如下图:

火焰图

图中的每个色块代表的是一个线程栈帧,色块中是该栈帧加载执行的函数以及一些采样信息。Y 轴代表调用栈的深度,调用从底部开始向上进行,顶部是正在执行的函数,下面是它的父函数,调用栈越深,火焰就越高。X 轴代表抽样数,如果一个函数在 X 轴占据的宽度越大,表示它被抽取到的次数越多,也就意味着它占用的 CPU 时间越长。

需要注意的是,图中的颜色并不重要,只用于区分不同的函数,同时 X 轴的从左到右的顺序也不重要,默认按照字典顺序排序。

我们可以简单举个例子来理解。一般在使用性能分析工具时,我们会选取一个合适的时机启动分析采样,然后等待一段时间后停止采样。假设采样时间为 20 秒,每秒采样 100 次,那么共采样 2000 次,在这 2000 次中,共有 1800 次执行的是 main 方法,其余 200 次执行的是 run 方法,同时 main 方法又调用了很多其他方法,run 方法也是,这就形成了一个从下往上的调用链,在这些调用链中,又同时会收集到每个方法采样的次数,最终形成一张火焰图。

火焰图是可以互动的,点击某个色块,该色块会被放大并占据所有的宽度,这样可以方便地查看某个函数内部的调用链信息。一般观察火焰图,就是看顶层函数占用的宽度,如果有些函数占用的宽度很大,形成了“平顶”,那么就代表该函数可能存在性能问题。当然,有时候直接观察平顶并不能快速定位问题,因为很多时候顶部的函数都是一些底层的库函数,这时候就应该先观察业务代码在火焰图中的宽度,然后再往上观察顶部的库函数来缩小范围。

从上面的火焰图发现TestController类的wl方法占据的 x 轴宽度较长,这是因为我们程序写了一个死循环到消耗了 CPU 。

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/wl")
@ApiOperationSupport(order = 1)
public Result<String> wl(String id) {
while (true){
System.out.println(1);
}
}

trace

使用 trace 命令查看耗时

我们写几个不同循环次数的方法,分别代表方法耗时不同,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//示例代码,timeCost是个接口

public String timeCost(){

cost1();

cost2();

cost3();

.......

}

private void cost3() {

//进行200万次循环......

}

private void cost2() {

//进行10万次循环......

}

private void cost1() {

//进行10次循环......

}

部署之后我们使用 trace 来查找最耗时的方法,在此过程中需要知道包名类名,以及方法的名字。上述代码方法所在包名为 com.cctest.arthas_demo.controller,类名为 StressSceneController,所以我们需要输入如下 trace 命令:

1
2
3
trace

com.cctest.arthas_demo.controller.StressSceneController timeCost

输完命令后回车,然后 arthas 程序就进入了等待访问状态。这时候访问接口 /timeCost,我们就可以看到被测程序台在疯狂打印日志,等结束之后,arthas 命令窗口便输出了各方法耗时时间,如图 3 所示:

vmtool

查看spring所管理的所有的bean名称

vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBeanDefinitionNames()’

查看具体的某个bean及其属性

vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBean(“testController”)’ -x 3

调用 bean 的方法

vmtool –action getInstances –className org.springframework.context.ApplicationContext –express ‘instances[0].getBean(“testController”).ok(1)’

ognl

ognl 语法:

  • #开头变量 在ognl 里面是变量全局的,可以传递 ;
  • 字段类型要注意
    • Doubble -> 1D
    • Long - >1L
    • String ->""
  • Class对象-> @xxxClass@class 相当于一个静态的变量

调用 Spring Bean 及对应方法

步骤说明

主要分为三步:

  • sc 获取工具类的 classLoaderHash
  • ognl 指定工具类的 classLoaderHash 获取对应 Bean
  • 根据需要调用的 Bean 对象相关方法进行调用

实战运用

1
sc -d cn.lovike.function.osier.core.util.SpringBeanUtils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[arthas@14466]$ sc -d cn.lovike.function.osier.core.util.SpringBeanUtils
class-info cn.lovike.function.osier.core.util.SpringBeanUtils
code-source /Users/wk/Documents/Code/Github/Personal/osier/osier-core/target/classes/
name cn.lovike.function.osier.core.util.SpringBeanUtils
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name SpringBeanUtils
modifier public
annotation org.springframework.stereotype.Component
interfaces org.springframework.context.ApplicationContextAware
super-class +-java.lang.Object
class-loader +-sun.misc.Launcher$AppClassLoader@18b4aac2
+-sun.misc.Launcher$ExtClassLoader@79ca92b9
classLoaderHash 18b4aac2

调用 Redis 工具类查看某个 Redis Key 的类型

1
ognl -c 18b4aac2 -x 3 '@cn.lovike.function.osier.core.util.SpringBeanUtils@getBean("redisUtils").type("osier:sys:dict")'

返回结果:

ognl -c 2e5d6d97 -x 3 '@com.it.chenyuan.business.utils.SpringUtil@getBean(“MyRedisUtil”).get(“aper_duty_rate:hwzy:1868547696917106690”)’

1
@DataType[HASH]

调用 Redis 工具类查看某个 Redis Key 是否存在

1
ognl -c 18b4aac2 -x 3 '@cn.lovike.function.osier.core.util.SpringBeanUtils@getBean("redisUtils").exists("osier:sys:dict")'

返回结果:

1
@Boolean[true]

单独设置UserController的logger level

1
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'

排查HTTP请求返回401

请求接口没有权限的时候一般就返回401 Unauthorized。

401通常是被权限管理的Filter拦截了,那么到底是哪个Filter处理了这个请求,返回了401?

跟踪所有的Filter函数

开始trace:

1
trace javax.servlet.Filter *

可以在调用树的最深层,找到AdminFilterConfig$AdminFilter返回了401

1
2
3
+---[3.806273ms] javax.servlet.FilterChain:doFilter()
| `---[3.447472ms] com.example.demo.arthas.AdminFilterConfig$AdminFilter:doFilter()
| `---[0.17259ms] javax.servlet.http.HttpServletResponse:sendError()
0%