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 urrent.TimeUnit;
@SpringBootApplication
public 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 arthas.aliyun/arthas-boot.jar
# 启动arthas-boot.jar,必须启动⾄少⼀个 java程序,否则会⾃动退出。运⾏此命令会⾃动发现 java进程,输⼊需要 attach 进程对应的序列号,例如,输⼊1按回车则会监听该进程。
java -jar arthas-boot.jar
# ⽐如输⼊JVM (jvm实时运⾏状态,内存使⽤情况等)
CPU占⽤⾼⽰例
package ller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import urrent.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;
@SpringBootApplication
public 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,代码⾏数为19 thread -n 5
# jad查看反编译的代码
jad ller.CpuController
线程死锁⽰例
package ller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import urrent.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演⽰程序,访问页⾯192.168.50.100:8080/thread/test
# 运⾏arthas,查看线程
thread
# 查看阻塞线程
thread -b
# jad反编译查看代码
jad --source-only ller.ThreadController
线上修复热部署
准备⼀个有问题的java类
package ller;
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 urrent.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 ller.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 ller;
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 = Int() / 10000;
List<Integer> idStrs = IdStr(number);
res = printList(number, idStrs);
}
catch (Exception e) {
System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage());        return res;
}
private List<Integer> getIdStr(int number) {
if (number < 5) {
++this.illegalArgumentCount;
throw new IllegalArgumentException("number is: " + number + ", need >= 5");
ArrayList<Integer> result = new ArrayList<Integer>();
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<Integer> 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);
String();
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。