Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。
Arthas 支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
是否有一个全局视角来查看系统的运行状况?
有什么办法可以监控到JVM的实时运行状态?
Arthas采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。
本篇文章内容参考官方教程:https://alibaba.github.io/arthas/arthas-tutorials?language=cn&id=arthas-advanced,推荐在线实际操作一遍。
实际操作
一、启动程序
1、启动 springboot-demo
下载 demo-arthas-spring-boot.jar
,再用 java -jar
命令启动:
# 下载 wget https://github.com/hengyunabc/katacoda-scenarios/raw/master/demo-arthas-spring-boot.jar # 启动 java -jar demo-arthas-spring-boot.jar
demo-arthas-spring-boot
是一个很简单的spring boot应用,源代码:查看
启动之后,可以访问80端口:http://localhost/
2、启动 arthas-boot
在新的Terminal 2里,下载arthas-boot.jar
,再用java -jar
命令启动:
# 下载 wget https://alibaba.github.io/arthas/arthas-boot.jar # 启动 java -jar arthas-boot.jar --target-ip 0.0.0.0
arthas-boot
是Arthas的启动程序,它启动后,会列出所有的Java进程,用户可以选择需要诊断的目标进程。
选择 demo-arthas-spring-boot.jar
进程编号,再Enter/回车
:
Attach成功之后,会打印Arthas LOGO。
输入 help
可以获取到更多的帮助信息。
二、查看JVM信息
下面介绍Arthas里查看JVM信息的命令。
1、sysprop 系统配置
sysprop
可以打印所有的System Properties
信息。
也可以指定单个key:
sysprop java.version
也可以通过grep来过滤:
sysprop | grep user
可以设置新的value:
sysprop testKey testValue
2、sysenv 系统环境变量
sysenv
命令可以获取到环境变量。和 sysprop
命令类似。
3、jvm
jvm
命令会打印出JVM的各种详细信息。
4、dashboard 数据面板
dashboard
命令可以查看当前系统的实时数据面板。
输入 Q
或者 Ctrl+C
可以退出命令。
三、提示
为了更好使用Arthas,下面先介绍Arthas里的一些使用技巧。
1、help
Arthas里每一个命令都有详细的帮助信息。可以用-h来查看。帮助信息里有EXAMPLES和WIKI链接。
比如:sysprop -h
2、自动补全
Arthas支持丰富的自动补全功能,在使用有疑惑时,可以输入Tab来获取更多信息。
比如输入 sysprop java.
之后,再输入Tab
,会补全出对应的key
:
[arthas@67655]$ sysprop java. java.runtime.name java.protocol.handler.pkgs java.vm.version java.vm.vendor java.vendor.url java.vm.name java.vm.specification.name java.runtime.version java.awt.graphicsenv java.endorsed.dirs java.io.tmpdir java.vm.specification.vendor java.library.path java.specification.name java.class.version java.awt.printerjob java.specification.version java.class.path java.vm.specification.version java.home java.specification.vendor java.vm.info java.version java.ext.dirs java.vendor java.awt.headless java.vendor.url.bug
3、readline的快捷键支持
Arthas支持常见的命令行快捷键,比如Ctrl + A
跳转行首,Ctrl + E
跳转行尾。
更多的快捷键可以用 keymap
命令查看。
Shortcut Description Name ------------------------------------------------------------ "\C-a" Ctrl + a beginning-of-line "\C-e" Ctrl + e end-of-line "\C-f" Ctrl + f forward-word "\C-b" Ctrl + b backward-word "\e[D" Left arrow backward-char "\e[C" Right arrow forward-char "\e[A" Up arrow history-search-backward "\e[B" Down arrow history-search-forward "\C-h" Ctrl + h backward-delete-char "\C-?" Ctrl + ? backward-delete-char "\C-u" Ctrl + u undo "\C-d" Ctrl + d delete-char "\C-k" Ctrl + k kill-line "\C-i" Ctrl + i complete "\C-j" Ctrl + j accept-line "\C-m" Ctrl + m accept-line "\C-w" Ctrl + w backward-delete-word "\C-x\e[3~" "\C-x\e[3~" backward-kill-line "\e\C-?" "\e\C-?" backward-kill-word "\e[1~" "\e[1~" beginning-of-line "\e[4~" "\e[4~" end-of-line "\e[5~" "\e[5~" beginning-of-history "\e[6~" "\e[6~" end-of-history "\e[3~" "\e[3~" delete-char "\e[2~" "\e[2~" quoted-insert "\e[7~" "\e[7~" beginning-of-line "\e[8~" "\e[8~" end-of-line "\eOH" "\eOH" beginning-of-line "\eOF" "\eOF" end-of-line "\e[H" "\e[H" beginning-of-line "\e[F" "\e[F" end-of-line
4、历史命令的补全
如果想再执行之前的命令,可以在输入一半时,按 Up/↑
或者 Ddown/↓
,来匹配到之前的命令。
比如之前执行过sysprop java.version
,那么在输入sysprop ja
之后,可以输入Up/↑
,就会自动补全为sysprop java.version
。
如果想查看所有的历史命令,也可以通过 history
命令查看到。
5、pipeline
Arthas支持在 pipeline
之后,执行一些简单的命令,比如:
sysprop | grep java sysprop | wc -l
四、sc/sm 查看已加载的类
下面介绍Arthas里查找已加载类的命令。
1、sc 查找加载类
sc
命令可以查找到所有JVM已经加载到的类。
如果搜索的是接口,还会搜索所有的实现类。比如查看所有的 Filter
实现类:sc javax.servlet.Filter
[arthas@67655]$ sc javax.servlet.Filter com.example.demo.arthas.AdminFilterConfig$AdminFilterjavax.servlet.Filter org.apache.tomcat.websocket.server.WsFilter org.springframework.boot.web.filter.OrderedCharacterEncodingFilter org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter org.springframework.boot.web.filter.OrderedRequestContextFilter org.springframework.web.filter.CharacterEncodingFilter org.springframework.web.filter.GenericFilterBean org.springframework.web.filter.HiddenHttpMethodFilter org.springframework.web.filter.HttpPutFormContentFilter org.springframework.web.filter.OncePerRequestFilter org.springframework.web.filter.RequestContextFilter org.springframework.web.servlet.resource.ResourceUrlEncodingFilter Affect(row-cnt:14) cost in 89 ms.
通过 -d
参数,可以打印出类加载的具体信息,很方便查找类加载问题:sc -d javax.servlet.Filter
[arthas@67655]$ sc javax.servlet.Filter -d class-info com.example.demo.arthas.AdminFilterConfig$AdminFilter code-source file:/Users/tingfeng/MyLib/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/ name com.example.demo.arthas.AdminFilterConfig$AdminFilter isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass true isPrimitive false isSynthetic false simple-name AdminFilter modifier static annotation interfaces javax.servlet.Filter super-class +-java.lang.Object class-loader +-org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a +-sun.misc.Launcher$AppClassLoader@55f96302 +-sun.misc.Launcher$ExtClassLoader@f6f4d33 classLoaderHash 33c7353a ...省略...
sc
支持通配,比如搜索所有的StringUtils:sc *StringUtils
[arthas@67655]$ sc *StringUtils com.taobao.arthas.core.util.StringUtils freemarker.core._CoreStringUtils org.apache.tomcat.util.buf.StringUtils org.springframework.util.StringUtils Affect(row-cnt:4) cost in 78 ms.
2、sm 查找具体函数
sm
命令则是查找类的具体函数。比如:sm java.math.RoundingMode
[arthas@67655]$ sm java.math.RoundingMode java.math.RoundingMode <init>(Ljava/lang/String;II)V java.math.RoundingMode values()[Ljava/math/RoundingMode; java.math.RoundingMode valueOf(I)Ljava/math/RoundingMode; java.math.RoundingMode valueOf(Ljava/lang/String;)Ljava/math/RoundingMode; Affect(row-cnt:4) cost in 78 ms.
通过-d
参数可以打印函数的具体属性:sm -d java.math.RoundingMode
[arthas@67655]$ sm -d java.math.RoundingMode declaring-class java.math.RoundingMode constructor-name <init> modifier private annotation parameters java.lang.String int int exceptions classLoaderHash null declaring-class java.math.RoundingMode method-name values modifier public,static annotation parameters return java.math.RoundingMode[] exceptions classLoaderHash null declaring-class java.math.RoundingMode method-name valueOf modifier public,static annotation parameters int return java.math.RoundingMode exceptions classLoaderHash null declaring-class java.math.RoundingMode method-name valueOf modifier public,static annotation parameters java.lang.String return java.math.RoundingMode exceptions classLoaderHash null
也可以查找特定的函数,比如查找构造函数:sm -d java.math.RoundingMode <init>
[arthas@67655]$ sm -d java.math.RoundingMode <init> declaring-class java.math.RoundingMode constructor-name <init> modifier private annotation parameters java.lang.String int int exceptions classLoaderHash null Affect(row-cnt:1) cost in 30 ms.
3、Jad 反编译
可以通过 jad
命令来反编译代码:jad com.example.demo.arthas.user.UserController
通过 --source-only
参数可以只打印出在反编译的源代码:jad --source-only com.example.demo.arthas.user.UserController
五、Ognl
在Arthas里,有一个单独的 ognl
命令,可以动态执行代码。
1、调用static函数
ognl '@java.lang.System@out.println("hello ognl")'
可以检查 Terminal 1 里的进程输出,可以发现打印出了 hello ognl
。
2、获取静态类的静态字段
使用 sc
命令,获取类加载的 hashCode 值
[arthas@67655]$ sc -d *UserController | grep classLoaderHash classLoaderHash 33c7353a
获取 UserController
类里的 logger
字段
# 命令 ognl -c 33c7353a @com.example.demo.arthas.user.UserController@logger # 测试 [arthas@67655]$ ognl -c 33c7353a @com.example.demo.arthas.user.UserController@logger @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=null, effectiveLevelInt=@Integer[20000], parent=@Logger[Logger[com.example.demo.arthas.user]], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ]
还可以通过 -x
参数控制返回值的展开层数。比如:
# 命令 ognl -c HashCode -x 2 @com.example.demo.arthas.user.UserController@logger # 测试 [arthas@67655]$ ognl -c 33c7353a -x 2 @com.example.demo.arthas.user.UserController@logger @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=null, effectiveLevelInt=@Integer[20000], parent=@Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user], level=null, effectiveLevelInt=@Integer[20000], parent=@Logger[Logger[com.example.demo.arthas]], childrenList=@CopyOnWriteArrayList[isEmpty=false;size=1], aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ DEFAULT_PACKAGING_DATA=@Boolean[false], root=@Logger[Logger[ROOT]], size=@Integer[369], noAppenderWarning=@Integer[0], loggerContextListenerList=@ArrayList[isEmpty=false;size=1], loggerCache=@ConcurrentHashMap[isEmpty=false;size=369], loggerContextRemoteView=@LoggerContextVO[LoggerContextVO{name='default', propertyMap={}, birthTime=1574834677738}], turboFilterList=@TurboFilterList[isEmpty=true;size=0], packagingDataEnabled=@Boolean[false], maxCallerDataDepth=@Integer[8], resetCount=@Integer[2], frameworkPackages=@ArrayList[isEmpty=true;size=0], ], ]
执行多行表达式,赋值给临时变量,返回一个List
[arthas@37]$ ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1,#value2}' @ArrayList[ @String[/usr/lib/jvm/java-8-oracle/jre], @String[Java(TM) SE Runtime Environment], ]
在Arthas里ognl表达式是很重要的功能,在很多命令里都可以使用 ognl
表达式。
一些更复杂的用法,可以参考:
OGNL特殊用法请参考:https://github.com/alibaba/arthas/issues/71
OGNL表达式官方指南:https://commons.apache.org/proper/commons-ognl/language-guide.html
六、案例: 排查函数调用异常
目前,访问 http://localhost/user/0
,会返回500异常:curl http://localhost/user/0
{"timestamp":1550223186170,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}
但请求的具体参数,异常栈是什么呢?
1、查看 UserController 参数/异常
在 Arthas 里执行 watch
命令:
# 命令 watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
第一个参数是类名,支持通配
第二个参数是函数名,支持通配
访问 curl http://localhost/user/0
,watch
命令会打印调用的参数和异常。如果想把获取到的结果展开,可以用-x
参数:
# 命令 watch com.example.demo.arthas.user.UserController * '{params, throwExp}' -x 2
2、返回值表达式
在上面的例子里,第三个参数是返回值表达式,它实际上是一个 ognl
表达式,它支持一些内置对象:
变量名 | 变量解释 |
---|---|
loader | 本次调用类所在的 ClassLoader |
clazz | 本次调用类的 Class 引用 |
method | 本次调用方法反射引用 |
target | 本次调用类的实例 |
params | 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组 |
returnObj | 本次调用返回的对象。当且仅当 isReturn==true 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 void ,则值为 null |
throwExp | 本次调用抛出的异常。当且仅当 isThrow==true 成立时有效,表明方法调用是以抛出异常的方式结束。 |
isBefore | 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时 isBefore==true 成立 ,同时 isThrow==false 和 isReturn==false ,因为在方法刚开始时,还无法确定方法调用将会如何结束。 |
isThrow | 辅助判断标记,当前的方法调用以抛异常的形式结束。 |
isReturn | 辅助判断标记,当前的方法调用以正常返回的形式结束。 |
所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合 OGNL 脚本语法或者引入了不在表格中的变量,则退出命令的执行;用户可以根据当前的异常信息修正条件表达式或观察表达式
你可以利用这些内置对象来组成不同的表达式。比如返回一个数组:
# 命令 watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}' # 测试 [arthas@67655]$ watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}' Press Q or Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:2) cost in 182 ms. ts=2019-11-27 17:29:47; [cost=0.696744ms] result=@ArrayList[ @Integer[0], @UserController[com.example.demo.arthas.user.UserController@3372deb0], null, ]
更多参考: https://alibaba.github.io/arthas/advice-class.html
3、条件表达式
watch
命令支持在第 4
个参数里写条件表达式,比如:
# 命令 watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'
当访问
curl http://localhost/user/1
时,watch
命令没有输出当访问
curl http://localhost/user/101
时,watch
会打印出结果。
4、当异常时捕获
watch
命令支持-e选项,表示只捕获抛出异常时的请求:
# 命令 watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e
5、按照耗时进行过滤
watch
命令支持按请求耗时进行过滤,比如:
# 命令 watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'
七、案例: 热更新代码
下面介绍通过 jad/mc/redefine
命令实现动态更新代码的功能。
目前,访问 http://localhost/user/0
,会返回500异常,下面通过热更新代码,修改这个逻辑。
1、jad 反编译
# 命令 jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
jad
反编译 UserController
的结果保存在 /tmp/UserController.java
文件里了。
然后用 vim /tmp/UserController.java
来编辑该文件。
比如当 user id 小于 1
时,也正常返回,不抛出异常:
@GetMapping(value={"/user/{id}"}) public User findUserById(@PathVariable Integer id) { logger.info("id: {}", (Object)id); if (id != null && id < 1) { return new User(id, "name" + id); // throw new IllegalArgumentException("id < 1"); } return new User(id.intValue(), "name" + id); }
2、sc 查找加载
# 命令 sc -d *UserController | grep classLoaderHash # 测试 [arthas@67655]$ sc -d *UserController | grep classLoaderHash classLoaderHash 33c7353a
可以发现是 spring boot LaunchedURLClassLoader@33c7353a
加载的。
3、mc 编译
Memory Compiler/内存编译器:编译 .java
文件生成 .class
通过
-c
指定ClassLoader
通过
-d
指定输出目录
# 命令 mc -c 33c7353a /tmp/UserController.java -d /tmp # 测试 [arthas@67655]$ mc -c 33c7353a /tmp/UserController.java -d /tmp Memory compiler output: /tmp/com/example/demo/arthas/user/UserController.class Affect(row-cnt:1) cost in 346 ms
4、redefine 重新加载
再使用 redefine
命令重新加载新编译好的 UserController.class
# 命令 redefine /tmp/com/example/demo/arthas/user/UserController.class # 测试 [arthas@67655]$ redefine com/example/demo/arthas/user/UserController.class redefine success, size: 1
redefine
成功之后,再次访问 http://localhost/user/0
测试
➜ ~ curl http://localhost/user/0 {"id":0,"name":"name0"}
八、案例: 动态更新应用日志级别
在这个案例里,动态修改应用的 Logger Level
。
1、查找 UserController 的 ClassLoader
# 命令 sc -d *UserController | grep classLoaderHash # 测试 [arthas@67655]$ sc -d *UserController | grep classLoaderHash classLoaderHash 33c7353a
2、用 ognl 获取 logger
# 命令 ognl -c 33c7353a '@com.example.demo.arthas.user.UserController@logger' # 测试 [arthas@67655]$ ognl -c 33c7353a '@com.example.demo.arthas.user.UserController@logger' @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=null, effectiveLevelInt=@Integer[20000], parent=@Logger[Logger[com.example.demo.arthas.user]], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ]
可以知道 UserController@logger
实际使用的是 logback
。可以看到 level=null
,则说明实际最终的 level
是从 root logger
里来的。
3、单独设置 UserController 的 logger level
# 命令 ognl -c 33c7353a '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
再次获取 UserController@logger
,可以发现已经是 DEBUG
了:
[arthas@67655]$ ognl -c 33c7353a '@com.example.demo.arthas.user.UserController@logger' @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=@Level[DEBUG], effectiveLevelInt=@Integer[10000], parent=@Logger[Logger[com.example.demo.arthas.user]], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ]
4、修改 logback 全局 logger level
通过获取 root logger
,可以修改全局的 logger level
:
# 命令 ognl -c 33c7353a '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
九、案例: 排查 logger 冲突问题
在这个案例里,展示排查logger冲突的方法。
1、确认应用使用的 logger 系统
以 UserController
为例,它使用的是 slf4j
api,但实际使用到的 logger 系统是 logback
。
[arthas@67655]$ ognl -c 33c7353a '@com.example.demo.arthas.user.UserController@logger' @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=@Level[DEBUG], effectiveLevelInt=@Integer[10000], parent=@Logger[Logger[com.example.demo.arthas.user]], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ]
2、获取 logback 实际加载的配置文件
# 命令 ognl -c 33c7353a '#map1=@org.slf4j.LoggerFactory@getLogger("root").loggerContext.objectMap, #map1.get("CONFIGURATION_WATCH_LIST")' # 测试 [arthas@67655]$ ognl -c 33c7353a '#map1=@org.slf4j.LoggerFactory@getLogger("root").loggerContext.objectMap, #map1.get("CONFIGURATION_WATCH_LIST")' @ConfigurationWatchList[ mainURL=@URL[jar:file:/home/scrapbook/tutorial/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/logback-spring.xml], fileWatchList=@ArrayList[isEmpty=true;size=0], lastModifiedList=@ArrayList[isEmpty=true;size=0], ]
3、使用 classloader 命令查找可能存在的 logger 配置文件
# 命令 classloader -c 33c7353a -r logback-spring.xml # 测试 [arthas@67655]$ classloader -c 33c7353a -r logback-spring.xml jar:file:/Users/tingfeng/MyLib/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/logback-spring.xml Affect(row-cnt:1) cost in 28 ms.
-c
表示:ClassLoader的hashcode-r
表示:用ClassLoader去查找resource
通过 classloader
可以知道加载的配置的具体来源。
可以尝试加载容易冲突的文件:
classloader -c 33c7353a -r logback.xml classloader -c 33c7353a -r log4j.properties
十、案例: 获取 Spring Context
在这个案例里,展示获取 spring context
,再获取 bean
,然后调用函数。
tt命令教程:https://alibaba.github.io/arthas/tt.html
1、使用 tt 命令获取到 spring context
tt
即 TimeTunnel,它可以记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。
-h
表示:帮助文档-t
表示:打印记录每次执行情况-n 3
表示:指定需要记录的次数-i
表示:跟着对应的INDEX
编号查看到他的详细信息-w
表示:监控 ognl 表达式
# 命令 tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
访问:https://localhost/user/1
,可以看到 tt
命令捕获到了一个请求:
[arthas@67655]$ tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod Press Q or Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 50 ms. INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------- 1002 2019-11-27 20:22:50 3.900109 true false 0x32bf079f RequestMappingHandlerAdapter invokeHandlerMethod
2、使用 tt 命令从调用记录里获取到 spring context
# 命令 tt -i 1000 -w 'target.getApplicationContext()' [arthas@67655]$ tt -i 1000 -w 'target.getApplicationContext()' @AnnotationConfigEmbeddedWebApplicationContext[ reader=@AnnotatedBeanDefinitionReader[org.springframework.context.annotation.AnnotatedBeanDefinitionReader@245d3875], scanner=@ClassPathBeanDefinitionScanner[org.springframework.context.annotation.ClassPathBeanDefinitionScanner@2a9ac28e], annotatedClasses=null, basePackages=null, ] Affect(row-cnt:1) cost in 43 ms.
3、获取 spring bean,并调用函数
tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()'
结果是:
$ tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()' @String[Hello World] Affect(row-cnt:1) cost in 52 ms.
十一、案例: 排查HTTP请求返回 401
在这个案例里,展示排查 HTTP 401 问题的技巧。
# 访问 ➜ ~ curl http://localhost/admin {"timestamp":1574860536323,"status":401,"error":"Unauthorized","message":"admin filter error.","path":"/admin"}%
我们知道 401
通常是被权限管理的 Filter
拦截了,那么到底是哪个 Filter 处理了这个请求,返回了401?
1、跟踪所有的 Filter 函数
开始 trace
跟踪:
# 命令 trace javax.servlet.Filter *
访问:https://localhost/admin
,可以在调用树的最深层看到一段儿,找到 AdminFilterConfig$AdminFilter
返回了 401
:
+---[3.806273ms] javax.servlet.FilterChain:doFilter() | `---[3.447472ms] com.example.demo.arthas.AdminFilterConfig$AdminFilter:doFilter() | `---[0.17259ms] javax.servlet.http.HttpServletResponse:sendError()
2、通过 stack 获取调用栈
通过 trace
命令来获取信息,我们可以知道通过 stack
跟踪 HttpServletResponse:sendError()
,同样可以知道是哪个 Filter
返回了 401
访问:https://localhost/admin
,再试一次
# 命令 stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401' # 测试 [arthas@67655]$ stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401' Press Q or Ctrl+C to abort. Affect(class-cnt:2 , method-cnt:4) cost in 265 ms. ts=2019-11-27 21:22:13;thread_name=http-nio-80-exec-2;id=12;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@3915eb83 @org.apache.catalina.connector.ResponseFacade.sendError() at com.example.demo.arthas.AdminFilterConfig$AdminFilter.doFilter(AdminFilterConfig.java:38) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
十二、案例: 排查HTTP请求返回 404
在这个案例里,展示排查 HTTP 404 问题的技巧。
➜ ~ curl http://localhost/a.txt {"timestamp":1574861225159,"status":404,"error":"Not Found","message":"No message available","path":"/a.txt"}%
那么到底是哪个 Servlet 处理了这个请求,返回了404?
1、跟踪所有的Servlet函数
开始trace:
trace javax.servlet.Servlet * > /tmp/servlet.txt
访问:http://localhost/a.txt
在Terminal 3里,查看 /tmp/servlet.txt
的内容:
less /tmp/servlet.txt
/tmp/servlet.txt
里的内容会比较多,需要耐心找到调用树里最长的地方。
可以发现请求最终是被 freemarker 处理的:
`---[13.974188ms] org.springframework.web.servlet.ViewResolver:resolveViewName() +---[0.045561ms] javax.servlet.GenericServlet:<init>() +---[min=0.045545ms,max=0.074342ms,total=0.119887ms,count=2] org.springframework.web.servlet.view.freemarker.FreeMarkerView$GenericServletAdapter:<init>() +---[0.170895ms] javax.servlet.GenericServlet:init() | `---[0.068578ms] javax.servlet.GenericServlet:init() | `---[0.021793ms] javax.servlet.GenericServlet:init() `---[0.164035ms] javax.servlet.GenericServlet:getServletContext()
十三、案例: 理解 Spring Boot 应用的 ClassLoader 结构
下面介绍 classloader
命令的功能。
先访问一个jsp网页,触发jsp的加载:http://localhost/hello
classloader 命令教程:https://alibaba.github.io/arthas/classloader.html
1、列出所有ClassLoader
# 命令 classloader -l # 测试 [arthas@67655]$ classloader -l name loadedCount hash parent BootstrapClassLoader 2856 null null com.taobao.arthas.agent.ArthasClassloader@75ad4f0 2154 75ad4f0 sun.misc.Launcher$ExtClassLoader@f6f4d33 java.net.FactoryURLClassLoader@250fe56d 842 250fe56d sun.misc.Launcher$AppClassLoader@55f96302 org.apache.jasper.servlet.JasperLoader@420348f1 1 420348f1 TomcatEmbeddedWebappClassLoader context: ROOT delegate: true ----------> Parent Classloader: org.springframework.boot.loader.LaunchedURLClass Loader@33c7353a TomcatEmbeddedWebappClassLoader 0 3915eb83 org.springframework.boot.loader.LaunchedURLClass context: ROOT Loader@33c7353a delegate: true ----------> Parent Classloader: org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a 5522 33c7353a sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$AppClassLoader@55f96302 45 55f96302 sun.misc.Launcher$ExtClassLoader@f6f4d33 sun.misc.Launcher$ExtClassLoader@f6f4d33 7 f6f4d33 null Affect(row-cnt:8) cost in 69 ms.
TomcatEmbeddedWebappClassLoader
加载的 class
数量是 0
,所以在 spring boot embedded tomcat
里,它只是一个空壳,所有的类加载都是 LaunchedURLClassLoader
完成的
2、列出 ClassLoader 里加载的所有类
列出上面的 org.apache.jasper.servlet.JasperLoader
加载的类:
# 命令 classloader -a -c 420348f1 # 测试 [arthas@67655]$ classloader -a -c 420348f1 hash:1107511537, org.apache.jasper.servlet.JasperLoader@420348f1 org.apache.jsp.jsp.hello_jsp Affect(row-cnt:0) cost in 6 ms.
反编译 jsp
的代码
# 命令 jad org.apache.jsp.jsp.hello_jsp
3、查看 ClassLoader 树
# 命令 classloader -t # 测试 [arthas@67655]$ classloader -t +-BootstrapClassLoader +-sun.misc.Launcher$ExtClassLoader@f6f4d33 +-com.taobao.arthas.agent.ArthasClassloader@75ad4f0 +-sun.misc.Launcher$AppClassLoader@55f96302 +-java.net.FactoryURLClassLoader@250fe56d +-org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a +-TomcatEmbeddedWebappClassLoader context: ROOT delegate: true ----------> Parent Classloader: org.springframework.boot.loader.LaunchedURLClassLoader@33c7353a +-org.apache.jasper.servlet.JasperLoader@420348f1 Affect(row-cnt:8) cost in 35 ms.
4、列出 ClassLoader 的 urls
比如上面查看到的 spring LaunchedURLClassLoader
的 hashcode 是 33c7353a
,可以通过 -c
参数来列出它的所有 urls
:
# 命令 classloader -c 33c7353a # 测试 [arthas@67655]$ classloader -c 33c7353a jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/spring-boot-starter-aop-1.5.13.RELEASE.jar!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/spring-boot-starter-1.5.13.RELEASE.jar!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/spring-boot-1.5.13.RELEASE.jar!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/spring-boot-autoconfigure-1.5.13.RELEASE.jar!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/spring-boot-starter-logging-1.5.13.RELEASE.jar!/ jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/lib/logback-classic-1.1.11.jar!/
5、加载指定 ClassLoader 里的资源文件
查找指定的资源文件:classloader -c 33c7353a -r logback-spring.xml
[arthas@67655]$ classloader -c 33c7353a -r logback-spring.xml jar:file:/Users/tingfeng/alibaba-arthas/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/logback-spring.xml Affect(row-cnt:1) cost in 8 ms.
6、尝试加载指定的类
比如用上面的 spring LaunchedURLClassLoader
尝试加载 java.lang.String
:
# 命令 classloader -c 33c7353a --load java.lang.String # 测试 [arthas@67655]$ classloader -c 33c7353a --load java.lang.String load class success. class-info java.lang.String code-source name java.lang.String isInterface false isAnnotation false isEnum false isAnonymousClass false isArray false isLocalClass false isMemberClass false isPrimitive false isSynthetic false simple-name String modifier final,public annotation interfaces java.io.Serializable,java.lang.Comparable,java.lang.CharSequence super-class +-java.lang.Object class-loader classLoaderHash null
十四、案例:查找 Top N 线程
# 查看所有线程信息 thread # 查看线程ID 16 的栈 thread 16 # 查看 CPU 使用率 top n 线程的栈 thread -n 3 # 查看 5 秒内的 CPU 使用率 top n 线程栈 thread -n 3 -i 5000 # 查找线程是否有阻塞 thread -b
十五、Web Console
Arthas 支持通过 Web Socket 来连接。
当在本地启动时,可以访问:http://127.0.0.1:3658/,通过浏览器来使用Arthas。
WebConsole 官方文档:https://alibaba.github.io/arthas/web-console.html?highlight=console
十六、Exit/Stop
1、reset
Arthas在 watch
/trace
等命令时,实际上是修改了应用的字节码,插入增强的代码。显式执行 reset
命令,可以清除掉这些增强代码。
2、退出 Arthas
用 exit
或者 quit
命令可以退出Arthas。
退出Arthas之后,还可以再次用 java -jar arthas-boot.jar 来连接。
3、彻底退出 Arthas
exit
/quit
命令只是退出当前session,arthas server还在目标进程中运行。
想完全退出Arthas,可以执行 stop
命令。
十七、arthas-boot支持的参数
arthas-boot.jar
支持很多参数,可以执行 java -jar arthas-boot.jar -h
来查看。
1、允许外部访问
默认情况下, arthas server 侦听的是 127.0.0.1
这个IP,如果希望远程可以访问,可以使用 --target-ip
的参数。
java -jar arthas-boot.jar --target-ip
2、列出所有的版本
java -jar arthas-boot.jar --versions
3、使用指定版本:
java -jar arthas-boot.jar --use-version 3.1.0
4、只侦听 Telnet 端口,不侦听 HTTP 端口
java -jar arthas-boot.jar --telnet-port 9999 --http-port -1
5、打印运行的详情
java -jar arthas-boot.jar -v
十六、生成火焰图
Arthas 3.1.5 版本带来下面全新的特性,其中就有开箱即用的Profiler/火焰图功能
火焰图的威名相信大家都有所耳闻,但可能因为使用比较复杂,所以望而止步。
在新版本的Arthas里集成了 async-profiler
,使用 profiler
命令就可以很方便地生成火焰图,并且可以在浏览器里直接查看。
profiler命令wiki: https://alibaba.github.io/arthas/profiler.html
profiler
命令基本运行结构是 profiler action [actionArg]
。下面介绍如何使用。
未经允许请勿转载:程序喵 » Alibaba Arthas 开源Java诊断工具使用