• BTrace学习总结


    一、简介:

    在生产环境中经常遇到格式各样的问题,如OOM或者莫名其妙的进程死掉。一般情况下是通过修改程序,添加打印日志;然后重新发布程序来完成。然而,这不仅麻烦,而且带来很多不可控的因素。有没有一种方式,在不修改原有运行程序的情况下获取运行时的数据信息呢?如方法参数、返回值、全局变量、堆栈信息等。Btrace就是这样一个工具,它可以在不修改原有代码的情况下动态地追踪java运行程序,通过hotswap技术,动态将跟踪字节码注入到运行类中,对运行代码侵入较小,对性能上的影响可以忽略不计。

    在下列情况时可以使用BTrace进行分析:

    1、接口性能变慢,分析每个方法的耗时情况;

    2、当在Map中插入大量数据,分析其扩容情况;

    3、分析哪个方法调用了System.gc(),调用栈如何;

    4、执行某个方法抛出异常时,分析运行时参数;

    5、..................

    二、安装:

    1、安装JDK;

    2、下载BTrace的压缩包,这里使用的是BTrace 1.3.11版本,可以到下面地址下载:

    http://www.voidcn.com/link?url=https://github.com/btraceio/btrace/releases/tag/v1.3.11

    3、将BTrace包解压,在系统的环境变量上添加变量BTRACE_HOME,并设置其路径为BTrace的路径,同时在PATH变量中添加上路径%BTRACE_HOME%in;

    4、编辑%BTRACE_HOME%intrace.bat文件,将其中的-Dcom.sun.btrace.unsafe=false改为-Dcom.sun.btrace.unsafe=true

    5、btrace命令的语法说明:

    btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]

    1)没有这个表明跳过预编译;

    2)include-path:指定用来编译脚本的头文件路径(关于预编译可参考例子ThreadBean.java)

    3)port:btrace agent端口,默认是2020

    4)classpath:编译所需类路径,一般是指btrace-client.jar等类所在路径;

    5)pid:java进程id

    6)btrace-script:btrace脚本可以是.java文件,也可以是.class文件;

    7)args:传递给btrace脚本的参数, 在脚本中可以通过$(), $length()来获取这些参数(定义在BTraceUtils)

    三、Demo

    (一)JavaSE应用Demo

    1、编写测试功能实现类:

    public class Calculator {

        public int add(int a, int b) {

            try {

                Thread.sleep(1000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            return a + b;

        }

    }

    2、编写调用代码:

    public class App

    {

        public static void main( String[] args )

        {

            Calculator calc = new Calculator();

            Random random = new Random();

            while (true) {

                int a = random.nextInt(10);

                int b = random.nextInt(20);

                int c = calc.add(a, b);

                System.out.println(String.format("%d + %d = %d", a, b, c));

            }

        }

    }

    上面的代码无限循环调用Calculator .add方法并输出调用结果;

    3、运行程序,可以看到屏幕上不停的输出各种加法运算的表达式;

    4、编写btrace脚本:

    @BTrace(unsafe = true)

    public class BTraceTest {

        @OnMethod(clazz = "com.ucar.test.Calculator", method = "add", location = @Location(Kind.RETURN))

        public static void traceTest(int a, int b, @Return int sum) {

            println(String.format("%d + %d = %d", a, b, sum));

        }

    }

    @BTrace注解中要加上unsafe=true,否则运行btrace脚本时会因为安全机制导致报错而无法执行脚本;

    @OnMethod注解中的clazz表示要跟踪的类名,method表示要跟踪的方法名称,location表示在什么时候进行拦截;

    5、运行btrace脚本,可以看到前面输出的加法运算表达式也能在这个窗口上输出;

    运行btrace脚本的命令为:

    btrace 3856 BTraceTest.java

    其中3856为刚才运行的java程序的进程ID;

    (二)web应用Demo

    1、新建SpringMVC的web应用程序(参考https://www.cnblogs.com/laoxia/p/9311442.html);

    2、实现Controller:

    @RestController

    @RequestMapping("/btrace")

    public class BTraceController {

        @RequestMapping("/arg1")

        public String arg1(@RequestParam("name") String name) {

            return "hello: " + name;

        }

    }

    3、生成war包并放到tomcat的webapp目录下,启动tomcat,浏览器中打开URL地址:http://localhost:8080/test/btrace/arg1?name=aaaaa,页面上应该能正常打印出“hello: aaaaa”;

    4、编写BTrace脚本:

    @BTrace(unsafe = true)

    public class PrintArgSimple {

        @OnMethod(clazz = "com.ucar.test.controller.BTraceController", method = "arg1", location = @Location(Kind.RETURN))

        public static void anyRead(@ProbeClassName String pcn, // 被拦截的类名

                                   @ProbeMethodName String pmn,  //被拦截的方法名

                                   AnyType[] args  //被拦截的方法的参数值) {

            BTraceUtils.printArray(args);

            BTraceUtils.println("className: " + pcn);

            BTraceUtils.println("MethodName: " + pmn);

            BTraceUtils.println();

        }

    }

    注意:需要在maven的POM文件中引入btrace-client.jar, btrace-boot.jar和btrace_agent.jar三个文件或者直接引入这三个jar包;

    5、运行脚本,然后在浏览器中请求第三步的URL地址,这时候就能看到屏幕上打印出运行过程中的相关信息;

    运行BTrace的命令为:

    btrace 1256 PrintArgSimple.java

    其中1256为这个web应用进程的进程ID

    6、注意:需要将web应用打包成war放到tomcat下运行,如果直接在idea下运行会报错;

    四、拦截时机:

    1、Kind.ENTRY:入口拦截,默认值;

    2、Kind.RETURN:拦截返回值,只有把拦截位置定义为Kind.RETURN,才能获取方法的返回结果@Return和执行时间@Duration

    3、Kind.THROW:发生异常时拦截;

    4、Kind.LINE:拦截某一行,可以监控代码是否执行到指定的位置;

    5、Kind.CALL:分析方法中调用其它方法的执行情况,比如在execute方法中,想获取add方法的执行耗时,必须把where设置成Where.AFTER

    五、技巧:

    1、拦截构造函数:

    指定method = "<init>"即可拦截指定类的构造函数;

    2、拦截同名函数:拦截同名重载方法,只需要在BTrace脚本的方法中声明与之对应的参数即可。

    比如有如下两个同名方法:

    @RequestMapping("/same1")

    public String same(@RequestParam("name") String name) {

        return "hello: " + name;

    }

    @RequestMapping("/same2")

    public User same(@RequestParam("id") int id,

                     @RequestParam("name") String name) {

        return new User(id, name);

    }

    编写如下的btrace脚本即可拦截:

    @OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) {

        BTraceUtils.println("ClassName: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println("name: " + name);

        BTraceUtils.println();

    }

    @OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same")

    public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) {

        BTraceUtils.println("ClassName: " + pcn);

        BTraceUtils.println("MethodName: " + pmn);

        BTraceUtils.println("id: " + id);

        BTraceUtils.println("name: " + name);

        BTraceUtils.println();

    }

    3、拦截返回值:

    指定location=@Location(Kind.RETURN),并且在方法的参数里面加上@Return AnyType result即可接收返回值;

    4、拦截异常:

    @BTrace

    public class PrintOnThrow {    

        @TLS

        static Throwable currentException;

        @OnMethod(

            clazz="java.lang.Throwable",

            method="<init>"

        )

        public static void onthrow(@Self Throwable self) {  // @Self其实就是拦截了this

            currentException = self;

        }

        @OnMethod(

            clazz="java.lang.Throwable",

            method="<init>"

        )

        public static void onthrow1(@Self Throwable self, String s) {        

            currentException = self;

        }

        @OnMethod(

            clazz="java.lang.Throwable",

            method="<init>"

        )

        public static void onthrow1(@Self Throwable self, String s, Throwable cause) {        

            currentException = self;

        }

        @OnMethod(

            clazz="java.lang.Throwable",

            method="<init>"

        )

        public static void onthrow2(@Self Throwable self, Throwable cause) {

            currentException = self;

        }

        @OnMethod(

            clazz="java.lang.Throwable",

            method="<init>",

            location=@Location(Kind.RETURN)

        )

        public static void onthrowreturn() {

            if (currentException != null) {

                // 打印异常堆栈

                BTraceUtils.Threads.jstack(currentException);

                BTraceUtils.println("=====================");

                // 打印完之后就置空

                currentException = null;

            }

        }

    }

    在命令行里运行该脚本,访问相应的接口后,即可输出异常堆栈;即使异常被try catch给隐藏起来了,这个脚本也一样能揪出来。

    5、拦截指定行:

    @BTrace

    public class PrintLine {

        @OnMethod(

                clazz="org.zero01.monitor_tuning.controller.BTraceController",

                method="exception",

                location=@Location(value=Kind.LINE, line=43)  // 拦截第43

        )

        public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) {

            BTraceUtils.println("ClassName: " + pcn);

            BTraceUtils.println("MethodName: " + pmn);

            BTraceUtils.println("line: " + line);

            BTraceUtils.println();

        }

    }

    如果没有任何输出的话,就代表那一行没有被执行到,所以没被拦截。这种拦截某一行的方式,不适用于判断是否有异常,只能单纯用于判断某一行是否被执行了。

    6、拦截复杂参数:

    比如要拦截下面方法的复杂参数类型User:

    @RequestMapping("/arg2")

    public User arg2(User user) {

        return user;

    }

    可以使用下面的btrace脚本拦截:

    @BTrace

    public class PrintArgComplex {

        @OnMethod(

                clazz = "org.zero01.monitor_tuning.controller.BTraceController",

                method = "arg2",

                location = @Location(Kind.ENTRY)

        )

        public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) {

            //print all fields

            BTraceUtils.print("print all fields: ");

            BTraceUtils.printFields(user);

            //print one field

            Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name");

            BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user));

            BTraceUtils.println("ClassName: " + pcn);

            BTraceUtils.println("MethodName: " + pmn);

            BTraceUtils.println();

        }

    }

    7、拦截环境变量:

    @BTrace

    public class PrintJinfo {

        static {

            // 打印系统属性

            BTraceUtils.println("System Properties:");

            BTraceUtils.printProperties();

            // 打印JVM参数

            BTraceUtils.println("VM Flags:");

            BTraceUtils.printVmArguments();

            // 打印环境变量

            BTraceUtils.println("OS Enviroment:");

            BTraceUtils.printEnv();

            // 退出脚本

            BTraceUtils.exit(0);

        }

    }

    8、使用正则表达式拦截:

    @BTrace

    public class PrintRegex {

        @OnMethod(

                // 类名也可以使用正则表达式进行匹配

                clazz = "org.zero01.monitor_tuning.controller.BTraceController",  

                // 正则表达式需要写在两个斜杠内

                method = "/.*/"  

        )

        public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) {

            BTraceUtils.println("ClassName: " + pcn);

            BTraceUtils.println("MethodName: " + pmn);

            BTraceUtils.println();

        }

    }

    六、注意事项:

    1、@ProbeClassName String clazz:此处String不能写为java.lang.String

    2、@OnMethod(clazz="com.alibaba.security.acl.support.PermissionFactory", method="createPermission", type="com.alibaba.security.acl.support.AbstractPermission(java.lang.String,java.lang.String,com.alibaba.security.acl.support.PermissionDefiner)")

    此处得String必须写成java.lang.String

    3、BTrace脚本默认只能本地运行,也就是只能调试本地的Java进程。如果需要在本地调试远程的Java进程的话,是需要自己去修改BTrace源码的;

    4、BTrace脚本在生产环境下可以使用,但是被修改的字节码不会被还原。所以我们需要先在本地调试好BTrace脚本,然后才能放到生产环境下使用。并且需要注意BTrace脚本中不能含有影响性能或消耗资源较多的代码,不然会导致线上的服务性能降低。

    七、其他:

    1、其他命令行工具说明:

    (1) Btracec:用于预编译BTrace脚本,用于在编译时期验证脚本正确性。

    btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>

    参数意义同btrace命令一致,directory表示编译结果输出目录。

    (2) Btracerbtracer命令同时启动应用程序和BTrace脚本,即在应用程序启动过程中使用BTrace脚本。而btrace命令针对已运行程序执行BTrace脚本。

    btracer <pre-compiled-btrace.class> <application-main-class> <application-args>

    参数说明:

    pre-compiled-btrace.class表示经过btracec编译后的BTrace脚本。

    application-main-class表示应用程序代码;

    application-args表示应用程序参数。

    2、方法上的注解:

    (1) @ OnMethod用来指定trace的目标类和方法以及具体位置,被注解的方法在匹配的方法执行到指定的位置会被调用。

    •  "clazz"属性用来指定目标类名,可以指定全限定类名,比如"java.awt.Component",也可以是正则表达式(表达式必须写在"//",比如"/java\.awt\..+/")
    •  "method"属性用来指定被trace的方法.表达式可以参考自带的例子(NewComponent.javaClassload.java,关于方法的注解可以参考MultiClass.java)
    •  有时候被trace的类和方法可能也使用了注解.用法参考自带例子WebServiceTracker.java
    •  针对注解也是可以使用正则表达式,比如像这个"@/com\.acme\..+/ ",也可以通过指定超类来匹配多个类,比如"+java.lang.Runnable"可以匹配所有实现了java.lang.Runnable接口的类.具体参考自带例子SubtypeTracer.java

    (2) @OnTimer定时触发Trace,时间可以指定,单位为毫秒,具体参考自带例子Histogram.java

    (3) @OnErrortrace代码抛异常或者错误时,该注解的方法会被执行.如果同一个trace脚本中其他方法抛异常,该注解方法也会被执行。

    (4) @OnExittrace方法调用内置exit(int)方法(用来结束整个trace程序),该注解的方法会被执行.参考自带例子ProbeExit.java

    (5) @OnEvent用来截获"外部"btrace client触发的事件,比如按Ctrl-C中断btrace执行时,并且选择2,或者输入事件名称,将执行使用了该注解的方法,该注解的value值为具体事件名称。具体参考例子HistoOnEvent.java

    (6) @OnLowMemory当内存超过某个设定值将触发该注解的方法,具体参考MemAlerter.java

    (7) @OnProbe使用外部文件XML来定义trace方法以及具体的位置,具体参考示例SocketTracker1.javajava.net.socket.xml

    3、参数上的注解:

    • @Self用来指定被trace方法的this,可参考例子AWTEventTracer.javaAllCalls1.java
    • @Return用来指定被trace方法的返回值,可参考例子Classload.java
    • @ProbeClassName (since 1.1)用来指定被trace的类名,可参考例子AllMethods.java
    • @ProbeMethodName (since 1.1)用来指定被trace的方法名,可参考例子WebServiceTracker.java
    • @TargetInstance (since 1.1)用来指定被trace方法内部被调用到的实例,可参考例子AllCalls2.java
    • @TargetMethodOrField (since 1.1)用来指定被trace方法内部被调用的方法名,可参考例子AllCalls1.javaAllCalls2.java

    4、属性上的注解:

    • @Export该注解的静态属性主要用来与jvmstat计数器做关联, 使用该注解之后,btrace程序就可以向jvmstat客户端(可以用来统计jvm堆中的内存使用量)暴露trace程序的执行次数, 具体可参考例子ThreadCounter.java
    • @Property使用了该注解的trace脚本将作为MBean的一个属性,一旦使用该注解, trace脚本就会创建一个MBean并向MBean服务器注册, 这样JMX客户端比如VisualVMjconsole就可以看到这些BTrace MBean, 如果这些被注解的属性与被trace程序的属性关联, 那么就可以通过VisualVMjconsole来查看这些属性了, 具体可参考例子ThreadCounterBean.javaHistogramBean.java
    • @TLS用来将一个脚本变量与一个ThreadLocal变量关联, 因为ThreadLocal变量是跟线程相关的, 一般用来检查在同一个线程调用中是否执行到了被trace的方法, 具体可参考例子OnThrow.javaWebServiceTracker.java

    5、类上的注解:

    • @com.sun.btrace.annotations.DTrace用来指定btrace脚本与内置在其脚本中的D语言脚本关联, 具体参考例子DTraceInline.java
    • @com.sun.btrace.annotations.DTraceRef用来指定btrace脚本与另一个D语言脚本文件关联, 具体参考例子DTraceRefDemo.java
    • @com.sun.btrace.annotations.BTrace用来指定该java类为一个btrace脚本文件。

    6、BTrace文件下的samples文件夹下包含了很多的示例,这些示例说明如下:

    AWTEventTracer.java -演示了对EventQueue.dispatchEvent()事件进行trace的做法,可以通过instanceof来对事件进行过滤,比如这里只针对focus事件trace.

    AllLines.java -演示了如何在被trace的程序到达probe指定的类和指定的行号时执行指定的操作(例子中指定的行号是-1表示任意行).

    AllSync.java -演示了如何在进入/退出同步块进行trace.

    ArgArray.java -演示了打印java.io包下所有类的readXXX方法的输入参数.

    Classload.java -演示打印成功加载指定类以及堆栈信息.

    CommandArg.java -演示如何获取btrace命令行参数.

    Deadlock.java -演示了@OnTimer注解和内置deadlock()方法的用法

    DTraceInline.java -演示@DTrace注解的用法

    DTraceDemoRef.java -演示@DTraceRef注解的用法.

    FileTracker.java -演示了如何对File{Input/Output}Stream构造函数中初始化打开文件的读写文件操作进行trace.

    FinalizeTracker.java -演示了如何打印一个类所有的属性,这个在调试和故障分析中非常有用.这里的例子是打印FileInputStream类的close() /finalize()方法被调用时的信息.

    Histogram.java -演示了统计javax.swing.JComponent在一个应用中被创建了多少次.

    HistogramBean.java -同上例,只不过演示了如何与JMX集成,这里的map属性通过使用@Property注解被暴露成一个MBean.

    HistoOnEvent.java -同上例,只不过演示了如何在通过按ctrl+c中断当前脚本时打印出创建次数,而不是定时打印.

    JdbcQueries.java -演示了聚合(aggregation)功能.关于聚合功能可参考DTrace.

    JInfo.java -演示了内置方法printVmArguments(), printProperties()printEnv()的用法

    JMap.java -演示了内置方法dumpHeap()的用法.即将目标应用的堆信息以二进制的形式dump出来

    JStack.java -演示了内置方法jstackAll()的用法,即打印所有线程的堆栈信息.

    LogTracer.java -演示了如何深入实例方法(Logger.log)并调用内置方法(field() )打印私有属性内容.

    MemAlerter.java -演示了使用@OnLowMememory注解监控内存使用情况.即堆内存中的年老代达到指定值时打印出内存信息.

    Memory.java -演示每隔4s打印一次内存统计信息.

    MultiClass.java -演示了通过使用正则表达式对多个类的多个方法进行trace.

    NewComponent.java -使用计数器每隔一段时间检查当前应用中创建java.awt.Component的个数.

    OnThrow.java -当抛出异常时,打印出异常堆栈信息.

    ProbeExit.java -演示@OnExit注解和内置exit(int)方法的用法

    Profiling.java -演示了对profile的支持. //我执行没成功, BTrace内部有异常

    Sizeof.java -演示了内置的sizeof方法的使用.

    SocketTracker.java -演示了对socketcreation/bind方法的trace.

    SocketTracker1.java -同上,只不过使用了@OnProbe.

    SysProp.java -演示了使用内置方法获取系统属性,这里是对java.lang.SystemgetProperty方法进行trace.

    SubtypeTracer.java -演示了如何对指定超类的所有子类的指定方法进行trace.

    ThreadCounter.java -演示了在脚本中如何使用jvmstat计数器. (jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count需要这样来从外部通过jstat来访问)

    ThreadCounterBean.java -同上,只不过使用了JMX.

    ThreadBean.java -演示了对预编译器的使用(并结合了JMX).

    ThreadStart.java -演示了脚本中DTrace的用法.

    Timers.java -演示了在一个脚本中同时使用多个@OnTimer

    URLTracker.java -演示了在每次URL.openConnection成功返回时打印出url.这里也使用了D语言脚本.

    WebServiceTracker.java -演示了如何根据注解进行trace.

    7、参考文档:

    http://huanghaifeng1990.iteye.com/blog/2121419

    http://agapple.iteye.com/blog/962119

    http://agapple.iteye.com/blog/1005918

  • 相关阅读:
    centos7.x网卡bond配置
    twemproxy源码解析系列三Twemproxy配置文件解析及相关组件初始化过程
    twemproxy源码解析系列二关键数据结构分析
    Nginx 变量漫谈(一)(转)
    twemproxy源码解析系列一特性及启动流程分析
    理解 Linux 的处理器负载均值
    man命令使用
    awk 查找文件中数字 字符串 email
    非阻塞socket调用connect, epoll和select检查连接情况示例
    Mysql日期类型大小比较拉取给定时间段的记录
  • 原文地址:https://www.cnblogs.com/laoxia/p/9773319.html
Copyright © 2020-2023  润新知