本文结合takin的探针实现讲讲javaagent。这是实现全链路监控和数据隔离的核心技术点。
那什么是javaagent呢?
大家可能有见过或者用过。类似如下:
java -javaagent:agent.jar=key1=value1;key2=value2 DemoMain
我们通过 -javaagent 来指定我们编写的 agent 的 jar 路径(agent.jar),以及要传给 agent 的参数(key1=value1;key2=value2)。
使用javaagent我们用到的主要功能就是对字节码做修改,以实现自定义逻辑。
使用起来还是有些限制的。比如不能增加字段、改变方法签名等。
在实际使用中这些限制一般是不影响我们,因为我们通常是在方法体内部插入事件通知代码的形式进行链路追踪或修改参数和返回值甚至通过主动抛出异常改变运行逻辑。
写一个javaagent是很简单的。只需要实现premain和agentmain两个方法。在定义好类转换器就可以了。网上有很多demo。但是要想熟练的使用它,还需要深入了解其实现机制。来看看这项技术的优点和缺点吧。
首先介绍JVMTI。
全称 JVM Tool Interface。是 JVM 暴露出来的一些供用户扩展的接口集合。JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
为什么要介绍JVMTI呢?其实我们每天都在和 JVMTI 打交道,只是你可能没有意识到而已,比如我们经常使用 IDEA 等工具调试 Java 代码,其实就是利用 JRE 自带的 jdwp agent 实现的。
-agentlib:jdwp=transport=dt_socket
那话说回来,我们的javaagent跟JVMTI有什么关系呢?
下面是openjdk8的源码。
可以看到它也是使用了instrument 这个agent实现的。
JVMTIAgent 其实就是一个动态库,利用 JVMTI 暴露出来的接口进行特定功能实现。
那加载之后是怎么调用的呢?
先通过vm获取到jvmtiEnv,然后注册相关回调函数。
如果有类加载事件,先通知JVMTIAgent然后再通知javaagent。
这就是javaagent整个执行流程了。
整个流程清楚了之后,仔细想想这里面隐藏了很多陷阱。
- agent引入的依赖如何不污染业务使用的类。
- 因为是动态字节码增强,如何尽量减少反射的开销。
- 动态字节码的修改需要在safepoint下执行,因此整体会触发stop-the-world。
- 动态字节码修改会清除JIT优化退回到解释执行模式。
- 如果想获取整个堆栈信息只能对所有方法进行增强。
针对第一条:takin使用自定义类加载器进行隔离加载,并且通过配置文件可以自定义类的export、import规则灵活加载。
针对第二条:尽量通过仅编译期依赖的方式静态绑定,如果因为类版本冲突等原因不能使用静态编译,则缓存对应的反射对象,减少反射查找开销。
其余三条暂时也没有特别好的办法。因为javaagent受制于jvm runtime的影响,功能受限。不过经过研究jdk源码,觉得可以往这个方向走:Native Agent。留给大家集思广益了。下期见。
本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请注明出处:https://news.shulie.io/?p=3635