Java的Agent字节码增强&链路追踪
2024-12-12 09:18:45 3 举报
AI智能生成
Java的Agent字节码增强&链路追踪是一种在Java应用程序运行时,通过拦截并修改其字节码,来实现功能增强和性能监控的技术。这种技术无需修改源代码,即可实现对Java应用程序的透明化改造。Agent字节码增强可以用于实现链路追踪、性能监控、安全审计等功能。链路追踪可以记录并展示一次请求在处理过程中的路径,帮助开发者快速定位问题。通过在应用程序的请求处理流程中注入链路追踪的代码,可以追踪请求的处理过程,收集性能数据,并在可视化工具中展示,为开发者提供实时的请求处理信息。
作者其他创作
大纲/内容
深入理解字节码增强,玩转javassist
Java体系结构
Java应用: 我们平时开发的程序就属于这个范畴
Java Agent:Java层面 代表作Arthas
JMX: jconsole、Java层面,jps
Serviceability Agent: 属于JVM层面,代表作品HSDB
提供这么多机制,主要做两件事
1.调试工具
2.监控工具
JNI早期是为了Java与C兼容,现在是提供JNI调用底层的能力
Java应用: 我们平时开发的程序就属于这个范畴
Java Agent:Java层面 代表作Arthas
JMX: jconsole、Java层面,jps
Serviceability Agent: 属于JVM层面,代表作品HSDB
提供这么多机制,主要做两件事
1.调试工具
2.监控工具
JNI早期是为了Java与C兼容,现在是提供JNI调用底层的能力
字节码增强分为两个部分来理解
1.字节码 (.class文件)
2.增强
字节码增强需要编译系统和运行系统工作协作。
编译系统通过静态编译javac输出成.class文件,然后作为运行系统的输入
一个Java应用需要调试和监控
1.字节码 (.class文件)
2.增强
字节码增强需要编译系统和运行系统工作协作。
编译系统通过静态编译javac输出成.class文件,然后作为运行系统的输入
一个Java应用需要调试和监控
目前主流的字节码增强技术有bcel、ASM、javassit、byte-buddy(Skywalking使用的技术)
为什么ASM字节码增强技术性能高?(从调用方法层面)
增强的代码技术分为两类
1.写Java代码 面向Java开发
2.字节码增强包 面向字节码开发
其中bcel、ASM技术可以直接生成字节码,而javassist、byte-budy是通过生成Java代码,然后再编译
性能评判标准是:越接近字节码,性能越高,变成难度越大
代码请见,如果觉得有用的话,还请点个star
https://github.com/2over/my-agent
为什么ASM字节码增强技术性能高?(从调用方法层面)
增强的代码技术分为两类
1.写Java代码 面向Java开发
2.字节码增强包 面向字节码开发
其中bcel、ASM技术可以直接生成字节码,而javassist、byte-budy是通过生成Java代码,然后再编译
性能评判标准是:越接近字节码,性能越高,变成难度越大
代码请见,如果觉得有用的话,还请点个star
https://github.com/2over/my-agent
目前JDK中获取一个类的信息的方式
1.反射(这也是JDK动态代理使用的方式)
2.MethodProxy.create(CGLIB使用的方式)
3.RuntimeSupport.find2Method(javassist中使用的方式)
4.MethodHandle(Lambda表达式中使用的方式)
1.反射(这也是JDK动态代理使用的方式)
2.MethodProxy.create(CGLIB使用的方式)
3.RuntimeSupport.find2Method(javassist中使用的方式)
4.MethodHandle(Lambda表达式中使用的方式)
JDK动态代理
CGlib动态代理
javassist
Lambda表达式中使用的方式
自实现动态代理机制
jdk与cglib动态代理的区别
jdk:
1.实现接口
2.invoke的三个参数:代理对象的实例、被代理类中的方法、方法参数
3.生成的代理类中不会生成增强方法
4.在invoke中通过反射直接调用被代理类中的方法
cglib:
1.代理类与被代理类时父子关系
2.invoke的四个参数:代理对象的实例、被代理类中的方法、方法参数、生成的代理类中的方法
add->intercept->CGLIB$add$0->super.add
对比:
1.不传代理对象的实例,传Class对象行不行?
不行,如果构造函数有参数就行不通
2.cglib生成的增强方法是否多余?为什么?
有点多余,
3.handler中的invoke方法中的第一个参数是代理对象的实例,有什么用?
还不得而知
Cglib是新生成了一个类,增强了方法,生成的代理类中可以直接调用原方法
Jdk新生成了一个类,没有增强方法,生成的代理类中不可以直接调原方法
jdk:
1.实现接口
2.invoke的三个参数:代理对象的实例、被代理类中的方法、方法参数
3.生成的代理类中不会生成增强方法
4.在invoke中通过反射直接调用被代理类中的方法
cglib:
1.代理类与被代理类时父子关系
2.invoke的四个参数:代理对象的实例、被代理类中的方法、方法参数、生成的代理类中的方法
add->intercept->CGLIB$add$0->super.add
对比:
1.不传代理对象的实例,传Class对象行不行?
不行,如果构造函数有参数就行不通
2.cglib生成的增强方法是否多余?为什么?
有点多余,
3.handler中的invoke方法中的第一个参数是代理对象的实例,有什么用?
还不得而知
Cglib是新生成了一个类,增强了方法,生成的代理类中可以直接调用原方法
Jdk新生成了一个类,没有增强方法,生成的代理类中不可以直接调原方法
JDK的invoke
Cglib的invoke
如果想要增强一个类,有如下途径可以实现
1.增强原有类/需要满足几个条件,一是能够增强类(javassist),二是需要被jvm载入,但是需要借助agent的热更新
2.创建新的类,反而更简单一些
1.增强原有类/需要满足几个条件,一是能够增强类(javassist),二是需要被jvm载入,但是需要借助agent的热更新
2.创建新的类,反而更简单一些
目前支持动态代理的机制有如下几种:
1.jdk
2.cglib
3.javassit
4.自实现
1.jdk
2.cglib
3.javassit
4.自实现
深入理解Agent
Agent用途
1.监控工具,随Java应用一同启动
2.调试工具,通过attach机制与Java应用建立通信
1.监控工具,随Java应用一同启动
2.调试工具,通过attach机制与Java应用建立通信
Agent用法
1.随java应用启动
```java
// 会优先运行这个premain
public static void premain(String agentArgs, Instrumentation inst);
// 如果没有两个参数的premain,就运行一个参数的
public static void premain(String agentArgs);
```
用法: 等号后面的是传参,多个参数需要自己在程序中解析
-javaagent:jaavagent-demo-1.0.jar=mode=test
2.attach
```java
// 会优先运行这个agentmain
public static void agentmain(String agentArgs, Instrumentation inst);
// 如果没有两个参数的agentmain,就运行一个参数的
```
agentArgs:就是外部传给agent程序的参数,如果是多个参数,建议采用这个格式,用下面这段代码解析
eg:javaagent-demo-1.0.jar=mode=test;name=cover;age=18
```java
private static Map<String, String> parseArgs(String args) {
Map<String, String> ret = new HashMap<>();
String[] argsArr = args.split(";");
for (String arg : argsArr) {
String[] strings = arg.split("=");
ret.put(strings[0], strings[1]);
}
return ret;
}
```
1.随java应用启动
```java
// 会优先运行这个premain
public static void premain(String agentArgs, Instrumentation inst);
// 如果没有两个参数的premain,就运行一个参数的
public static void premain(String agentArgs);
```
用法: 等号后面的是传参,多个参数需要自己在程序中解析
-javaagent:jaavagent-demo-1.0.jar=mode=test
2.attach
```java
// 会优先运行这个agentmain
public static void agentmain(String agentArgs, Instrumentation inst);
// 如果没有两个参数的agentmain,就运行一个参数的
```
agentArgs:就是外部传给agent程序的参数,如果是多个参数,建议采用这个格式,用下面这段代码解析
eg:javaagent-demo-1.0.jar=mode=test;name=cover;age=18
```java
private static Map<String, String> parseArgs(String args) {
Map<String, String> ret = new HashMap<>();
String[] argsArr = args.split(";");
for (String arg : argsArr) {
String[] strings = arg.split("=");
ret.put(strings[0], strings[1]);
}
return ret;
}
```
Instrumentation这个对象是什么?为什么premain和agentmain都需要传这个参数?
先来看一个重要的数据结构:_JPLISAgent,全称:Java Programming Language Instrumentation Services Agent
各属性的含义:
1.mNormalEnvironment:主要提供正常的类transform及redefine功能的
2.mRetransformEnvironment:主要提供类retransform功能的
3.mInstrumentationImpl:这个对象非常重要,也是我们java agent和JVM进行交互的入口,写过javaagent的人在写premain和agent方法的时候注意到了有个Instrumentation的参数,这个参数其实就是这里的对象
4.mPremainCaller:指向sun.instrument.InstrumentationImpl#loadClassAndCallPremain方法,如果agent是在启动的时候加载的,那该方法会被调用
5.mAgentmainCaller:指向sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain,该方法在通过attach的方式动态加载agent的时候调用
6.mTransform: 指向sun.instrument.InstrumentationImpl#transform方法
7.mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class
8.mOptionString:传给agent的一些参数
9.mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置CanRedefine-Classes:true
10.mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true
11.mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,那将会设置mRetransformEnvironment的mIsRetransformer为true
先来看一个重要的数据结构:_JPLISAgent,全称:Java Programming Language Instrumentation Services Agent
各属性的含义:
1.mNormalEnvironment:主要提供正常的类transform及redefine功能的
2.mRetransformEnvironment:主要提供类retransform功能的
3.mInstrumentationImpl:这个对象非常重要,也是我们java agent和JVM进行交互的入口,写过javaagent的人在写premain和agent方法的时候注意到了有个Instrumentation的参数,这个参数其实就是这里的对象
4.mPremainCaller:指向sun.instrument.InstrumentationImpl#loadClassAndCallPremain方法,如果agent是在启动的时候加载的,那该方法会被调用
5.mAgentmainCaller:指向sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain,该方法在通过attach的方式动态加载agent的时候调用
6.mTransform: 指向sun.instrument.InstrumentationImpl#transform方法
7.mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class
8.mOptionString:传给agent的一些参数
9.mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置CanRedefine-Classes:true
10.mNativeMethodPrefixAvailable:是否支持native方法前缀设置,同样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true
11.mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,那将会设置mRetransformEnvironment的mIsRetransformer为true
Inst对象是何时创建的?
Agent_OnAttach
Agent_OnAttach
success = createInstrumentationImpl(jni_env, agent);
premain何时被调用
在Threads::create_vm中调用JvmtiExport::post_vm_initialized
Agent_OnLoad进来
createNewJPLISAgent创建JPLISAgent对象
initializeJPLISAgent中调用了eventHandlerVMInit
接着里面调用了processJavaStart
这个mPremainCallerMethod就是sun.instrument.InstrumentationImpl#loadClassAndCallPremain
loadClassAndCallPremain何时被调用
mainCallingMethod是前面processJavaStart方法传递过来的参数
agentmain何时被调用
HotSpot源码:Agent_OnAttach
Java代码:sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain
HotSpot源码:Agent_OnAttach
Java代码:sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain
如何监控类的加载?
这其实涉及到JVMTI Agent了,目前暂时不去深入
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#GetLoadedClasses
这其实涉及到JVMTI Agent了,目前暂时不去深入
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#GetLoadedClasses
attach的本质
首先从java端virtualMachine = VirtualMachine.attach(pid);开始
接着是LinuxAttachProvider
Agent进程是一个独立的进程,attach底层是通过socket底层实现,不是通过tcp、udp而是unix域 127.0.0.1回环网卡通信
首先从java端virtualMachine = VirtualMachine.attach(pid);开始
接着是LinuxAttachProvider
Agent进程是一个独立的进程,attach底层是通过socket底层实现,不是通过tcp、udp而是unix域 127.0.0.1回环网卡通信
再者是LinuxVirtualMachine
attach底层是通过socket实现的
是基于本地域实现的socket,保证通信效率
也可以使用命令netstat -apnl | grep java根据进程id去查看
也可以使用命令netstat -apnl | grep java根据进程id去查看
监控的时候就需要用到premain
热更新的时候需要用到agentmain,需要attach
热更新的时候需要用到agentmain,需要attach
实现链路追踪引擎及热更新
背景
自SpringCloud问世以来,微服务以席卷之势风靡全球,企业架构都在从传统的SOA向微服务转型。然而微服务这把双刃剑在带来各种优势的同时,也给运维、性能监控、错误的排查带来了极大的困难。
在大型项目中,服务架构会包含数十乃至上百个服务节点。往往一次请求会涉及到多个微服务,想要排查一次请求链路中经过了哪些节点,每个节点的执行情况如何,就成为了亟待解决的问题,于是分布式系统的APM管理系统应运而生。
自SpringCloud问世以来,微服务以席卷之势风靡全球,企业架构都在从传统的SOA向微服务转型。然而微服务这把双刃剑在带来各种优势的同时,也给运维、性能监控、错误的排查带来了极大的困难。
在大型项目中,服务架构会包含数十乃至上百个服务节点。往往一次请求会涉及到多个微服务,想要排查一次请求链路中经过了哪些节点,每个节点的执行情况如何,就成为了亟待解决的问题,于是分布式系统的APM管理系统应运而生。
什么是APM系统?
APM系统可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题,这就是APM系统,
全称是Application Performance Monitor
谷歌公开的论文提到的Google Dapper可以说是最早的APM系统了,给google的开发者和运维团队帮了大忙,所以谷歌公开论文分享了Dapper.而后,很多的技术攻击基于这篇论文的原理,涉及开发了很多出色的APM框架,例如Pinpoint、SkyWalking等。而SrpingCloud官网也集成了一套这样的系统:SpringCloud Sleuth,结合Zipkin.
APM系统可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题,这就是APM系统,
全称是Application Performance Monitor
谷歌公开的论文提到的Google Dapper可以说是最早的APM系统了,给google的开发者和运维团队帮了大忙,所以谷歌公开论文分享了Dapper.而后,很多的技术攻击基于这篇论文的原理,涉及开发了很多出色的APM框架,例如Pinpoint、SkyWalking等。而SrpingCloud官网也集成了一套这样的系统:SpringCloud Sleuth,结合Zipkin.
APM的基本原理
目前大部分的APM系统都是基于Google的Dapper原理实现,例如一次请求调用示例:
目前大部分的APM系统都是基于Google的Dapper原理实现,例如一次请求调用示例:
如何才能实现追踪呢?
Google的Dapper涉及了下面的几个概念用来记录请求链路:
Span: 请求中的基本工作单元,每一次链路调用(RPC、Rest、数据库调用)都会创建一个Span。结构如下
type Span struct {
TraceID int 64 // 用于表示一次完整的请求id
Name string // 单元名称
ID int64 // 当前这次调用span_id
ParentID int64 // 上层服务的span_id,最上层服务parent_id为null,代表根服务
Annotation[] Annotation // 注释,用于记录被调用中的详细信息,例如时间
}
Trace: 一次完整的调用链路,包含多个Span的树状结构,具有唯一的TraceID
一次请求的每个链路,通过spanId、parentId就能串联起来;
当然,从请求到服务器开始,服务器返回Response结束,每个span存在相同的唯一标识trace_id
Google的Dapper涉及了下面的几个概念用来记录请求链路:
Span: 请求中的基本工作单元,每一次链路调用(RPC、Rest、数据库调用)都会创建一个Span。结构如下
type Span struct {
TraceID int 64 // 用于表示一次完整的请求id
Name string // 单元名称
ID int64 // 当前这次调用span_id
ParentID int64 // 上层服务的span_id,最上层服务parent_id为null,代表根服务
Annotation[] Annotation // 注释,用于记录被调用中的详细信息,例如时间
}
Trace: 一次完整的调用链路,包含多个Span的树状结构,具有唯一的TraceID
一次请求的每个链路,通过spanId、parentId就能串联起来;
当然,从请求到服务器开始,服务器返回Response结束,每个span存在相同的唯一标识trace_id
APM的筛选标准
目前主流的APM框架都会包含下列几个组件来完成链路信息的收集和展示:
1.探针(Agent):负责在客户端程序运行时搜索服务调用链路信息,发送给收集器
2.收集器(Collector):负责将数据格式化,保存到存储器
3.存储器(Storage):保存数据
4.UI界面(WebUI):统计并展示收集到的信息
因此,要筛选一款合格的APM框架,就是对比各个组件的使用差异,主要对比项:
1.探针的性能
主要是agent对服务的吞吐量、CPU和内存的影响。如果探针在收集微服务运行数据时,对微服务的运行产生了比较大的性能影响,相信没什么人愿意使用。
2.collector的可扩展性
能够水平扩展以便支持大规模服务器集群,保证收集器的高可用特性
3.全面的调用链路数据分析
数据的分析要快,分析的维度尽可能多。跟踪系统能够提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应,最好提供源码级别的可见性以便轻松定位失败点和瓶颈
4.对于开发透明,容易开关
即也作为业务组件,应当尽可能少入侵或者无入侵其他业务系统,对于使用方透明,减少开发人员的负担
5.完整的调用链应用拓扑
自动检测应用拓扑,帮助我们搞清楚应用的架构
目前主流的APM框架都会包含下列几个组件来完成链路信息的收集和展示:
1.探针(Agent):负责在客户端程序运行时搜索服务调用链路信息,发送给收集器
2.收集器(Collector):负责将数据格式化,保存到存储器
3.存储器(Storage):保存数据
4.UI界面(WebUI):统计并展示收集到的信息
因此,要筛选一款合格的APM框架,就是对比各个组件的使用差异,主要对比项:
1.探针的性能
主要是agent对服务的吞吐量、CPU和内存的影响。如果探针在收集微服务运行数据时,对微服务的运行产生了比较大的性能影响,相信没什么人愿意使用。
2.collector的可扩展性
能够水平扩展以便支持大规模服务器集群,保证收集器的高可用特性
3.全面的调用链路数据分析
数据的分析要快,分析的维度尽可能多。跟踪系统能够提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应,最好提供源码级别的可见性以便轻松定位失败点和瓶颈
4.对于开发透明,容易开关
即也作为业务组件,应当尽可能少入侵或者无入侵其他业务系统,对于使用方透明,减少开发人员的负担
5.完整的调用链应用拓扑
自动检测应用拓扑,帮助我们搞清楚应用的架构
目前主流的APM框架分别是:
1.Zipkin:由Twitter公司开源,开放源代码分布式的追踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展示
2.Pinpoint:一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式追踪组件
3.Skywalking:国产的的优秀APM组件,是一个对Java分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。现在时Apache的顶级项目之一。
可见,zipkin的探针性能、开发透明性、数据分析能力都不占优,实在是下下之选。
而pinpoint在数据分析能力、开发透明性上有较大的优势,不过Pinpoint的部署相对比较复杂,需要的硬件资源较高。
Skywalking的探针性能和开发透明性上具有较大优势,数据分析能力也还不错,重要的是其部署比较方便灵活,比起Pinpoint更适合中小型企业使用
1.Zipkin:由Twitter公司开源,开放源代码分布式的追踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展示
2.Pinpoint:一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式追踪组件
3.Skywalking:国产的的优秀APM组件,是一个对Java分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。现在时Apache的顶级项目之一。
可见,zipkin的探针性能、开发透明性、数据分析能力都不占优,实在是下下之选。
而pinpoint在数据分析能力、开发透明性上有较大的优势,不过Pinpoint的部署相对比较复杂,需要的硬件资源较高。
Skywalking的探针性能和开发透明性上具有较大优势,数据分析能力也还不错,重要的是其部署比较方便灵活,比起Pinpoint更适合中小型企业使用
为什么要有APM框架,不直接用专门的日志收集或者AOP、动态代理来做监控?
它们有一个共同的缺点就是需要改代码,这类技术是一种侵入式的,而APM是无侵入式的监控系统
它们有一个共同的缺点就是需要改代码,这类技术是一种侵入式的,而APM是无侵入式的监控系统
Agent技术主要分为两种
1.监控 通过premain实现
2.调试 通过agentmain vm.attach
增强一个类一般来说,分为两种:
1.新创建一个类
2.增强原有类,增强方法
1.监控 通过premain实现
2.调试 通过agentmain vm.attach
增强一个类一般来说,分为两种:
1.新创建一个类
2.增强原有类,增强方法
transform能够直接拦截所有类的原理
transform是运行在类加载阶段的加载和链接之间的一种技术手段
(加载->trasform 监控 -> 链接 -> 初始化 -> redefine 热更新 -> 卸载)
对应的HotSpot源码部分在.class->parseClassFile->JvmtiExport::post_load_class();
JavaAgent底层是基于JVMTI Agent生成的
如果是在初始化阶段之后做增强,这就属于热更新了redefine
transform是运行在类加载阶段的加载和链接之间的一种技术手段
(加载->trasform 监控 -> 链接 -> 初始化 -> redefine 热更新 -> 卸载)
对应的HotSpot源码部分在.class->parseClassFile->JvmtiExport::post_load_class();
JavaAgent底层是基于JVMTI Agent生成的
如果是在初始化阶段之后做增强,这就属于热更新了redefine
这个方法会判断有没有hook方法,如果有,则调用到transform
热更新的时候会STW,如果类加载初始化完成了,希望重新加载,可以使用redefine技术.
关键的方法:
1.VirtualMachine.attach(pid); 开启unix域类型的socket
2.VirtualMachine.loadAgent(jarPath, agentArgs); 把jar包加载进去
关键的方法:
1.VirtualMachine.attach(pid); 开启unix域类型的socket
2.VirtualMachine.loadAgent(jarPath, agentArgs); 把jar包加载进去
0 条评论
下一页
为你推荐
查看更多