您好,欢迎来到爱go旅游网。
搜索
您的当前位置:首页Java线上问题排查神器Arthas实战原理解析

Java线上问题排查神器Arthas实战原理解析

来源:爱go旅游网
Java线上问题排查神器Arthas实战原理解析

概述

背景

是不是在实际开发⼯作当中经常碰到⾃⼰写的代码在开发、测试环境⾏云流⽔稳得⼀笔,可⼀到线上就经常不是缺这个就是少那个反正就是⼀顿报错抽风似的,线上调试代码⼜很⿇烦,让⼈头疼得抓狂;⽽且debug不⼀定是最⾼效的⽅法,遇到线上问题不能debug了怎么办。原先我们Java中我们常⽤分析问题⼀般是使⽤JDK⾃带或第三⽅的分析⼯具如jstat、jmap、jstack、 jconsole、visualvm、Java Mission Control、MAT等。但此刻的你没有看错,还有⼀款神器Arthas⼯具着实让⼈吃惊,可帮助程序员解决很多繁琐的问题,使得加班解决线上问题成为过去的可能性⼤⼤提⾼。

定义

Arthas是⼀个Java诊断⼯具,由阿⾥巴巴中间件团队开源,⽬前已在Java开发⼈员中被⼴泛采⽤。Arthas能够分析,诊断,定位Java应⽤问题,例如:JVM信息,线程信息,搜索类中的⽅法,跟踪代码执⾏,观测⽅法的⼊参和返回参数等等。并能在不修改应⽤代码的情况下,对业务问题进⾏诊断,包括查看⽅法的出⼊参,异常,监测⽅法执⾏耗时,类加载信息等,⼤⼤提升线上问题排查效率。简单的话:就是再不重启应⽤的情况下达到排查问题的⽬的。

特性

仪表盘实时查看系统的运⾏状态。

OGNL表达式查看参数和返回值/例外,查看⽅法参数、返回值和异常。通过jad/sc/redefine实现在线热插拔。快速解决类冲突问题,定位类加载路径。快速定位应⽤热点和⽣成⽕焰图。⽀持在线诊断WebConsole。

Arthas对应⽤程序没有侵⼊(但对宿主机jvm有侵⼊),代码或项⽬中不需要引⼊jar包或依赖,因为是通过attach的机制实现的,我们的应⽤的程序和arthas都是独⽴的进程,arthas是通过和jvm底层交互来获取运⾏在其上的应⽤程序实时数据的,灵活查看运⾏时的值,这个和hickwall,jprofiler等监控软件的区别(JPofiler也有这样的功能,但是是收费的)动态增加aop代理和监控⽇志功能,⽆需重启服务,⽽且关闭arthas客户端后会还原所有增强过的类,原则上是不会影响现有业务逻辑的。

对应⽤程序所在的服务器性能的影响,个别命令使⽤不当的话,可能会撑爆jvm内存或导致应⽤程序响应变慢,命令的输出太多,接⼝调⽤太频繁会记录过多的数据变量到内存⾥,⽐如tt指令,建议加 -n 参数 限制输出次数,sc * 通配符的使⽤不当,范围过⼤,使⽤异步任务时,请勿同时开启过多的后台异步命令,以免对⽬标JVM性能造成影响,⼀把双刃剑(它甚⾄可以修改jdk⾥的原⽣类),所以在线上运⾏肯定是需要权限和流程控制的。

使⽤场景

在⽇常开发中,当我们发现应⽤的某个接⼝响应⽐较慢,这个时候想想要分析⼀下原因,找到代码中耗时的部分,⽐较容易想到的是在接⼝链路的 IO 操作上下游打印时间⽇志,再根据⼏个时间点的⽇志算出耗时长的 IO 操作。这种⽅式没有问题,但是加⽇志需要发布,既繁琐⼜低效,这个时候可以引⼊⼀些线上 debug 的⼯具,arthas 就是很好的⼀种,除了分析耗时,还可以打印调⽤栈、⽅法⼊参及返回,类加载情况,线程池状态,系统参数等等,其实现原理是解析 JVM 在操作系统中的⽂件,⼤部分操作是只读的,对服务进程没有侵⼊性,因此可以放⼼使⽤。

实战

CPU占⽤⾼⽰例

创建⼀个springboot项⽬并打包成arthas-demo-1.0.jar,启动arthas-demo-1.0.jar代码⽰例如下

package cn.itxs;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;import java.util.UUID;

import java.util.concurrent.TimeUnit;@SpringBootApplicationpublic class App {

public static void main(String[] args) { SpringApplication.run(App.class,args); new Thread( () -> { while (true) {

String str = UUID.randomUUID().toString().replaceAll(\"-\ }

},\"cpu demo thread\").start(); try {

TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }

},\"cpu with sleep thread\").start(); }}

安装与使⽤

推荐⽅式

# 下载`arthas-boot.jar`这种也是官⽅推荐的⽅式curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动arthas-boot.jar,必须启动⾄少⼀个 java程序,否则会⾃动退出。运⾏此命令会⾃动发现 java进程,输⼊需要 attach 进程对应的序列号,例如,输⼊1按回车则会监听该进程。java -jar arthas-boot.jar

# ⽐如输⼊JVM (jvm实时运⾏状态,内存使⽤情况等)

CPU占⽤⾼⽰例

package cn.itxs.controller;

import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;@RestController

@RequestMapping(\"/thread\")public class ThreadController {

private Object obj1 = new Object(); private Object obj2 = new Object(); @RequestMapping(\"/test\") @ResponseBody public String test(){ new Thread(() -> {

synchronized (obj1){ try {

TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { synchronized (obj2){

System.out.printf(\"thread 1执⾏到此\"); } } }

},\"thread 1\").start();

synchronized (obj2) { synchronized (obj1){

System.out.printf(\"thread 2执⾏到此\"); },\"thread 2\").start(); return \"thread test\"; }}

SpringBoot启动类

package cn.itxs;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class App {

public static void main(String[] args) { SpringApplication.run(App.class,args); }}

第⼀部分时显⽰JVM中运⾏的所有线程:所在线程组,优先级,线程的状态,CPU的占有率,是否是后台进程等。第⼆部分显⽰的JVM内存的使⽤情况和GC的信息。第三部分是操作系统的⼀些信息和 Java版本号。

# 当前最忙的前N个线程 thread -b, ##找出当前阻塞其他线程的线程 thread -n 5 -i 1000 #间隔⼀定时间后展⽰,本例中可以看到最忙CPU线程为id=45,代码⾏数为19thread -n 5

# jad查看反编译的代码

jad cn.itxs.controller.CpuController

线程死锁⽰例

package cn.itxs.controller;

import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;@RestController

@RequestMapping(\"/thread\")public class ThreadController {

private Object obj1 = new Object(); private Object obj2 = new Object(); @RequestMapping(\"/test\") @ResponseBody public String test(){ new Thread(() -> {

synchronized (obj1){ try {

TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { }

synchronized (obj2){

System.out.println(\"thread 1执⾏到此\"); }

},\"thread 1\").start(); try {

TimeUnit.SECONDS.sleep(2);

} catch (InterruptedException e) { e.printStackTrace(); }

synchronized (obj2) { synchronized (obj1){

System.out.println(\"thread 2执⾏到此\"); },\"thread 2\").start(); return \"thread test\"; }}

# 启动SpringBoot演⽰程序,访问页⾯http://192.168.50.100:8080/thread/test# 运⾏arthas,查看线程thread

# 查看阻塞线程thread -b

# jad反编译查看代码

jad --source-only cn.itxs.controller.ThreadController

线上修复热部署

准备⼀个有问题的java类

package cn.itxs.controller;

import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import java.util.UUID;

import java.util.concurrent.TimeUnit;@RestController

@RequestMapping(\"/hot\")public class HotController { @RequestMapping(\"/test\") @ResponseBody public String test(){ boolean flag = true; if (flag) {

System.out.println(\"开始处理逻辑\");

throw new RuntimeException(\"出异常了\"); }

System.out.println(\"结束流程\"); return \"hot test\"; }}

# 第⼀步:`jad命令` 将需要更改的⽂件先进⾏反编译,保存下来 ,编译器修改,-c 指定这个类的classloader的哈希值,–source-only只显⽰源码,最后是⽂件反编译之后的存放路径jad --source-only cn.itxs.controller.HotController > /home/commons/arthas/data/HotController.java

# 我们将HotController.java中的throw new RuntimeException(\"出异常了\")代码删掉,修改完后需要将类重新加载到JVM

# 第⼆步:`SC命令` 查找当前类是哪个classLoader加载的,⾸先,使⽤sc命令找到要修改的类.sc全称-search class, -d表⽰detail,主要是为了获取classLoader的hash值sc -d *HotController | grep classLoader

classLoaderHash 6267c3bb #类加载器 编号

# 第三步:`MC命令` ⽤指定的classloader重新将类在内存中编译

mc -c 6267c3bb /home/commons/arthas/data/HotController.java -d /home/commons/arthas/class# 第四步:`redefine命令` 将编译后的类加载到JVM,参数是编译后的.class⽂件地址redefine /home/commons/arthas/class/cn/itxs/controller/HotController.class

上⾯我们是⼿⼯⼀步步执⾏,当然我们可以使⽤shell脚本串起来简单操作。

此外还可以安装Alibaba Cloud Toolkit热部署组件(⼀键retransform),热部署组件⽀持⼀键将编辑器中修改的 Java 源码快速编译,并更新到远端应⽤服务中,免去⼿动 dump、mc 的过程。此外,也可以⼀键还原 retransform 的类⽂件。

安装基于Arthas实现的简单好⽤的热部署插件ArthasHotSwap可以⼀键⽣成热部署命令,提⾼我们线上维护的效率。

线上问题常见定位

watch(⽅法执⾏数据观测)

package cn.itxs.controller;

import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import java.util.ArrayList;import java.util.List;

import java.util.Random;@RestController

@RequestMapping(\"/watch\")public class WatchController {

private static Random random = new Random(); private int illegalArgumentCount = 0; @RequestMapping(\"/test\") @ResponseBody public String test(){ String res = null; try {

int number = random.nextInt() / 10000;

List idStrs = this.getIdStr(number); res = printList(number, idStrs); }

catch (Exception e) {

System.out.println(String.format(\"illegalArgumentCount:%3d, \ return res; }

private List getIdStr(int number) { if (number < 5) {

++this.illegalArgumentCount;

throw new IllegalArgumentException(\"number is: \" + number + \ ArrayList result = new ArrayList(); int count = 2;

while (count <= number) { if (number % count == 0) { result.add(count); number /= count; count = 2; continue; }

++count; return result;

private String printList(int number, List primeFactors) { StringBuffer sb = new StringBuffer(number + \"=\"); for (int factor : primeFactors) { sb.append(factor).append('*'); if (sb.charAt(sb.length() - 1) == '*') { sb.deleteCharAt(sb.length() - 1); System.out.println(sb); return sb.toString();}

# Arthas中的**watch**命令可以让我们能⽅便的观察到指定⽅法的调⽤情况,可以观察到返回值,⼊参,以及变量等。# watch 全路径类名 ⽅法名 观察表达式 -x 3 ,观察表达式匹配ognl表达式,观察的维度也⽐较多。# ⽐如:watch cn.itxs.controller.WatchController printList \"{params,returnObj}\" -x 3

# 查看printList⽅法的⼊参和出参,-x表⽰的是遍历结果深度默认1,只会打印对象的堆地址,看不到具体的属性值,-x 2会打印结果的属性值的信息 -x 3会输出⼊参属性值和结果属性值# -n 1只抓紧⼀次,由于我们这⾥是模拟⼀直请求的

watch cn.itxs.controller.WatchController printList '{params}' -n 1# -x 表⽰的是遍历结果深度默认3

watch cn.itxs.controller.WatchController printList '{params}' -n 1 -x 3# params[0]代表第⼀个参数

watch cn.itxs.controller.WatchController printList '{params[0]}' -n 1 -x 3

# ⽅法的返回值

watch cn.itxs.controller.WatchController getIdStr '{returnObj}' -n 1 -x 3# ⽅法参数和返回值

watch cn.itxs.controller.WatchController getIdStr '{params,returnObj}' -n 1 -x 3

# 观察⽅法执⾏前后当前对象属性值

watch cn.itxs.controller.WatchController getIdStr 'target.illegalArgumentCount'

# 观察异常信息,观察表达式⾥增加throwExp就好了。如果增加-e 参数就能过滤掉⾮异常的监听了。

# 在观察表达式后⾯,我们可以增加条件表达式,例如按时间过滤:#cost>0.5,单位是毫秒,那么控制台输出来的都是耗时在0.5毫秒以上的⽅法调⽤watch cn.itxs.controller.WatchController getIdStr '{params}' '#cost>0.5'

# 按条件过滤观察params[1].size>4:这⾥⽀持ognl表达式。下⾯例⼦的意思是:第⼆个参数(也就是List primeFactors),的size⼤于4的时候才观察⼊参。watch cn.itxs.controller.WatchController printList '{params}' 'params[1].size>4' -x 3

monitor(⽅法执⾏监控)

monitor结果包括如下

timestamp:时间戳class:Java类

method:⽅法(构造⽅法、普通⽅法)total:调⽤次数success:成功次数fail:失败次数rt:平均RT

fail-rate:失败率

# -c :统计周期,默认值为10秒

monitor -c 10 cn.itxs.controller.WatchController getIdStr# 在⽅法调⽤之前计算condition-express,⽅法后可带表达式monitor -b -c 10 cn.itxs.controller.WatchController getIdStr

trace

# trace:⽅法内部调⽤路径,并输出⽅法路径上的每个节点上耗时trace cn.itxs.controller.WatchController test -n 2

#包含jdk的函数--skipJDKMethod skip jdk method trace, default value true.默认情况下,trace不会包含jdk⾥的函数调⽤,如果希望trace jdk⾥的函数,需要显式设置--skipJDKMethod false。trace --skipJDKMethod false cn.itxs.controller.WatchController test -n 2

# 调⽤耗时过滤,只会展⽰耗时⼤于10ms的调⽤路径,有助于在排查问题的时候,只关注异常情况trace cn.itxs.controller.WatchController test '#cost > 1'

stack

# 输出当前⽅法被调⽤的调⽤路径,getIdStr是从test⽅法调⽤进来的stack cn.itxs.controller.WatchController getIdStr -n 1

# 输出当前⽅法被调⽤的调⽤路径,条件表达过滤,第0个参数⼩于0,也可以根据执⾏时间来过滤,'#cost>1'stack cn.itxs.controller.WatchController getIdStr 'params[0]<0' -n 1

tt

tt(TimeTunnel):⽅法执⾏数据的时空隧道,记录下指定⽅法每次调⽤的⼊参和返回信息,并能对这些不同的时间下调⽤进⾏观测。对于⼀个最基本的使⽤来说,就是记录下当前⽅法的每次调⽤环境现场。

# 记录指定⽅法的每次调⽤环境现场

tt -t cn.itxs.controller.WatchController getIdStr # 列出所有调⽤记录

tt -l cn.itxs.controller.WatchController getIdStr

# 筛选调⽤记录

tt -s 'method.name==\"getIdStr\"'# 查看调⽤信息tt -i 1001

# 重新发起⼀次调⽤tt -i 1001 -p

Web Console

# 启动时指定Linux的ip

java -jar arthas-boot.jar --target-ip 192.168.50.94

profiler

profiler 命令⽀持⽣成应⽤热点的⽕焰图。本质上是通过不断的采样,然后把收集到的采样结果⽣成⽕焰图。⼀般分析性能可以先通过Arthas profiler命令⽣成jfr⽂件;在本地通过jprofiler来分析jfr⽂件,定位

谁在调⽤我。

# 启动profiler 默认情况下,⽣成的是cpu的⽕焰图,即event为cpu。可以⽤--event参数来指定profiler start

# 获取已采集的sample的数量profiler getSamples# 查看profiler状态profiler status

# 停⽌profiler ⽣成html格式结果,默认情况下,结果⽂件是html格式,也可以⽤--format参数指定;或者在--file参数⾥⽤⽂件名指名格式。⽐如--file /tmp/result.htmlprofiler stop --format html

# profiler⽀持的eventsprofiler list

# 可以⽤--event参数指定要采样的事件,⽐如对alloc事件进⼊采样:profiler start --event alloc

# 使⽤execute来执⾏复杂的命令

profiler execute 'start,framebuf=5000000'

# ⽣成 jfr格式结果;注意,jfr只⽀持在 start时配置。如果是在stop时指定,则不会⽣效。profiler start --file /tmp/test.jfr

# 配置 include/exclude 来过滤数据

profiler start --include 'java/*' --include 'demo/*' --exclude '*Unsafe.park*'# profiler执⾏ 300 秒⾃动结束,可以⽤ -d/--duration 参数指定profiler start --duration 300

其他功能

{

\"action\": \"exec\

\"requestId\": \"req112\

\"sessionId\": \"94766d3c-8b39-42d3-8596-98aee3ccbefb\ \"consumerId\": \"955dbd1325334a84972b0f3ac19de4f7_2\ \"command\": \"version\ \"execTimeout\": \"10000\"}

docker使⽤,很多时候,应⽤在docker⾥出现arthas⽆法⼯作的问题,是因为应⽤没有安装 JDK ,⽽是安装了 JRE 。如果只安装了 JRE,则会缺少很多JAVA的命令⾏⼯具和类库,Arthas也没办法正常⼯作,可以使⽤公开的JDK镜像和包管理软件来安装这两种⽅式在Docker⾥使⽤JDK。

# 选择需要监控应⽤的进程编号,回车后Arthas会attach到⽬标进程上,并输出⽇志:docker exec -it arthas-demo /bin/sh -c \"java -jar /opt/arthas/arthas-boot.jar\"# 甚⾄我们可以直接把arthas放到容器镜像⽂件中:

Arthas Spring Boot Starter:应⽤启动后,spring会启动arthas,并且attach⾃⾝进程。

com.taobao.arthas

arthas-spring-boot-starter ${arthas.version}

⾮spring boot应⽤使⽤⽅式

com.taobao.arthas arthas-agent-attach ${arthas.version}

com.taobao.arthas arthas-packaging ${arthas.version}

import com.taobao.arthas.agent.attach.ArthasAgent;

public class ArthasAttachExample {

public static void main(String[] args) { ArthasAgent.attach(); }}

到此这篇关于Java线上问题排查神器Arthas实战分析的⽂章就介绍到这了,更多相关Java线上问题排查神器Arthas内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- igat.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务