基于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配置使用

SkyWalking系列之skywalking go agent配置使用

SkyWalking系列之skywalking go agent 使用问题