基于Java Agent实现APM
Posted Shi Peng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Java Agent实现APM相关的知识,希望对你有一定的参考价值。
一、APM概述
APM系统(Application Performance Management,即应用性能管理),用于对应用系统做实时监控,目的是实现对应用性能管理和故障定位。
1.1、为什么需要APM
APM主要用于对微服务做 “监控及故障发现”,如性能监控,出现故障时发送告警,及辅助排障;更进一步可提前做故障预警。
以电商系统为例,用户的请求链路会经由多个微服务系统后返回:
在线上,当用户的响应时常出现异常时,怎样定位是哪引起的呢?
如果没有tracing系统,研发需要去逐个排查请求链路所经历的每个微服务系统,耗时比较长,且排查定位问题麻烦。
但有了Tracing监控系统,我们可以一眼找到问题。而且,在定位问题后,如果是某个微服务出现了瓶颈,可以根据Tracing的结果查看其请求量增加了多少,从而可精准地判断需要扩容多少机器。
这就是Tracing存在的主要意义。
1.2、微服务都监控什么?
对微服务的监控,主要包括下面三点:
1、Logging
记录日志,包括正常系统行为,及异常。如把日志存到ES中,再通过Kibana分析。
2、Metrics
系统在单位时间内对某一系统指标的度量,如每分钟的请求次数。
如Prometheus和open-falcon都是干这个的,最终展示曲线图给研发人员排障。
3、Tracing
Tracing指分布式链路跟踪。在微服务系统中,一个请求会经过多个微服务系统,根据Tracing可在视图上直观看到,请求都经过了哪个微服务,执行时间是多少,从而快速发现是哪个微服务出了问题。
另外,当各个微服务之间的调用依赖关系发生变化,从Tracing的视图中也能够快速的展示出来。
google的经典论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》就是各互联网公司做分布式链路跟踪的理论基础。
Tracing的典型视图是这样子的:
可以看出不同微服务间的调用依赖关系,且可看到每个经过每个微服务的执行时间,从而方便定位性能瓶颈。
1.3、业内经典的APM系统
成熟的互联网公司,通常都有自己的全方位监控系统,这样可及时发现故障,并为系统优化提供数据支持。
目前流行的APM如下:
1、CAT
美团点评开源,Java语言开发的,但提供了多语言客户端,如Java, C/C++, Node.js, Python, Go等。监控数据全量统计。
CAT需要研发人员手动在代码中添加埋点,是有侵入的。
但Cat Agent也支持探针式无侵入。
目前使用CAT的公司有美团点评、携程、拼多多等。
2、Zipkin
twitter公司开源,Java语言开发,侵入性比Cat低一些,但需要对web.xml等配置做修改,方便与Spring Cloud集成,也是Spring Cloud推荐的APM系统
3、Pinpoint
韩国开源的APM,基于Java Agent,值需要在启动时添加启动参数,对代码无侵入。支持Java 和 php语言,使用HBase存储数据。但缺点是探针收集数据的粒度非常细,这样性能损耗明显。
4、Skywalking
Apache顶级项目,支持Java, .Net, Node.js等探针,数据存储支持mysql, ES等。
Skywalking与Pinpoint相同,采用Java Agent实现,对业务无侵入。但其探针采集粒度较Pinpoint略粗,所以性能更好。
目前,Skywalking社区活跃,中文文档齐全,且支持多种框架,如Dubbo, grpc等。
5、其余
此外,还有淘宝鹰眼、google dapper等等。
二、Skywalking简介
Skywalking是基于OpenTracing规范的、开源的经典的APM系统,专门为微服务架构及云原生而设计的。
Skywalking的核心功能有:
1)服务的指标分析
2)服务的拓扑图分析
3)服务的SLA分析
4)慢查检测
5)告警
Skywalking的特点:
1)多语言探针,如java, .net等
2)为多种开源项目提供插件,如Tomcat, HttpClient, Spring, RabbitMQ, MySQL等各种场景的基础设施与组件提供自动探针
3)微内核 + 插件的架构,存储、集群管理、使用插件可进行自由选择
4)支持告警
5)优秀的可视化效果
Skywalking的架构图:
Skywalking分三个核心部分:
1、Agent:Agent运行在各个服务实例中,负责采集服务实例的Trace, Metrics等数据,然后通过grpc上报给skywalking后端。
2、OAP:Skywalking的后端服务,主要功能有两个:
1)负责接收Agent上报的Trace, Metrics数据,交给Analysis(涉及Skywalking OAP中的多个模块)进行流式分析,并将最终结果写入持久化存储中。Skywalking可支持ES, MySQL等存储。
2)负责响应skywalking UI界面后端发来的查询请求,然后去查持久化数据,组成正确的响应结果返回给UI界面。
3、UI界面:负责将优化的查询请求封装为GraphQL请求提交给OAP后端触发查询操作,并把后端的返回结果在前端展示。
Skywalking监控的目标有三个维度:
1)Service(服务):提供独立功能的模块,单独部署成一个集群对外提供服务
2)ServiceInstance(服务实例):Service集群中的一个节点
3)Endpoint(端点):服务对外暴露的接口,如“/query/userInfo”接口,或RPC接口。
三、Java Agent
3.1、Java Agent是什么
Java agent是独立于主应用程序的代理程序,用来协助检测设置更改替换主应用程序的代码,基于Java Agent可实现虚拟机级别的AOP。
Agent分两种,一种是在主程序之前运行,一种在主程序之后运行。
3.2、Java Agent的执行流程
在JAM启动时,在执行main() 函数前,虚拟机会先找到 -javaagent命令指定的jar包,然后执行 premain-class中的 premain() 方法。所以,premain()函数可以理解为main()函数之前的拦截器。
3.3、Java Agent怎样用
Java Agent本质上是一个特殊的 jar 包,但它不能单独运行,他必须依附在一个主JVM进程上,所为其“寄生”插件。
1、MANIFEST.MF 文件 – 用于指定premain-class
Java Agent既然要打成Jar包,首先需要在一个叫做
MANIFEST.MF 文件,通过此文件来指定,其premain-class是哪一个,其需要根据需求定义Can-Redefine-Classes(是否和重新装载)和 Can-Retransform-Classes(是否可传值)。
MANIFEST.MF 文件文件内容如下:
Manifest-Version: 1.0
Premain-Class: com.demo.agent.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
这里注意最后要留一行空行。
2、编写Agent类
import java.lang.instrument.Instrumentation;
import com.alibaba.ttl.threadpool.agent.TtlAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
/**
* 我的Java代理
*
*/
public class MyAgent
/**
* 该方法在main方法之前运行,与main方法运行在同一个JVM中
* 并被同一个System ClassLoader装载
* 被统一的安全策略(security policy)和上下文(context)管理
*
*/
public static void premain(String agentOps, Instrumentation inst)
System.out.println("this is an perform monitor agent.");
TtlAgent.premain(agentOps, inst);
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer()
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader)
return builder
.method(ElementMatchers.<MethodDescription>any()) // 拦截任意方法
// .method(ElementMatchers.nameContains("debug")) // 拦截任意方法
.intercept(MethodDelegation.to(TimeInterceptor.class)); // 委托
;
AgentBuilder.Listener listener = new AgentBuilder.Listener()
@Override
public void onTransformation(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, DynamicType dynamicType)
@Override
public void onIgnored(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module)
@Override
public void onError(String typeName, ClassLoader classLoader, JavaModule module, Throwable throwable)
System.out.println("error typeName:" + typeName + ", exception:" + throwable);
@Override
public void onComplete(String typeName, ClassLoader classLoader, JavaModule module)
// System.out.println("complete typeName:" + typeName);
;
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith("com.shanhy.demo.Log")) // 指定需要拦截的类
.transform(transformer)
.with(listener)
.installOn(inst);
System.out.println("=========premain method run 111========");
System.out.println(agentOps);
/**
* 如果不存在 premain(String agentOps, Instrumentation inst)
* 则会执行 premain(String agentOps)
*/
public static void premain(String agentOps)
System.out.println("=========premain method run 2========");
System.out.println(agentOps);
这里就是MANIFEST.MF中指定的的premain-class叫做MyAgent ,其premain() 函数会在 主应用程序main()函数之前执行,通过他可拦截 主应用程序中的 方法,在被拦截方法前后添加执行指令,设置改写其被拦截方法的执行代码。
premain(String agentOps, Instrumentation inst)函数有两个参数:
1)String agentOps:是 -java agent 命令传进来的参数,如java -cp -javaagent:D:\\myagent.jar=abc -jar d:\\mydemo.jar中的abc
2)Instrumentation inst:他提供了操作类定义的相关方法
3、pom依赖
Java Agent工程的pom依赖为:
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.5.7</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.5.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.37</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactSet>
<includes>
<include>javassist:javassist:jar:</include>
<include>net.bytebuddy:byte-buddy:jar:</include>
<include>net.bytebuddy:byte-buddy-agent:jar:</include>
<include>com.alibaba:transmittable-thread-local:</include>
<include>com.alibaba:fastjson:jar:</include>
</includes>
</artifactSet>
</configuration>
</plugin>
</plugins>
</build>
这里重点关注两个依赖:
1)byte-buddy:基于byte-buddy,可更方便拦截 指定类名、方法名,获取方法参数等
2)transmittable-thread-local:可解决对于线程池中的异步线程传递thread local变量值(如traceId)
4、拦截指定方法后的处理函数
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import com.alibaba.fastjson.JSON;
public class TimeInterceptor
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object[] args) throws Exception
long start = System.currentTimeMillis();
try
// 原有函数执行
System.out.println("premain Object: " + callable);
System.out.println("premain method: " + method);
// System.out.println("premain param list size:" + args.length);
// for (Object obj : args)
// System.out.println("=========param:" + obj.toString());
//
if (method.toString().contains("com.shanhy.demo.Log.info"))
System.out.println(args[0].toString() + ", info, requestId=aaa");
return new String("info");
if (method.toString().contains("com.shanhy.demo.Log.debug"))
System.out.println(args[0].toString() + ", debug, requestId=bbb");
return new String("debug");
return callable.call();
// return new String("aaa");
finally
// System.out.println(method + " took " + (System.currentTimeMillis() - start) + "ms");
4、对Java agent工程打成jar包
这里注意要选择MANIFEST.MF文件,打包成myagent.jar
5、为主应用程序打可运行jar
随便写个最简单的java 应用程序,并打成可运行的jar包 mydemo.jar
6、和主应用一起执行java agent
java -cp -javaagent:D:/myagent.jar=traceId123 -jar d:/mydemo.jar
执行结果:
main traceId:null, thread id:1
main traceId2:fa0faca36c414509a1ab99062ee1e9d8, thread id:1
this is log.info, traceId=fa0faca36c414509a1ab99062ee1e9d8, thread id:1
this is log.debug, traceId=fa0faca36c414509a1ab99062ee1e9d8, thread id:1
sub thread run, traceId:fa0faca36c414509a1ab99062ee1e9d8, thread id:12
cleaned traceId:fa0faca36c414509a1ab99062ee1e9d8
get traceId:null
main traceId:null, thread id:1
main traceId2:5db619ae648747f99023e50500f8df9c, thread id:1
this is log.info, traceId=5db619ae648747f99023e50500f8df9c, thread id:1
this is log.debug, traceId=5db619ae648747f99023e50500f8df9c, thread id:1
cleaned traceId:5db619ae648747f99023e50500f8df9c
sub thread run, traceId:5db619ae648747f99023e50500f8df9c, thread id:14
get traceId:null
Thread pool traceId:fa0faca36c414509a1ab99062ee1e9d8, thread id:13
Thread pool traceId:5db619ae648747f99023e50500f8df9c, thread id:15
Attach API
在 java 5中仅提供了 premain() 方法,他只能在 main() 之前执行;在 java 6 时,提供了agentmain() 方法,可以在man()后执行。
agentmain()方法主要用于 JVM attach工具中,Attach API 是 Java扩展API,可以向目标 JMV “附着” (Attach)一个代理工具程序,这个代理工具程序的入口就是agentmain()。
四、Byte Buddy
4.1、Byte Buddy有什么用 – 作为代码生成库
Java是强类型语言,即在赋值时,一定要明确指定其变量的类型。但这种强类型方式在某些场景下,会带来麻烦:
例如我们对外提供一个通用的jar包,但我们并不知道用户在使用jar时,会传入什么类型的变量或方法,这怎么办呢?虽然Java可通过反射机制,帮我们获取位置类型,及调用其方法,但Java反射的API有两个明显的缺点:
1)早期的JDK版本,反射性能很差
2)反射API会绕过类型安全检查,所以反射API自身并不是类型安全的
为了解决这个问题,“运行时代码生成”在Java应用之后再动态生成一些类定义,这样就可以模拟出,只有动态编程语言才有的特性,同时不会丢失Java强类型检查。
所以,我们借助Byte Buddy这个代码生成库,在运行时生成代码,解决了反射带来的性能问题,同时保留了Java强类型检查。
但这种“运行时代码生成”的方式生成的Java类型,被JVM加载后,一般不会被垃圾回收,因此不应该过度使用此方式。
4.2、代码生成库为什么要选Byte Buddy
目前主流的Java代码生成库有:
1、Java Proxy(缺点是业务类必须实现指定接口)
他是JDK自带的代理工具,允许实现一系列接口的类的代理类,但他要求目标类必须实现接口,这给他带来了限制:例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,这时Java Proxy就不行了。
2、javassist
Javassist使用Java源代码字符串和Javassist提供的一些简单API,共同拼凑出用户想要的Java类,Javassist自带一个编译器,拼凑好的Java类在程序运行时会被编译成字节码并加载到JVM中。
javasssit优点是简单易用,且使用Java语法构建类与平时写Java代码类似,但是Javassist编译器在性能上不如Javac编译器,而且在动态组合字符串以实现较复杂的逻辑时容易出错。
Byte Buddy
Byte Buddy,可通过编写简单的Java代码即可创建自定义的运行时类。且Byte Buddy的定制能力也很好,可应付不同复杂度的需求。
下面是Byte Buddy官方给的数据,显示了代码生产库的性能,单位是纳秒,括号内是标准偏差:
对于代码生成库来说,需要在 “生成快速的代码” 与 “快速生成代码” 之间进行折中。
Byte Buddy折中的考虑是类型动态创建不是程序中的常见步骤,但方法调用等操作很常见,所以,Byte Buddy侧重于生成更快速的代码。
4.3、Byte Buddy基础入门
4.3.1、动态生成类和方法
先利用Byte Buddy代码生成库类生成一个类:任何一个由Byte Buddy生成的类都是通过ByteBuddy类的实例来完成的:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class) // 生成Object子类
.name("com.demo.Log.info") // 生成类的名称为"com.demo.Log.info"
.make();
包括subclass,Byte Buddy动态增强代码总共有三种方式:
1)subclass:对应ByteBuddy.subclass()方法,就是为目标类(被增强的类)生成一个子类,在子类中插入动态代码。
2)rebasing:对应ByteBuddy.rebasing()方法。当使用rebasing方式增强一个类时,ByteBuddy报错目标类中的所有方法的实现:当Byte Buddy遇到冲突的字段或方法时,会将原理的字段或方法实现,负债到具有兼容签名的重新命名的私有方法中,而不会抛弃这些这段和方法实现,从而达到不丢失的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。
举例说明:
class Foo // Foo的原始定义
String bar() return "bar";
class Foo //增强后的Foo定义
String bar() return "foo" + bar$original();
private String bar$original() return "bar"
3)redefinition:对应ByteBuddy.redefine()方法。当重新定义一个类时,Byte Buddy可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原理存在的方法就会消失。例如,增强Foo类bar()方法使其直接返回“unknown”字符串,增强结果入下:
public Foo //增强后的Foo定义
String bar() return "unknow";
4.3.2、加载动态生成的类
通过上述三种方法完成类的增强后,我们得到的是DynamicType.Unloaded对象,表示一个未加载的类型,可以使用ClassLoadingStragegy加载此类型。
Byte Buddy提供了几种类加载策略,这些策略定义在ClassLoadingStrategy.Default中:
1)WRAPPER策略:创建一个新的ClassLoader来加载动态生成的类型
2)CHILD_FIRST:创建一个子类优先加载的ClassLoader,即打破了双亲委派模型。
3)INJECTION策略:使用反射将动态生成的类型直接注入到当前的ClassLoader中。
具体使用方式如下:
Class<?> loaded = new ByteBuddy()
.subclass(Object.class) // 生成Object子类
.name("com.demo.Log.Info") // 生成类的名称为"com.demo.Log.info"
.make()
.load(Info.class.getClassLoader(),ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
这里动态生成的com.demo.Log.Info类只是简单继承了Object类,在实际应用中动态生成新类型的目的一般就是为了增强原始的方法。
下面的例子是Byte Buddy如何增强toString()方法:
String str = new ByteBuddy()
.subclass(Object.class)
.name("com.demo.Log.Info")
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World"))
.make()
.load(ByteBuddy.class.getClassLoader())
.getLoaded()
.newInstance()
.toString();
System.out.pritnln(str);
这里,首先关注method()方法,method() 方法可以通过传入的ElementMathers参数匹配多个需要修改的方法,这里的ElementMatchers.named(“toString”) 即为按照方法名匹配的toString()方法,如果存在多个重载方法,则可以通过使用ElementMathcers其他API描述的方法签名,如:
ElementMatchers.named("toString") // 指定方法名称
.and(ElementMatchers.returns(String.class)) //指定方法的返回值
.and(ElementMatchers.takesArguments(0)) // 指定方法参数
接下来关注intercept()方法,通过method()方法拦截到的所有方法都会由intercept()方法指定的Implementation对象决定如何增强。这里的FixValue.value()会将方法的实现修改为固定值,上例中就是返回“Hello world”字符串。
Byte Buddy可以设置多个method()和intercept()方法进行拦截和修改,Byte Buddy会按照栈的顺序来进行拦截。下面例子会进行说明:
一个Info类有三个方法:
class Foo
public String bar() return null;
public String foo() return null;
public String foo(Object o) return null;
接下来,用Byte Buddy动态生成一个Foo的子类,并修改其中的方法:
Foo dynamiceFoo = new ByteBuddy().subclass(Foo.class)
.method(ElementMatchers.isDeclaredBy(Foo.classagent实现apm上报
SkyWalking系列之skywalking go agent配置使用
SkyWalking系列之skywalking go agent配置使用
SkyWalking系列之skywalking go agent配置使用