arthas诊断工具简单使用

Arthas(阿尔萨斯)是什么

Arthas是Alibaba开源的Java诊断工具

Arthas能做什么

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 是否有一个全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?
  8. 怎样直接从JVM内查找某个类的实例?

开发中常见的问题

  1. 前端调用一个后端的接口,该接口逻辑非常复杂,可能会调用其他service接口或其他模块的FeignClient,但是在其中某个接口报错了,报错信息又不是很明显,这种情况下,一般要么分析代码逻辑,要么增加日志输出来排查发生异常的原因

    使用Arthas后,可以通过watch命令观察指定方法的调用情况,可以观察入参返回值异常信息本次调用的方法信息本次调用的类信息等,配合ognl表达式可以实现复杂的操作

  2. 后端改动或者增加一个复杂的接口后,联调时可能遇到一个问题,改一遍代码,然后pull代码,编译,重启服务,然后又遇到问题,再修改代码,再重复同样的操作,这些步骤十分的耗时,如果开发时间比较紧迫,更加让人难以忍受

    使用Arthas的jadmcretransform命令修改java代码,编译字节码文件,加载新的字节码,不需要重新pull代码,编译,重启等一系列耗时的操作,就能直接实现修改后的效果

如何使用

  1. 安装启动

    安装方式很多,有直接启动jar包、使用shell脚本、rpm安装等,这里使用直接启动jar包的方式

    1
    2
    3
    4
    5
    # 下载arthas-boot.jar
    curl -O https://arthas.aliyun.com/arthas-boot.jar

    # 启动
    java -jar arthas-boot.jar
  2. 选择要attach的进程

    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
    [INFO] arthas-boot version: 3.5.4
    [INFO] Process 10294 already using port 3658
    [INFO] Process 10294 already using port 8563
    [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
    * [1]: 10294 java-study-0.0.1-SNAPSHOT.jar
    [2]: 13008 org.apache.zookeeper.server.quorum.QuorumPeerMain
    [3]: 19681 /usr/nacos/target/nacos-server.jar
    [4]: 4819 org.elasticsearch.bootstrap.Elasticsearch
    [5]: 21381 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
    [6]: 22535 kafka.Kafka
    [7]: 21772 /usr/my_service/java_study/java-study-0.0.1-SNAPSHOT.jar
    1
    [INFO] arthas home: /root/.arthas/lib/3.5.4/arthas
    [INFO] The target process already listen port 3658, skip attach.
    [INFO] arthas-client connect 127.0.0.1 3658
    ,---. ,------. ,--------.,--. ,--. ,---. ,---.
    / O \ | .--. ''--. .--'| '--' | / O \ ' .-'
    | .-. || '--'.' | | | .--. || .-. |`. `-.
    | | | || |\ \ | | | | | || | | |.-' |
    `--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'


    wiki https://arthas.aliyun.com/doc
    tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
    version 3.5.4
    main_class
    pid 10294
    time 2021-09-15 10:18:15
  3. 输入help查看命令帮助信息

使用watch命令观察方法的调用情况,如入参,返回值,异常信息等

  1. watch命令参数

    参数名称 参数说明
    class-pattern 类名表达式匹配
    method-pattern 方法名表达式匹配
    express 观察表达式,默认值:{params, target, returnObj}
    condition-express 条件表达式
    [b] 方法调用之前观察
    [e] 方法异常之后观察
    [s] 方法返回之后观察
    [f] 方法结束之后(正常返回和异常返回)观察
    [E] 开启正则表达式匹配,默认为通配符匹配
    [x:] 指定输出结果的属性遍历深度,默认为 1
  2. 观察表达式

    观察表达式的构成主要由ognl表达式组成,使用它可以获取对象的属性,调用对象的方法等

    ognl表达式可以使用的对象有params(入参)、target(当前调用的类信息)、returnObj(返回值)、throwExp(异常信息)等,详细内容参考表达式核心变量

  3. 测试代码

    根据id查询用户信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public User getUserById(Long id) throws Exception {
    if (StringUtils.isEmpty(id)) {
    throw new Exception("用户id为空!");
    }

    List<User> collect = userList.stream().filter(u -> id.equals(u.getId()))
    .collect(Collectors.toList());
    if (CollectionUtils.isEmpty(collect)) {
    throw new Exception("根据id未找到用户信息");
    }

    return collect.get(0);
    }
  4. 观察方法调用情况

    • 观察方法入参和返回值

      执行watch命令:

      1
      2
      # {params, returnObj} 表示需要观察方法入参和返回值,-n 5 表示执行5次后结束 -x 3 表示遍历对象的深度
      watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1

      查看控制台输出:这里result中包含了入参和返回的对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 25
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:28:29; [cost=1.151249ms] result=@ArrayList[
      @Object[][
      @Long[1],
      ],
      @User[
      id=@Long[1],
      username=@String[1001],
      avatar=@String[https://xxxx.com/1001.png],
      phoneNumber=@String[10010010001],
      age=@Integer[20],
      gender=@String[男],
      ],
      ]
    • 观察方法抛出的异常信息

      执行watch命令:

      1
      2
      # {params, throwExp} 表示需要观察方法入参和异常对象
      watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1001

      查看控制台输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 70 ms, listenerId: 26
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
      ts=2021-09-17 14:29:56; [cost=2.055759ms] result=@ArrayList[
      @Object[][
      @Long[1001],
      ],
      java.lang.Exception: 根据id未找到用户信息
      at com.codecho.test.service.UserServiceImpl.getUserById(UserServiceImpl.java:48)
      at com.codecho.test.controller.TestController.getUserById(TestController.java:28)
      at sun.reflect.GeneratedMethodAccessor25.invoke(Unknown Source)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:498)
      ...

      可以通过调整观察表达式只展示异常具体信息

      1
      watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3

      重新调用接口,再次查看控制台输出

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, throwExp.getMessage}' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 27
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExceptionExit
      ts=2021-09-17 14:31:08; [cost=1.685761ms] result=@ArrayList[
      @Object[][
      @Long[1001],
      ],
      @String[根据id未找到用户信息],
      ]
    • 只观察满足条件的方法调用

      执行watch命令:

      1
      2
      # params[0]%2==0 表示当用户id为偶数时才会记录调用信息
      watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3

      调用接口:

      1
      2
      3
      4
      curl http://localhost:8088/test/user/1
      curl http://localhost:8088/test/user/2
      curl http://localhost:8088/test/user/3
      curl http://localhost:8088/test/user/4

      查看控制台输出:

      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
      [arthas@10294]$ watch com.codecho.test.service.UserService getUserById '{params, returnObj}' 'params[0]%2==0' -n 5 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 1) cost in 71 ms, listenerId: 28
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:32:06; [cost=0.067629ms] result=@ArrayList[
      @Object[][
      @Long[2],
      ],
      @User[
      id=@Long[2],
      username=@String[1002],
      avatar=@String[https://xxxx.com/1002.png],
      phoneNumber=@String[10010010002],
      age=@Integer[23],
      gender=@String[男],
      ],
      ]
      method=com.codecho.test.service.UserServiceImpl.getUserById location=AtExit
      ts=2021-09-17 14:32:11; [cost=0.081801ms] result=@ArrayList[
      @Object[][
      @Long[4],
      ],
      @User[
      id=@Long[4],
      username=@String[1004],
      avatar=@String[https://xxxx.com/1004.png],
      phoneNumber=@String[10010010004],
      age=@Integer[21],
      gender=@String[女],
      ],
      ]

      执行watch命令:

      1
      2
      # #cost > 5 表示耗时超过5ms才会记录调用信息 -v 表示打印条件表达式的值和结果
      watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3

      调用接口:

      1
      curl http://localhost:8088/test/user/1

      查看控制台输出:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      [arthas@10294]$ watch com.codecho.test.service.UserService * '{params, returnObj}' '#cost > 5' -v -n 1 -x 3
      Press Q or Ctrl+C to abort.
      Affect(class count: 2 , method count: 8) cost in 76 ms, listenerId: 29
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false
      Condition express: #cost > 5 , result: false

使用trace命令查看方法的调用路径和每个节点的耗时

  1. trace命令参数

    参数名称 参数说明
    class-pattern 类名表达式匹配
    method-pattern 方法名表达式匹配
    condition-express 条件表达式
    [E] 开启正则表达式匹配,默认为通配符匹配
    [n:] 命令执行次数
    #cost 方法执行耗时
  2. 观察接口调用路径

    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
    [arthas@40528]$ trace *ActionInfoServiceImpl interactionAssist -n 2
    Press Q or Ctrl+C to abort.
    Affect(class count: 2 , method count: 2) cost in 857 ms, listenerId: 4
    `---ts=2021-09-17 15:37:53;thread_name=http-nio-8325-exec-1;id=85;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@4258106
    `---[26.143404ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl$$EnhancerBySpringCGLIB$$34d24eff:interactionAssist() [throws Exception]
    +---[25.926649ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() [throws Exception]
    | `---[21.546574ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:interactionAssist() [throws Exception]
    | +---[0.131916ms] com.alibaba.fastjson.JSONObject:toJSONString() #6957
    | +---[0.190136ms] org.apache.logging.log4j.Logger:info() #6957
    | +---[0.00505ms] com.hand.core.util.UserUtil:getUser() #6960
    | +---[0.00535ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getInteractionId() #6966
    | +---[1.262189ms] com.hand.interaction.base.interaction.service.InteractionService:queryByIdCache() #6966
    | +---[0.00564ms] com.hand.interaction.base.interaction.model.Interaction:getEndTime() #6970
    | +---[0.00526ms] com.hand.interaction.base.interaction.model.Interaction:getLicenseMode() #6977
    | +---[0.010319ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:checkUserInfo() #6977
    | +---[0.004508ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getInteractionId() #6980
    | +---[0.00497ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getOpenId() #6980
    | +---[0.006282ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getMobilePhone() #6980
    | +---[0.127087ms] com.hand.interaction.base.util.CommonUtils:tianYu() #6980
    | +---[0.005831ms] com.hand.base.user.model.User:getOpenId() #6982
    | +---[0.005991ms] com.hand.core.util.StringUtils:isBlank() #6983
    | +---[0.009688ms] com.hand.interaction.base.interaction.model.Interaction:getBoostType() #6988
    | +---[0.008306ms] com.hand.interaction.base.actioninfo.model.ActionInfo:getHpOpenid() #6988
    | +---[0.004388ms] com.hand.core.util.StringUtils:isBlank() #6989
    | +---[0.007474ms] com.hand.interaction.base.actioninfo.model.ActionInfo:setHpOpenid() #6992
    | +---[min=0.003366ms,max=0.007244ms,total=0.01061ms,count=2] com.hand.interaction.base.interaction.model.Interaction:getInteractionFormat() #6997
    | +---[0.005109ms] com.hand.interaction.base.interaction.model.Interaction:getShareMode() #7026
    | +---[0.004529ms] com.hand.interaction.base.interaction.model.Interaction:getId() #7028
    | +---[0.005249ms] com.hand.interaction.base.interaction.model.Interaction:getBoostType() #7035
    | +---[19.019048ms] com.hand.interaction.base.actioninfo.service.ActionInfoServiceImpl:newMemBoostValidate() #7050 [throws Exception]
    | `---throw:com.hand.core.basic.service.BasicServiceException #7585 [您已经是会员,无法助力!]
    `---throw:com.hand.core.basic.service.BasicServiceException #49 [您已经是会员,无法助力!]

使用jadmcretransform命令热更新代码

  1. 测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void payMoney(User user) throws Exception {
    // 查找用户
    User queryUser = null;
    if (!StringUtils.isEmpty(user.getId())) {
    queryUser = getUserById(user.getId());
    } else {
    queryUser = getUserByUsername(user.getUsername());
    }

    // 测试调用普通方法
    callPublicMethod(queryUser);
    callPrivateMethod(queryUser);

    // 支付收款
    payService.pay(queryUser, new BigDecimal("88.8"));
    }
  2. 使用jad命令反编译要修改的类的字节码,并保存到指定目录

    1
    jad --source-only *UserServiceImpl > /usr/arthas/UserServiceImpl.java
  3. 使用vim编辑反编译后的java代码,将金额从88.8改成99.9

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void payMoney(User user) throws Exception {
    // 查找用户
    User queryUser = null;
    if (!StringUtils.isEmpty(user.getId())) {
    queryUser = getUserById(user.getId());
    } else {
    queryUser = getUserByUsername(user.getUsername());
    }

    // 测试调用普通方法
    callPublicMethod(queryUser);
    callPrivateMethod(queryUser);

    // 支付收款
    payService.pay(queryUser, new BigDecimal("88.8"));
    }
  4. 使用mc命令内存编译修改后的java代码,-d 指定输出class的目录

    1
    mc /usr/arthas/UserServiceImpl.java -d /usr/arthas

    注意mc命令是可能编译失败的,如果编译失败,可以在本地修改java代码后将编译后的class文件上传到服务器上再使用mc命令

  5. 使用retransform命令加载修改后的字节码文件

    1
    retransform /usr/arthas/UserServiceImpl.class
  6. 使用watch命令观察接口入参,重新调用接口

    1
    watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    1
    curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    Press Q or Ctrl+C to abort.
    Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
    method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
    ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
    @User[
    id=@Long[2],
    username=@String[1002],
    avatar=@String[https://xxxx.com/1002.png],
    phoneNumber=@String[10010010002],
    age=@Integer[23],
    gender=@String[男],
    ],
    @String[99.9],
    ]
  7. 如果要取消retransform命令的效果,需要删除修改类的retransform entry 并重新触发retransform,如果不进行此操作,即使stop arthas后,retransform的效果仍会生效

    1
    2
    3
    4
    5
    6
    7
    8
    # 查看retransform entry
    retransform -l

    # 删除指定的retransform entry
    retransform -d 1

    # 重新触发retransform
    retransform --classPattern *UserServiceImpl

    使用watch命令观察接口入参,重新调用接口

    1
    watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    1
    curl -H "Content-Type:application/json" -X POST -d '{"username": "1002"}' http://localhost:8088/test/user/payMoney
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 99.9 元的收款
    测试调用普通public方法
    测试调用普通private方法
    用户 1002支付了一笔 88.8 元的收款
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [arthas@10294]$ watch *PayServiceImpl pay '{params[0], params[1].toString}' -n 5 -x 3
    Press Q or Ctrl+C to abort.
    Affect(class count: 1 , method count: 1) cost in 48 ms, listenerId: 30
    method=com.codecho.test.service.PayServiceImpl.pay location=AtExit
    ts=2021-09-17 15:40:10; [cost=0.112135ms] result=@ArrayList[
    @User[
    id=@Long[2],
    username=@String[1002],
    avatar=@String[https://xxxx.com/1002.png],
    phoneNumber=@String[10010010002],
    age=@Integer[23],
    gender=@String[男],
    ],
    @String[88.8],
    ]

quitstop

  1. quit

    退出当前arthas客户端,arthas服务端不会关闭,所做的修改不会重置

  2. stop

    关闭arthas服务端,所有arthas客户端都会退出,关闭前增强类会被重置,使用redefineredefine重加载的类不会被重置