动态追踪技术是一个可以不用重启线上 java 项目来进行问题排查的技术,比如前面讲的 Arthas 就属于一种动态追踪的工具。它里面提供的 monitor 还有 watch 等命令就是动态的追踪技术。 当然我们学技术要知其然还要知其所以然,Arthas 工具的基础,就是 Java Agent 技术,可以利用它来构建一个附加的代理程序,用来协助检测性能,还可以替换一些现有功能,甚至 JDK 的一些类我们也能修改,有点像 JVM 级别的 AOP 功能。
Java Agent 技术
既然作为 JVM 的 AOP,就必须要有 AOP 的功能,所以 Java Agent 提供了两个类似于 AOP 的方法:
一个方法叫做 premain 方法,可以在 main 运行之前的进行一些操作(Java 入口是一个 main 方法)。
一个是 agentmain 方法,是控制类运行时的行为(Arthas 使用的就是这种)。
但在一个 JVM 中,只会调用一个。
怎么玩
已有一个项目案例 app:
要构建一个 agent 程序,大体可分为以下步骤:
使用字节码增强工具,编写增强代码;
在 manifest 中指定 Premain-Class/Agent-Class 属性;
使用参数加载或者使用 attach 方式改变 app 项目中的内容。
编写 Agent
Java Agent 体现方式是一个 jar 包,使用 IDEA 创建一个默认的 maven 工程即可。
加入 maven 依赖,我们借用 javassist 完成字节码增强:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>d>javassist</artifactId>
<version>3.24.1-GA</version>
</dependency>
创建一个普通的 Java 类,添加 premain 或者 agentmain 方法,它们的参数完全一样。 示例中使用的是 AgentApp:
Transformer --变形金刚,有一句话叫做:穷人靠变异 富人靠科技。Agent 就靠这个来变异,所以这个就是核心方法。
编写 Transformer
假如我们要统计某个方法的执行时间,使用 JavaAssist 工具来增强字节码。
比如就是 app 项目中 MainRun 类的 hello 方法。
那么步骤如下:
代码逻辑需要实现 ClassFileTransformer 接口,编写一个 Agent 类实现,然后在 transform 方法中实现以下逻辑:
获取 MainRun 类的字节码;
获取 hello 方法的字节码;
在方法前后,加入时间统计,首先定义变量 _begin,然后追加要写的代码;
最后把字节码返回。
打包 Agent
MANIFEST.MF 文件(让外界知晓的)
具体路径在 src/main/resources/META-INF/MANIFEST.MF:
maven 打包会覆盖这个文件,所以我们需要为它指定一个,用 manifestFile 来指定你的 MANIFEST.MF 文件。
在命令行,执行 mvn install 安装到本地代码库。
得到 Agent 的 jar 包。
使用
使用方式取决于你使用的 premain 还是 agentmain。
Premain:
直接命令行中加入参数即可,在 jvm 启动时启用代理。
java -javaagent:agent.jar MainRun
在 IDEA 中,可以将参数配置 jvm options 里。
执行后,直接输出 hello world。通过增强以后,还额外的输出了执行时间,以及一些 debug 信息。其中,debug 信息在 main 方法执行之前输出。
Agentmain
这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:
获取机器上运行的所有 JVM 进程 ID;
选择要诊断的 jvm;
将 jvm 使用 attach 函数链接上;
使用 loadAgent 函数加载 agent,动态修改字节码;
卸载 jvm。
Java Attach API
Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Attach API 只有 2 个主要的类,都在 com.sun.tools.attach 包(在 jdk 的 lib目录下 tools.jar 里面)里面:
VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ;
VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。
Java Attach API 是一个 API 接口,JDK 提供的,它可以将应用程序连接到另一个目标虚拟机。然后,您的应用程序可以将代理应用程序装入目标虚拟机,例如,用于执行监视状态之类的任务。
JVM Attach API 功能上非常简单,主要功能如下:
Attach 到其中一个 JVM 上,建立通信管道;
让目标 JVM 加载 Agent。
使用入门:
不过我在 IDEA 使用它的时候,要注意到,这个 API 属于 JDK 的包,所以使用它项目中必须要引用它。
使用案例如下:
1、首先项目中引用到 tools.jar(这个包在 JDK 的安装目录的 lib 下面)
1、JVM 进程号,通过 jps 命令获取;
2、使用 VirtualMachine.attach()向目标 JVM ”附着” ;
3、利用 VirtualMachine 和 VirtualMachineDescriptor 就可以获取 VM 相关的信息。
但是问题来了,我们的思路上不单单应该是修改字节码,同时还有一个问题,我们只是达到了修改目标应用字节码的目标。
这个就类似于你安装了间谍到敌方,但是间谍在敌方宣传我方思想,这个是很明显的暴露,要怎么做到我安插的间谍只给我传输数据。其实很简单,还是进行字节码的增强,只是不在敌方打印,只在把相关信息获取出来再传递给我方就可以了。
Arthas 它里面提供的 monitor 还有 watch 等命令本质上就是通过这种字节码修改的方式,在目标应用中把相关的参数统计出来,然后再传递到 Arthas 中即可,当然这个里面会涉及到数据的传递问题(这里不做深入的探讨)。
借助 Btrace 手写动态追踪框架
BTrace 是什么
什么是 BTrace 呢?BTrace 已经开源,项目描述极其简短:
A safe, dynamic tracing tool for the Java platform. BTrace 是基于 Java 语言的一个安全的、可提供动态追踪服务的工具。BTrace 基于 ASM、Java Attach API、Instrument 开发,为用户提供了很多注解。依靠这些注解,我们可以编写 BTrace 脚本(简单的 Java 代码)达到我们想要的效果,而不必深陷于 ASM 对字节码的操作中不可自拔。
ASM 是什么?cglib、Spring 等框架中对于字节码的操作就建立在 ASM 之上。
我们都知道,Spring 的 AOP 是基于动态代理实现的,Spring 会在运行时动态创建代理类,代理类中引用被代理类,在被代理的方法执行前后进行一些神秘的操作。那么,Spring 是怎么在运行时创建代理类的呢?动态代理的美妙之处,就在于我们不必手动为每个需要被代理的类写代理类代码,Spring 在运行时会根据需要动态地创造出一个类。这里创造的过程并非通过字符串写 Java 文件,然后编译成 class 文件,然后加载。Spring 会直接“创造”一个 class 文件,然后加载,创造 class 文件的工具,就是 ASM 了。
BTrace 配置
首先要下载 btrace 的压缩包,并解压
打开 CMD 命令行,输入 btrace 出现帮助及配置成功
怎么玩
准备一个 springboot 的工程,写一个 controller。
建立一个简单的工程,maven 的也可以,只是这个工程里面需要依赖 btrace 的三个包。
脚本
写一个类 MoreBtrace,类似于 spring 中的 bean 加入@BTrace 注解,然后在类中写脚本。
在命令行执行 jps,和 btrace pid 脚本名.java,如:
监控中出现监控结果:
脚本语法
OnMethod
@OnMethod 可以指定 clazz 、method、location。
由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。
拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截。
clazz
clazz 支持,精准定位、正则表达式定位、
按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如@OnMethod(clazz="+com.kite.base", method="doSome")>、
按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService", method="@javax.jws.WebMethod")>
method 支持精准定位、正则表达式定位、按注解定位。
location
1. Kind.Entry 与 Kind.Return:
分别表示函数的开始和返回,不写 location 的情况下,默认为 Kind.Entry,仅获取参数值,可以用 Kind.Entry ,要获取返回值或执行时间就要用 Kind.Return。
2. Kind.Error, Kind.Throw 和 Kind.Catch:
表示异常被 throw 、异常被捕获还有异常发生但是没有被捕获的情况,在拦截函数的参数定义里注入一个 Throwable 的参数,代表异常。
3、Kind.Call 表示被监控的方法调用了哪些其他方法,Kind.Line 监测类是否执行到了设置的行数。
案例解释
1、监控 DemoController 的 test 方法,打印 class 和 method 以及方法的参数:
2、监控 DemoController 的 test 方法,打印这个方法中所有的方法调用后的实例、参数、方法名、耗时(纳秒):
3、监控 DemoController 的 exception 方法,这个方法中会有异常,但是被我 try{}catch 给吃了:
BTrace 注解
BTrace 注解可以分为:
类注解 @BTrace
方法注解如@OnMethod
参数注解如:@ProbeClassName
参数注解
@ProbeClassName
用于标记处理方法的参数,仅用户@OnMethod, 该参数的值就是被跟踪的类名称
@ProbeMethodName
用于表姐处理方法的参数,仅用户 @OnMethod,该参数值是被跟踪方法名称
@Self
当前截取方法的封闭实例参数
@Return
当前截取方法的的返回值, 只对 location=@Location(Kind.RETURN) 生效
@Duration
当前截取方法的执行时间
@TargetInstance
当前截取方法内部调用的实例
@TargetMethodOrField
当前截取方法内部被调用的方法名
方法注解
@OnMethod
用于指定跟踪方法到目标类,目标方法和目标位置,格式
@Location 属性有:
value 默认值为 Kind.ENTRY 即参数的入口位置
where 限定探测位置 默认值为 Where.BEFORE 也可以设置为 Where.AFTER
clazz
method
field
type
line
@Kind 注解的值有:
Kind.ENTRY-被 trace 方法参数
Kind.RETURN-被 trace 方法返回值
Kind.THROW -抛异常
Kind.ARRAY_SET, Kind.ARRAY_GET -数组索引
Kind.CATCH -捕获异常
Kind.FIELD_SET -属性值
Kind.LINE -行号
Kind.NEW -类名
Kind.ERROR -抛异常
@OnTimer
用于指定跟踪操作定时执行。value 用于指定时间间隔
@OnError
当 trace 代码抛异常或者错误时,该注解的方法会被执行.如果同一个 trace 脚本中其他方法抛异常,该注解方法也会被执行。
Btrace 的限制
BTrace 最终借 Instrument 实现 class 的替换。出于安全考虑,Instrument 在使用上存在诸多的限制,这就好比给一架正在飞行的飞机换
发动机一样一样的,因此 BTrace 脚本的限制如下:
不允许创建对象
不允许创建数组
不允许抛异常
不允许 catch 异常
不允许随意调用其他对象或者类的方法,只允许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输出工具)
不允许改变类的属性
不允许有成员变量和方法,只允许存在 static public void 方法
不允许有内部类、嵌套类
不允许有同步方法和同步块
不允许有循环
不允许随意继承其他类(当然,java.lang.Object 除外)
不允许实现接口
不允许使用 assert
不允许使用 Class 对象
如此多的限制,其实可以理解。BTrace 要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。
工具总结
其实作为 Java 的动态追踪技术,站在比较底层的角度上来说,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。Arthas 都是针前面这些技术的一个封装而已。
Btrace 功能虽然强大,但都是比较难入门,这就是为什么 Btrace 出来这么多年,还是只在小范围内被使用。相对来说,Arthas 显得友好而且安全得多。
但无论工具如何强大,一些基础知识是需要牢固掌握的,否则,工具中出现的那些术语,也会让人一头雾水。
工具常变,但基础更加重要。如果你想要一个适应性更强的技术栈,还是要多花点时间在原始的排查方法上。