记一次使用arthas线上问题排查

背景介绍

    项目中有使用到com.github.dreamroute excel-helper这个工具来辅助Excel文件的解析,出错时的代码是这样写的:如下所示(非源代码)

        try {
            excelDTOS = ExcelHelper.importFromFile(ExcelType.XLSX, file, ExcelDTO.class);
        } catch (Exception e) {
            log.error("ExcelHelper importFromFile exception msg {}", e.getMessage());
        }

因为打印异常信息时,使用了e.getMessage()方法,没有将异常信息打印出来。而且本地复现也没有复现出来。所以只能考虑使用arthas来协助排查这个问题了。

排查过程

1、线上服务器安装arthas。https://alibaba.github.io/arthas/getstatic.html
2、使用watch命令监控指定方法,打印出异常的堆栈信息,命令如下:

watch com.github.dreamroute.excel.helper.ExcelHelper importFromFile '{params,throwExp}' -e -x 3

再次调用方法,捕获到异常栈信息如下:

image.png

已经捕获到异常,并打印出堆栈信息。
3、根据对应的堆栈信息,定位到具体的代码,如下:
image.png

    代码很简单,从代码中可以很清晰的看到如果没有从headerInfoMap中没有获取到指定的headerInfo,就会抛这个异常。没有找到只有两种情况:
    1、headerInfoMap中保存的信息不对。
    2、cell中的columnIndex超出的正常的范围导致没有获取到对应的HeaderInfo。
    对于第二种情况,首先去校验了一下上传的Excel文件是否有问题,本地测试了一下Excel文件,没有任何问题。本地测试也是成功的,所以主观判断,第二种情况的可能性不大。
    所以说主要检查第一种情况是否发生,这个时候可以再去看一下该方法的第一行代码

Map headerInfoMap = processHeaderInfo(rows,cls);

    可以看到headerInfoMap是通过processHeaderInfo中获取的。找到processHeaderInfo的代码,如下所示。

public static Map proceeHeaderInfo(Iterator rows, Class cls) {
    if (rows.hasNext()) {
        Row header = rows.next();
        return CacheFactory.findHeaderInfo(cls, header);
    }
    return new HashMap<>(0);
}
public static Map findHeaderInfo(Class cls, Row header) {
    Map headerInfo = HEADER_INFO.get(cls);
    if (MapUtils.isEmpty(headerInfo)) {
        headerInfo = ClassAssistant.getHeaderInfo(cls, header);
        HEADER_INFO.put(cls, headerInfo);
    }
    return headerInfo;
}
public static Map getHeaderInfo(Class cls, Row header) {
    Iterator cellIterator = header.cellIterator();
    List fields = ClassAssistant.getAllFields(cls);
    Map headerInfo = new HashMap<>(fields.size());
    while (cellIterator.hasNext()) {
        org.apache.poi.ss.usermodel.Cell cell = cellIterator.next();
        String headerName = cell.getStringCellValue();
        for (Field field : fields) {
            Column col = field.getAnnotation(Column.class);
            String name = col.name();
            if (Objects.equals(headerName, name)) {
                HeaderInfo hi = new HeaderInfo(col.cellType(), field);
                headerInfo.put(cell.getColumnIndex(), hi);
                break;
            }
        }
    }

    return headerInfo;
}

    主要通过CacheFactory类的findHeaderInfo来生成,在findHeaderInfo方法中,通过一个被static final修饰的HEADER_INFO变量来做缓存,被调用时先去HEADER_INFO中查,如果有则直接返回,没有则重新创建(也就说明相同的Excel文件,仅初始化一次HeaderInfo)。创建的步骤在ClassAssistant.getHeaderInfo()方法中。
    简单的看一下HeaderInfo的生成过程,根据Excel文件的第一行中的各个Cell值与自定义实体类的注解比较,如果名字相同,就存为一个键值对(HeaderInfo的数据结构为HashMap)。
4、这个时候需要再确认一下HEADER_INFO中保存的ExcelDTO.class相关的HeaderInfo是怎样的。通过ognl命令或者getstatic命令来查看。这里使用ognl命令

ognl '#value=new com.tom.dto.ExcelDTO(),#[email protected]@HEADER_INFO,#valueMap.get(#value.getClass()).entrySet().iterator.{#this.value.name}'

    结果如下:正常情况下这个Excel文件有6列信息,为什么只产生了4个键值对呢?如果HEADER_INFO中保存了错的,从上面的逻辑来看,后面上传的正确的Excel文件在解析时都会抛错。



5、询问了当时发现这个问题的同事,得知他第一次上传的Excel文件是有问题的,后面想改正,再上传时便出现了问题。到这里问题也算是找到了。
6、经验总结
1、打印异常信息是不要只打印异常信息,要将异常堆栈也打印出来。
2、建议使用阿里开源easyexcel作为Excel解析工具。
3、arthas是一个很好的问题排查工具。

Arthas原理探究

有了实际的使用之后,不免会想到,Arthas是如何做到在程序运行时,动态监测我们的代码的呢?带着这样的问题,我们一起来看下Java Agent技术实现原理。

Java Agent技术

Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是目标JVM启动之时,也可以是在目标JVM运行时进行加载,而在目标JVM运行时进行Agent加载具备动态性。

基础概念
  • JVMTI(JVM Tool Interface):是JVM暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
  • JVMTIAgent(JVM Tool Interface):是一个动态库,利用JVMTI暴露出来的一些接口帮助我们在程序启动时或程序运行时JVM Attach机制,将Agent加载到目标JVM中。
  • JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任。
  • VirtualMachine :提供了Attach动作和Detach动作,允许我们通过attach方法,远程连接到JVM上,然后通过loadAgent方法向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在class加载前改变class的字节码,也可以在class加载后重新加载。
  • Instrumentation:可以在class加载前改变class的字节码(premain),也可以在class加载后重新加载(agentmain)。
执行过程
image.png
动手写一个Demo

通过javassist,在运行时更改指定方法的代码,在方法之前后添加自定义逻辑。


1、定义Agent类。当前Java提供了两种方式可以将代码代码注入到JVM中,这里我们的Demo选择使用agentmain方法来实现。
premain:在启动时通过javaagent命令,将代理注入到指定的JVM中。
agentmain:运行时通过attach工具激活指定代理。

/**
 * AgentMain
 *
 * @author tomxin
 */
public class AgentMain {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException, ClassNotFoundException {
        instrumentation.addTransformer(new InterceptorTransformer(agentArgs), true);
        Class clazz = Class.forName(agentArgs.split(",")[1]);
        instrumentation.retransformClasses(clazz);
    }
}

/**
 * InterceptorTransformer
 *
 * @author tomxin
 */
public class InterceptorTransformer implements ClassFileTransformer {

    private String agentArgs;

    public InterceptorTransformer(String agentArgs) {
        this.agentArgs = agentArgs;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        //javassist的包名是用点分割的,需要转换下
        if (className != null && className.indexOf("/") != -1) {
            className = className.replaceAll("/", ".");
        }
        try {
            //通过包名获取类文件
            CtClass cc = ClassPool.getDefault().get(className);
            //获得指定方法名的方法
            CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[2]);
            //在方法执行前插入代码
            m.insertBefore("{ System.out.println(\"=========开始执行=========\"); }");
            m.insertAfter("{ System.out.println(\"=========结束执行=========\"); }");
            return cc.toBytecode();
        } catch (Exception e) {

        }
        return null;
    }
}

2、使用Maven配置MANIFEST.MF文件,该文件能够指定Jar包的main方法,

    
        
            
                org.apache.maven.plugins
                maven-jar-plugin
                2.3.1
                
                    
                        
                            true
                        
                        
                            com.tom.mdc.AgentMain
                            true
                            true
                        
                    
                
            
        
    

3、定义Attach方法,通过VirtualMachine.attach(#{pid})来指定要代理的类,

import com.sun.tools.attach.VirtualMachine;

import java.io.IOException;

/**
 * AttachMain
 *
 * @author tomxin
 */
public class AttachMain {
    public static void main(String[] args) {
        VirtualMachine virtualMachine = null;
        try {
            virtualMachine = VirtualMachine.attach(args[0]);
            // 将打包好的Jar包,添加到指定的JVM进程中。
            virtualMachine.loadAgent("target/agent-demo-1.0-SNAPSHOT.jar",String.join(",", args));
        } catch (Exception e) {
            if (virtualMachine != null) {
                try {
                    virtualMachine.detach();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

3、定义测试的方法

package com.tom.mdc;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * PrintParamTarget
 *
 * @author toxmxin
 */
public class PrintParamTarget {

    public static void main(String[] args) {
        // 打印当前进程ID
        System.out.println(ManagementFactory.getRuntimeMXBean().getName());
        Random random = new Random();
        while (true) {
            int sleepTime = 5 + random.nextInt(5);
            running(sleepTime);
        }
    }

    private static void running(int sleepTime) {
        try {
            TimeUnit.SECONDS.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("running sleep time " + sleepTime);
    }
}

你可能感兴趣的:(记一次使用arthas线上问题排查)