Alibaba Arthas 开源Java诊断工具使用

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。

arthas.png

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/

webdemo.png

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。

arthas-logo.png

输入 help 可以获取到更多的帮助信息。

help.png

二、查看JVM信息

下面介绍Arthas里查看JVM信息的命令。

1、sysprop 系统配置

sysprop 可以打印所有的System Properties信息。

  • 也可以指定单个key:sysprop java.version

  • 也可以通过grep来过滤:sysprop | grep user

  • 可以设置新的value:sysprop testKey testValue

sysprop.png

2、sysenv 系统环境变量

sysenv 命令可以获取到环境变量。和 sysprop 命令类似。

3、jvm

jvm 命令会打印出JVM的各种详细信息。

4、dashboard 数据面板

dashboard 命令可以查看当前系统的实时数据面板。

输入 Q 或者 Ctrl+C 可以退出命令。

dashboard.png

三、提示

为了更好使用Arthas,下面先介绍Arthas里的一些使用技巧。

1、help

Arthas里每一个命令都有详细的帮助信息。可以用-h来查看。帮助信息里有EXAMPLES和WIKI链接。

比如:sysprop -h

sysprop-help.png

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

jad.png

通过 --source-only 参数可以只打印出在反编译的源代码:jad --source-only com.example.demo.arthas.user.UserController

jad-source-only.png

五、Ognl

在Arthas里,有一个单独的 ognl 命令,可以动态执行代码。

1、调用static函数

ognl '@java.lang.System@out.println("hello ognl")'

可以检查 Terminal 1 里的进程输出,可以发现打印出了 hello ognl

hello-ognl.png

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 表达式。

一些更复杂的用法,可以参考:

六、案例: 排查函数调用异常

目前,访问 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/0watch 命令会打印调用的参数和异常。如果想把获取到的结果展开,可以用-x参数:

# 命令
watch com.example.demo.arthas.user.UserController * '{params, throwExp}' -x 2

watch.png

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

jad-jsp.png

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

thread.png

十五、Web Console

Arthas 支持通过 Web Socket 来连接。

当在本地启动时,可以访问:http://127.0.0.1:3658/,通过浏览器来使用Arthas。

webconsole.png

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诊断工具使用

点  赞 (1) 打  赏
分享到: