在生产环境中可能经常遇到各种问题,定位问题需要获取程序运行时的数据信息,如方法参数、返回值、全局变量、堆栈信息等。为了获取这些数据信息,我们可以通过改写代码,增加日志信息的打印,再发布到生产环境。通过这种方式,一方面将增大定位问题的成本和周期,对于紧急问题无法做到及时响应;另一方面重新部署后环境可能已被破坏,很难重新问题的场景。
对于程序员来说最头大的问题之一就是线上出了故障了,但是我们无法debug来找出问题原因,同时在上线的时候日志级别限定了我们不可能把所有的细节都打印到log上,这个时候故障都等在哪里,能办的手段无非看源码,通过仔细看代码来找出问题,并编译重新上线解决,这种手段能解决一部分代码,但是对于一些隐藏较深的bug就无能为力了,例如OOM或是频繁的full gc,一般是一个很多的对象没有被释放或是一个对象被频繁的创建调用。在一个复杂的项目中,一个OOM问题可能是一个对象被频繁创建,这种方式指望通过看源码是很难解决的,但是BTrace可以迅速帮助我们定位到问题所在地。
Btrace (Byte Trace)是sun推出的一款java 动态、安全追踪(监控)工具,可以不停机的情况下监控线上情况,并且做到最少的侵入,占用最少的系统资源。BTrace应用较为广泛的原因应该是其安全性和无侵入性,其中热交互技术,使得我们无需启动Agent的情况下动态跟踪分析,其安全性不会导致对目标Java进程的任何破坏性影响,使得BTrace成为我们线上产品问题定位的利器。无侵入性无需我们对原有代码做任何修改,降低上线风险和测试成本,并且无需重启启动目标Java进程进行Agent加载即可动态分析和跟踪目标程序,可以说BTrace可以满足大部分的应用场景。
BTrace主要包含btracec和btracer两个命令编译和启动BTrace脚本:
1. btrace
功能: 用于运行BTrace跟踪程序。
命令格式:
btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]
示例:
btrace -cp build/ 1200 AllCalls1.java
参数含义:
include-path指定头文件的路径,用于脚本预处理功能,可选;
port指定BTrace agent的服务端监听端口号,用来监听clients,默认为2020,可选;
classpath用来指定类加载路径,默认为当前路径,可选;
pid表示进程号,可通过jps命令获取;
btrace-script即为BTrace脚本;btrace脚本如果以.java结尾,会先编译再提交执行。可使用btracec命令对脚本进行预编译。
args是BTrace脚本可选参数,在脚本中可通过"$"和"$length"获取参数信息。
2. btracec
功能: 用于预编译BTrace脚本,用于在编译时期验证脚本正确性。
btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>
参数意义同btrace命令一致,directory表示编译结果输出目录。
3. btracer
功能: btracer命令同时启动应用程序和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表示应用程序参数。
该命令的等价写法为:
java -javaagent:btrace-agent.jar=script=<pre-compiled-btrace-script1>[,<pre-compiled-btrace-script1>]* <MainClass> <AppArguments>
虽然BTrace很强大,但Btrace脚本就是一个普通的用@Btrace注解的Java类,其中包含一个或多个public static void修饰的方法,为了保证对目标程序不造成影响,Btrace脚本对其可以执行的动作做了很多限制,如下:
方法上的注解
参数上的注解
非注解的方法参数
未使用注解的方法参数一般都是用来做方法签名匹配用的,他们一般和被trace方法中参数出现的顺序一致.不过他们也可以与注解方法交错使用,如果一个参数类型声明为*AnyType[]*,则表明它按顺序"通吃"方法所有参数.未注解方法需要与*Location*结合使用:
属性上的注解
类上的注解
1.跟踪WildFly内存信息,用@OnTimer 这个annotation每隔4秒钟打印一次内存堆栈信息:
import com.sun.btrace.annotations.BTrace;
import com.sun.btrace.annotations.OnTimer;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class TraceMemory {
//heapUsage()/nonHeapUsage() – 打印堆/非堆内存信息,包括init、used、commit、max
@OnTimer(4000)
public static void printM(){
//打印内存信息
println("heap:");
println(heapUsage());
println("no-heap:");
println(nonHeapUsage());
}
}
查找 WildFly pid:
[root@mdwareweb1 test]# jps
7119 Jps
5513 jboss-modules.jar
执行:
btrace 5513 TraceMemory.java
结果:
[root@mdwareweb1 test]# btrace 5513 TraceMemory.java
heap:
init = 67108864(65536K) used = 70036960(68395K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43569672(42548K) committed = 78348288(76512K) max = 318767104(311296K)
heap:
init = 67108864(65536K) used = 70893560(69231K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43569672(42548K) committed = 78348288(76512K) max = 318767104(311296K)
heap:
init = 67108864(65536K) used = 70893560(69231K) committed = 209387520(204480K) max = 477233152(466048K)
no-heap:
init = 19136512(18688K) used = 43573768(42552K) committed = 78348288(76512K) max = 318767104(311296K)
2、打印WildFly运行时系统信息:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.BTrace;
@BTrace
public class TraceJInfo {
static{
println("java vm properties:===>");
printVmArguments();
println("System properties:===>");
printProperties();
println("OS properties:===>");
printEnv();
exit();
}
}
执行结果:
[root@mdwareweb1 test]# btrace 5513 TraceJInfo.java
java vm properties:===>
[-D[Standalone], -Xms64m, -Xmx512m, -XX:MaxPermSize=256m, -Djava.net.preferIPv4Stack=true, -Djboss.modules.system.pkgs=org.jboss.byteman, -Djava.awt.headless=true, -Dorg.jboss.boot.log.file=/opt/jboss/wildfly-8.0.0.Final/standalone/log/server.log, -Dlogging.configuration=file:/opt/jboss/wildfly-8.0.0.Final/standalone/configuration/logging.properties]
System properties:===>
org.jboss.security.context.ThreadLocal = true
java.vm.version = 23.25-b01
sun.jnu.encoding = UTF-8
java.vendor.url = http://java.oracle.com/
java.vm.info = mixed mode
org.apache.xml.security.ignoreLineBreaks = true
jboss.server.name = mdwareweb1
java.awt.headless = true
user.dir = /opt/jboss/wildfly-8.0.0.Final/bin
sun.cpu.isalist =
logging.configuration = file:/opt/jboss/wildfly-8.0.0.Final/standalone/configuration/logging.properties......
3、排查ClassNotFoundException
写过Java代码的读者估计都碰到过ClassNotFoundException/NoClassDefFoundError/NoSuchMethodException(还有一个常见的ClassCastException就不在这里说了)。当碰到ClassNotFoundException/NoClassDefFound时,如果很确定这个class应该是从哪个路径装载的,则可以去相应的路径找下是否有对应的class文件存在,例如web应用通常会在*.war(ear)/WEB-INF/lib或classes目录下,对于lib下的jar包,可通过写个小脚本jar -tvf的方式找找;如不确定class是从哪装载的,则可以先看看日志里是否有堆栈信息,如果有的话则可以看到具体是哪个ClassLoader实现在装载class,之后则可以通过www.grepcode.com或jar包反编译看看具体是从哪装载的class;如果日志中没有,则可以用btrace来跟踪下抛出以上两个异常的堆栈信息,btrace脚本类似如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
@OnMethod(
clazz="java.lang.ClassNotFoundException",
method="<init>"
)
public static void traceExecute(){
jstack();
}
}
拿到堆栈信息后,可以继续使用上面的方法进行排查,在确认了class装载的位置后,则可将相应的class/jar加上即可。
4、排查OutOfMemoryError
尽管JVM是自动管理内存的分配和回收的,但Java程序员们还是会经常碰到各种各样的内存问题。最常见的第一个问题是java.lang.OutOfMemoryError,估计写Java的读者都碰到过。在日志中可能会看到java.lang.OutOfMemoryError: Unable to create new native thread,可以先统计下目前的线程数(例如ps -eLf | grep java -c),然后可以看看ulimit -u的限制值是多少,如线程数已经达到限制值,如限制值可调整,则可通过调整限制值来解决;如不能调限制值,或者创建的线程已经很多了,那就需要看看线程都是哪里创建出来的,同样可通过btrace来查出是哪里创建的,脚本类似如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace public class Trace{
@OnMethod(
clazz="java.lang.Thread",
method="start"
)
public static void traceExecute(){
jstack();
}
}
以下是笔者编写的一个demo,主要用于测试OOM,当然也会对BTrace的功能进行介绍。
首先,笔者编写了两个用于测试的类,如下:
StartObject.java
import java.util.Random;
public class StartObject {
private static int totalTime = 0;
public int work(int sleepTime) throws InterruptedException{
System.out.println("sleep " + sleepTime);
totalTime += sleepTime;
return totalTime;
}
}
HeapOOM.java
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class HeapOOM {
public static void main(String agrs[]) throws InterruptedException{
Random random = new Random();
List<Integer> list = new ArrayList<Integer>();
Thread.sleep(10000);
while(true){
int sleepTime = random.nextInt(1000);
list.add(new StartObject().work(sleepTime));
}
}
}
由于笔者将运行时jvm参数设置为:-Xms10m -Xmx10m
因此程序会因为HeapOOM.java中红色一行代码不断创建对象而发生OutOfMemoryError异常。
以下,我们便通过StartObject这个类来查找是在哪不断新建对象并调用其work方法,于是编写BTrace脚本如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace
public class TraceObject {
@OnMethod(clazz = "StartObject", method = "work", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))
public static void checkWhoCallMe() {
println("check who ActionObject.work method:");
jstack();
}
}
通过jps查找到HeapOOM执行的pid
执行:btrace [pid] TraceObject.java
在终端我们看到HeapOOM类中执行StratObject.work方法,如此即可找到我们想要的执行对象,从而可以去修改。
若是得到work方法的执行时间的话,脚本如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace
public class TraceObjectCost {
@TLS
static long startTime;
@OnMethod(clazz = "StartObject", method = "work", location = @Location(Kind.RETURN))
public static void start() {
startTime = timeMillis();
}
@OnMethod(clazz = "StartObject", method = "work", location = @Location(Kind.RETURN))
public static void getMethodExecuteCost(int sleepTime,@Return int totalTime) {
String str = str(timeMillis() - startTime);
String strcat = strcat("execute work method cost:", str);
String strcat2 = strcat(strcat, " ms");
println(strcat2);
}
}
若是期望的到一个method哪几行被执行了的话,脚本如下:
import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;
@BTrace
public class TraceObjectLineCall {
@OnMethod(clazz="StartObject",method="work",location=@Location(value=Kind.CALL, clazz="/.*/", method="/.*/"))
public static void lineCall(@Self StartObject self, @TargetMethodOrField String method, @ProbeMethodName String probeMethod){
println(Strings.strcat(method, Strings.strcat(" in ", probeMethod)));
}
}
虽然 Btrace 在关键时候能起到迅速排查问题的作用,但我个人感觉,这还是不到万不得已才使用的好。首先,我们代码上线前,应该充分 review ,充分和相关方进行沟通,以避免不必要的问题发生。其次,我们应该养成记 log 的良好习惯。遇到问题,如果有相关日志可以排查,是最方便的,同时也是最安全,成本最低的一种排查方法。最后,我们可以结合 btrace 和 jdk 自带的 tool 来排查问题,比如 jstack 、 jstat 等等,快速的定位问题。
http://kenai.com/projects/btrace/pages/UserGuide
http://learnworld.iteye.com/blog/1402763
http://inter12.iteye.com/blog/1759882
http://www.cnblogs.com/serendipity/archive/2012/05/14/2499840.html
相关实例说明
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 -演示了对socket的creation/bind方法的trace.
SocketTracker1.java -同上,只不过使用了@OnProbe.
SysProp.java -演示了使用内置方法获取系统属性,这里是对java.lang.System的getProperty方法进行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.