Netty技术Spring技术Java技术

Java-debug-tool

2019-06-01  本文已影响57人  一字马胡

Java-debug-tool

Java-debug-tool解决什么问题

Java-debug-tool是为了解决日常问题排查的痛点而设计的,问题排查分成两个主要阶段,问题定位和问题修复,问题定位是说找到问题的原因,问题修复是说将问题解决,使得系统恢复正常运行。
对于问题定位来说,我们的需求是:

本质上,问题定位的需求是实现单步调试,因为这样是最容易发现问题出在什么地方的,但是对于java来说,单步调试技术会停顿整个JVM,所以只能在测试的时候使用这种技术,对于生产环境来说就不能使用了,所以对于线上问题排查来说,基本可以不用考虑单步调试,但是如果集群有流量摘除等功能的话,倒是可以使用;java-debug-tool解决了这个问题,可以模拟单步调试的同时不会停顿正在运,使用行的JVM,下面会介绍Java-debug-tool到底实现了一些什么功能。

找到了问题出现的原因,接着就是问题修复,问题修复最大的痛点其实是恢复生产,对于java来说,恢复生产意味着需要重启JVM,这样就会造成问题修复时间变长,Java-debug-tool为此提供了技术支持Java Instrumentation技术,可以在运行时的JVM中替换类的字节码,实现热修复。


Java-debug-tool不能解决什么问题


如何使用

Java-debug-tool使用Java开发,下面介绍如何使用Java-debug-tool进行问题排查;

    <!-- dynamic debug bean -->
    <bean id = "javaDebugInitializer" class="io.javadebug.spring.JavaDebugInitializer" factory-method="initializer" destroy-method="destroy" lazy-init="false"/>
./javadebug-agent-launch.sh PID

这样就可以在目标JVM上启动一个tcp服务,默认地址为:127.0.0.1:11234,如果你想要指定其他的地址,可以使用下面的命令:

./javadebug-agent-launch.sh PID@IP:PORT

之后就可以在IP:PORT启动tcpServer,attach到目标JVM上之后,就可以连接目标JVM进行动态调试了,连接到目标JVM只需要执行下面的命令即可:

 ./javadebug-client-launch.sh

默认就是连接 127.0.0.1:11234,如果attach目标JVM的时候指定的地址不是这个,需要显示指定地址:

 ./javadebug-client-launch.sh IP:PORT


命令详解

Java-debug-tool目前支持的命令不多,下面分别介绍一下当前支持的核心命令。首先介绍一下命令输出界面信息介绍:

---------------------------------------------------------------------------------------------
命令              :mt
命令执行Round       :1
客户端ID           :10000
客户端类型           :client:1
协议版本            :version:1
命令耗时            :179 (ms)
STW时间           :45 (ms)
---------------------------------------------------------------------------------------------
[ReturnTest.getIntVal] with params
[1]
[0 ms] (37)
[0 ms] (43) [startTime = 1559358148073]
[0 ms] (44) [strTag = the return/throw line test tag]
[0 ms] (45)
[0 ms] (47)
[0 ms] (51)
[3 ms] (52) [paramModel = 1.1]
[0 ms] (53)
return value:[101]  at line:53 with cost:5 ms

---------------------------------------------------------------------------------------------

每个输出字段都介绍一下:

字段 含义
命令 本次输出执行的命令是什么 就是你输入的命令名称
命令执行Round 这个调试客户端和目标JVM交互了几次 交互次数
客户端ID 每个客户端首次连接服务端都会被分配一个ContextId,后续的交互都需要将这个ID带上 唯一ID
客户端类型 这是一个保留字段,Java-debug-tool认为第一个连接到目标JVM的调试客户端应该是一个Master Client,权限最高
协议版本 防伪,只有是从服务端拿到的协议才能继续交互
命令耗时 命令的执行耗时,从命令输入处理开始计算,到命令结果展示出来结束,所以是客户端耗时 + 服务端耗时
STW时间 动态增强字节码涉及到JVM字节码替换,会造成STW,这个时间就记录到底STW了多长时间,这个时间会比实际STW的时间长,只是一个粗略的时间 如果一个方法被一个client增强过了,后续的client就不能增强了,除非增强该方法的client退出,其他client才能继续增强;同时,一个client增强过的方法,其他client可以共享

接着就是具体方法的执行路径信息,比如上面这个例子,说明本次观察的方法执行是 "ReturnTest.getIntVal",方法入参是1,方法执行路径是37-43-44-45-47-51-52-53,最终从53行退出,其中第52行耗时3ms,其他行耗时小于1ms,所以无法收集到,最终方法的执行结果是101,本次方法耗时5ms,并且可以看到43、44、52行都有变量赋值信息,格式为 varName = varVal.toString(),需要注意的是,varName可能是错误的,但是varVal是正确的,如果有多个,按照赋值顺序展示;这是方法正常返回的结果展示,下面看一个方法抛出异常的结果展示:

---------------------------------------------------------------------------------------------
命令              :mt
命令执行Round       :1
客户端ID           :10001
客户端类型           :client:0
协议版本            :version:1
命令耗时            :75 (ms)
STW时间           :0 (ms)
---------------------------------------------------------------------------------------------
[ReturnTest.getIntVal] with params
[7]
[0 ms] (37)
[0 ms] (43) [startTime = 1559358921527]
[0 ms] (44) [strTag = the return/throw line test tag]
[0 ms] (45)
[0 ms] (47)
[0 ms] (51)
[0 ms] (54)
[0 ms] (59)
[0 ms] (73) [paramModel = ParamModel{intVal=0, doubleVal='0.0'}]
[0 ms] (74)
[0 ms] (75)
[0 ms] (76) [subVal = 200]
[0 ms] (78)
[0 ms] (82)
throw exception:[java.lang.IllegalStateException: error occ with in:7]  at line:82 with cost:0 ms

---------------------------------------------------------------------------------------------

可以看到本次方法执行路径,参数为7,在82行抛出了异常,其他信息和正常返回时类似,就不做过多解释了。

methodTrace命令

就像命令名称一样,这个命令是用于观察方法执行路径的,可以使用mt来替代命令,该命令参数较多,但是大部分都是可选的,下面先介绍每一个参数的含义,然后再介绍如何实现具体的功能。

命令基本格式:
mt -c <class> -m <method>

可选参数:

下面根据上面的参数来实现不同的观察功能,首先是用于测试的Java类:

public class ReturnTest {

    private TestClass testClass = new TestClass();

    public static void say(int a) {
        int b = a * 10;
        System.out.println("hello:" + b);
        //return b;
    }

    public int getIntVal(int in) {
//        if (in < 7) {
//            System.out.println("in < 7, return");
//            throw new UnsupportedOperationException("test");
//        }

        if (in == 5) {
            String msg = null;
            // produce npe
            in += msg.length();
        }

        long startTime = System.currentTimeMillis() + fibonacci(2);
        String strTag = "the return/throw line test tag";
        if (in < 0) {
            return strTag.charAt(0);
        } else if (in == 0) {
            return 1000;
        }
        // > 0
        if (in < 2) {
            double dbVal = 1.1;
            return (int) (dbVal + 100);
        } else if (in == 2) {
            float fVal = 1.2f;
            return (int) (fVal + 200);
        }
        // > 2
        if (in % 2 == 0) {
            Random random = new Random();
            int rdm = random.nextInt(100);
            if (rdm >= 50) {
                throw new IllegalArgumentException("npe test");
            } else if (rdm <= 20) {
                throw new NullPointerException("< 20");
            }
            // end time
            long end = System.currentTimeMillis();
            long cost = startTime - end;
            int ret = testClass.test(in);
            return (int) (rdm * 10 + ret + (cost / 1000));
        } else {
            ParamModel paramModel = new ParamModel();
            paramModel.setIntVal(in);
            paramModel.setDoubleVal(1.0 * in);
            int subVal = getSubIntVal(paramModel);

            if (subVal == 100) {
                throw new IllegalArgumentException("err occ with in:" + subVal);
            }

            throw new IllegalStateException("error occ with in:" + in);
        }
    }

    /**
     *  不支持递归函数
     *
     * @param n
     * @return
     */
    public int fibonacci(int n) {
        if (n < 0) {
            return -1;
        }
        if (n == 0) {
            return 0;
        }
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    public int getSubIntVal(ParamModel paramModel) {
        if (paramModel == null) {
            return -1;
        }
        if (paramModel.getIntVal() <= 0) {
            return (int) paramModel.getDoubleVal();
        } else if (paramModel.getIntVal() <= 5) {
            return 100;
        } else if (paramModel.getIntVal() <= 8) {
            return 200;
        } else {
            throw new RuntimeException("ill");
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            private Random random = new Random();
            private ReturnTest returnTest = new ReturnTest();

            @Override
            public void run() {
                while (true) {
                    try {
                        System.err.println(returnTest.getIntVal(random.nextInt(10)));
                        TimeUnit.MILLISECONDS.sleep(5);
                    } catch (Exception e) {
                        //e.printStackTrace();
                        //System.out.println("error:" + e.getMessage());
                    }
                }
            }
        }).start();
    }
}

public class TestClass {

    Aa aa = new Aa();

    public int test(int in) {

        if (in == 5) {
            return 100;
        }
        String tag = "the in:" + in;
        if (in < 5) {
            in += 2;
        } else {
            in -= 1;
        }

        if (in > 5) {
            throw new IllegalArgumentException("must <= 5");
        }
        if (in <= 3) {
            throw new NullPointerException("must >= 3");
        }

        return in * 100;
    }

}
观察一次方法调用执行路径

上面的图片展示了观察一次 "ReturnTest.getIntVal"方法调用的执行路径,本次方法入参是2,返回值是201,是从56行代码退出的,耗时1ms;

观察方法异常退出执行路径

当然,如果你想要观察的是某种特定的异常,可以指定-e参数:

观察指定的异常 观察特定输入 录制方法请求

录制完成后可以回放请求:

观察回放请求 观察特定参数-1 观察特定参数-2

redefineClass命令

redefineClass命令用于替换一个类的字节码,可以简写成rdf,用于快速恢复生产环境,命令的参数没有mt命令复杂,但是需要有几点需要注意:

我们把getIntVal方法的开始部分的注释去掉,也就是:

//        if (in < 7) {
//            System.out.println("in < 7, return");
//            throw new UnsupportedOperationException("test");
//        }

这一段内容,去掉之后,只要输入的参数小于7,那么就会抛出异常,我们使用mt命令配合custom来验证我们的rdf结果:

执行rdf命令

可以看到,此时输入参数为5的时候抛出了那个期望的异常;

rollback命令

rollback命令用于将一个增强过的类恢复到初始状态,可以使用back简写,目前仅支持恢复到初始状态,后续会记录增强stage,然后恢复到上一次增强过的字节码:

回滚一个类

findClass命令

是不是曾经出现过因为jar包冲突导致的类加载错误的情况呢?findClass命令用于快速判断一个类是不是在目标JVM加载了,如果加载了,是从哪个jar包中加载的(jar一般都会有版本号,可以看看是不是从期望的jar版本中加载的),是被什么类加载器加载的,还可以仅仅使用类名(不含包名)来匹配,甚至通过正则表达式来匹配,可以使用简写fc:

查找类信息

help命令

如果你不知道怎么使用一个命令,那么可以试试help命令:

help命令

如何重复发生上一次发送的命令

有时候需要重复上一次输入的命令,但是上一次命令输入内容很多,如何快速实现上一次命令的复制呢?下面的一些字符可以快速帮你搞定这件事情:"p","r","s","go","last"

重复命令发送

后续将支持命令历史记录回放的功能,目前仅支持回放上一次输入。

规划中的功能

上一篇 下一篇

猜你喜欢

热点阅读