pinpoint插件开发实践



640?wx_fmt=png

程序员突破成长的好伙伴 连接技术 接力价值 640?wx_fmt=jpeg


本文为吴振先生投稿至中生代技术


责编 | 姜新城

 第  715  篇技术好文:8888字 | 23分钟阅读


01

Pinpoint是什么

_____

Pinpoint是一款全链路分析工具,提供了无侵入式的调用链监控、方法执行详情查看、应用状态信息监控等功能。基于Google Dapper论文进行的实现。核心思想就是在服务各节点彼此调用的时候,记录并传递一个应用级别的标记,这个标记可以用来关联各个服务节点之间的关系。比如两个节点之间使用 HTTP 作为请求协议的话,那么这些标记就会被加入到HTTP头中,各应用的Agent在进行上报的时候,将该标记以及对应的上下级应用上报到Pinpoint collector中,通过该标记标识请求,并将各个应用串联成完整的调用链路。

Pinpoint的特点如下

分布式事务跟踪,跟踪跨分布式应用的消息

自动检测应用拓扑,帮助你搞清楚应用的架构

水平扩展以便支持大规模服务器集群

提供代码级别的可见性以便轻松定位失败点和瓶颈

使用字节码增强技术,添加新功能而无需修改代码

Pinpoint针对不同的组件提供了丰富的插件,其支持以下模块:

JDK 6+

Tomcat 6/7/8, Jetty 8/9, JBoss EAP 6, Resin 4, Websphere 6/7/8, Vertx 3.3/3.4/3.5

Spring, Spring Boot (Embedded Tomcat, Jetty)

Apache HTTP Client 3.x/4.x, JDK HttpConnector, GoogleHttpClient, OkHttpClient, NingAsyncHttpClient

Thrift Client, Thrift Service, DUBBO PROVIDER, DUBBO CONSUMER

MySQL, Oracle, MSSQL, CUBRID,POSTGRESQL, MARIA

Arcus, Memcached, Redis, CASSANDRA

iBATIS, MyBatis

DBCP, DBCP2, HIKARICP

gson, Jackson, Json Lib

log4j, Logback


02

插件知识和相关数据结合

_____

pinpoint插件能够在代码级别对感兴趣的方法进行拦截,可以针对业务代码,第三方包等,如记录方法的执行时间,参数,方法返回结果,在RPC调用中插入标识ID以记录调用关系等, pinpoint插件很容易扩展,官方也提供了很多插件,基本覆盖了常用的组件,如hystrix,dubbo等,部署即可用。

1、插件结构

Pinpoint 插件由TraceMetadataProvider和ProfilerPlugin的实现组成TraceMetadataProvider实现 给pinpoint agent,collector,web组件提供ServiceType和AnnotationKey,ProfilerPlugin 实现用于agent转换目标类以记录追踪数据。

Pinpoint插件以jar文件的形式部署。Agent在plugin目录下用ServiceLoader 搜索TraceMetadataProvider和ProfilerPlugin的实现,web和collector在 WEB-INF/lib目录下搜索,ServiceLoader要求provider配置文件存在于META-INF/services目录下,所以在plugin jar中必须放置以下文件,实现类通过 Java 的服务发现机制进行加载。

1META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin
2META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider


2. 介绍下几种关键的类

2.1 TraceMetadataProvider

TraceMetadataProvider 实现类中提供ServiceTypes和AnnotationKeys。

2.1.1 ServiceTypes

每个 Span 和 SpanEvent 都包含一个 ServiceType,这个ServiceType表示跟踪方法所属的库,以及跟踪它的Span和spanevent应该如何处理。下表显示ServiceType包含哪些属性:


属性

描述

Name

ServiceType 的名称,必须唯一


Code

ServiceType 的编码,短整形,必须唯一


Desc

描述


Properties

属性


Pinpoint 为了尽量压缩 Agent 到 Collector 数据包的大小,ServiceType 被设计成不是以序列化字符串的形式发送的,而是以整形数字发送的 (code 字段),这就需要建立一个映射关系,将 code 转换成对应的 ServiceType 实例,这个映射机制就是由 TraceMetadataProvider 负责的。

ServiceType code必须是惟一的。如果要编写一个将被公开共享的插件,就必须联系pinpoint团队来获得分配的ServiceType code。如果所开发的插件是私有的,那可以从下面的表格中选择一个ServiceType code,如一会我们要展示的示例中一样。

公开的ServiceType Code范围


类型

范围

Internal Use

0 ~ 999


Server

1000 ~ 1999


DB Client

2000 ~ 2999


Cache Client

8000 ~ 8999


RPC Client

9000 ~ 9999


Others

5000 ~ 7999


私有的ServiceType Code范围


类型

范围

Server

1900 ~ 1999


DB Client

2900 ~ 2999


Cache Client

8900 ~ 8999


RPC Client

9900 ~ 9999


Others

7500 ~ 7999


ServiceType还有一些属性

属性

描述

TERMINAL

调用远程方法但是目标节点不能被pinpoint追踪

INCLUDE_DESTINATION_ID

记录destination id 但是远程服务器是不可被追踪类型

RECORD_STATISTICS

Pinpoint收集器收集执行期间的统计信息

2.1.2、AnnotationKey

Annotation 是包含在 Span 和 SpanEvent 中的更详尽的数据,以键值对的形式存在,键就是AnnotationKey,值是基本类型,String或者byte[]。Pinpoint 内置了很多的 AnnotationKey,如果不够用的话也可以通过 TraceMetadataProvider 来自定义。AnnotationKey 的数据结构如下:


属性

描述

Name

AnnotationKey 名称


Code

AnnotationKey 编码,整形,必须唯一


Properties

附加属性


同 ServiceType 的 code 字段一样,AnnotationKey的 code 字段也是全局唯一的。如果开发的插件包含一个公开的AnnotationKey,就要联系pinpoint团队分配一个AnnotationKey code,如果是私有插件,那可以在900到999之间选择一个值作为code。

属性

描述

VIEW_IN_RECORD_SET

在Call tree中显示这个注解 

ERROR_API_METADATA

这个不是插件注解

2.2、ProfilerPlugin

ProfilerPlugin修改目标库的类来收集跟踪数据。插件的工作原理:

1. Pinpoint Agent 随 JVM 一起启动

2. Agent 加载所有 plugin 目录下的插件

3. Agent 调用已加载的插件的 ProfilerPlugin.setup(ProfilerPluginSetupContext) 方法

4. 在 setup 方法中,插件定义那些需要被转换的类并注册回调TransformerCallback

5. 目标应用启动

6. 每当类被加载的时候,Pinpoint Agent 会寻找注册到该类的回调 TransformerCallback

7. 如果 TransformerCallback 被注册,Agent 就调用它的 doInTransform 方法

8. TransformerCallback 修改目标类的字节码 (例如添加拦截器、字段等)

9. 修改后的代码返回到 JVM,类被加载的时候使用修改后的字节码

10. 应用程序继续

11. 当调用到被修改的方法的时候,已注入的拦截器的 before 和 after 方法会被调用

12. 拦截器记录追踪数据最重要的几点可以归结为

1)找出哪些方法值得跟踪。

2)注入拦截器来实际跟踪这些方法。

这些拦截器用于提取、存储和传递跟踪数据,然后将其发送给收集器。拦截器甚至可以相互协作,在它们之间共享上下文。插件还可以通过向目标类添加getter或定制字段来帮助跟踪,以便拦截器在执行期间可以访问它们。


03

字节码注入怎么工作的

_____

640?wx_fmt=png

由于字节码技术必须处理java字节码,它会增加开发的风险而降低生产效率。此外,研发人员容易犯错误。在Pinpoint中,通过拦截器抽象提高了生产力和可访问性。

Pinpoint中注入必要的代码,通过在类装入时插入应用程序代码来跟踪分布式事务和性能信息。由于跟踪代码直接注入到应用程序代码中,因此提高了性能。

在 Pinpoint中,API截取和数据记录是分离的。如上图,拦截器被注入到我们希望跟踪的方法中,在该方法前后调用before()和after()处理数据记录。通过字节码指令, Pinpoint Agent可以仅从必要的方法记录数据,从而使分析数据的大小变得紧凑。

下面就根据以上原理来实现一个插件,该插件能够拦截配置的方法。

先定义些常量类型,设置ServiceType 和 相应的code,AnnotationKey和相应的code

1public interface GeneralConfigConstants {
2   //定义service type和code
3ServiceType GENERAL_CONFIG_SERVICE_TYPE = ServiceTypeFactory.of(7510"GENERAL_CONFIG", RECORD_STATISTICS);
4   //定义annotation key 和 code
5AnnotationKey GENERAL_CONFIG_RESULT     = AnnotationKeyFactory.of(902"general.config.result", AnnotationKeyProperty.VIEW_IN_RECORD_SET);
6   String GENERAL_CONFIG_INTERCEPTOR       = "com.navercorp.pinpoint.plugin.general.config.interceptor.GeneralConfigInterceptor";
7   String PINPOINT_CONFIG_PATH             = "/pinpoint/config/project.properties";
8}

定义数据类型类用于读取配置文件中的配置,文件中的配置规则为

有参数方法

package.Clazz.MethodArgs=arg1,arg2  

无参数方法

package.Clazz.MethodArgs

每行一个配置项

1public class ConfigInfo {
2   /**
3    * e.g. com.bestpay.demo.Clazz.MethodArgs=arg1,arg2
4           com.bestpay.demo.Clazz2.Method2   (no args)
5    */

6   private String clazz;
7   private List<MethodArgs> methodArgs;
8
9   public String getClazz({
10       return clazz;
11   }
12
13   public void setClazz(String clazz{
14       this.clazz = clazz;
15   }
16
17   public List<MethodArgs> getMethodArgs({
18       return methodArgs;
19   }
20
21   public void setMethodArgs(List<MethodArgs> methodArgs{
22       this.methodArgs = methodArgs;
23   }
24}
25public class MethodArgs {
26   private String method;
27   private String[] args;
28
29   public String getMethod({
30       return method;
31   }
32
33   public void setMethod(String method{
34       this.method = method;
35   }
36   public String[] getArgs({
37       return args;
38   }
39   public void setArgs(String[] args{
40       this.args = args;
41   }
42}

定义GeneralConfigMetadataProvider提供ServiceType元数据

1public class GeneralConfigMetadataProvider implements TraceMetadataProvider {
2   @Override
3   public void setup(TraceMetadataSetupContext context) {
4       context.addServiceType(GeneralConfigConstants.GENERAL_CONFIG_SERVICE_TYPE);
5context.addAnnotationKey(GeneralConfigConstants.GENERAL_CONFIG_ARGS);
6        context.addAnnotationKey(GeneralConfigConstants.GENERAL_CONFIG_RESULT);
7   }
8}

定义针对方法的拦截器GeneralConfigInterceptor继承自SpanEventSimpleAroundInterceptorForPlugin,这里在方法执行后简单记录方法的名称,参数,返回值

1public class GeneralConfigInterceptor extends SpanEventSimpleAroundInterceptorForPlugin {
2   private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
3   public GeneralConfigInterceptor(TraceContext context, MethodDescriptor methodDescriptor){
4       super(context, methodDescriptor);
5   }
6
7   @Override
8   protected void doInBeforeTrace(SpanEventRecorder recorder, Object target, Object[] args) {
9   }
10   @Override
11   protected void doInAfterTrace(SpanEventRecorder recorder, Object target, Object[] args, Object result, Throwable throwable) {
12recorder.recordServiceType(GeneralConfigConstants.GENERAL_CONFIG_SERVICE_TYPE);
13if (args != null && ArrayUtils.hasLength(args)){
14   recorder.recordApi(getMethodDescriptor(), args);
15   recorder.recordAttribute(GeneralConfigConstants.GENERAL_CONFIG_ARGS, args);
16else {
17   recorder.recordApi(getMethodDescriptor());
18}
19recorder.recordAttribute(GeneralConfigConstants.GENERAL_CONFIG_RESULT, result);
20recorder.recordException(throwable);
21}

最后重要的是我们的插件,传入配置和转换模板transformTemplate

1public class GeneralConfigPlugin implements ProfilerPluginTransformTemplateAware{
2   private final PLogger logger = PLoggerFactory.getLogger(this.getClass());
3   private GeneralConfigConfiguration configuration;
4   private TransformTemplate transformTemplate;
5   @Override
6   public void setTransformTemplate(TransformTemplate transformTemplate) {
7       this.transformTemplate = transformTemplate;
8   }
9   @Override
10   public void setup(ProfilerPluginSetupContext context) {
11       configuration = new GeneralConfigConfiguration(context.getConfig());
12       if (!configuration.isGeneralConfigEnabled()){
13           return;
14       }
15       List<ConfigInfo> list = PropertyLoader.propertiesToList(null);
16       addTransformers(list);
17   }
18
19   private void addTransformers(List<ConfigInfo> configInfos){
20       if (configInfos == null) {
21           logger.error("No configuration for general config plugin");
22return;
23       }
24       for (ConfigInfo configInfo : configInfos){
25           addTransformer(configInfo);
26       }
27   }
28   private void addTransformer(final ConfigInfo configInfo) {
29       transformTemplate.transform(configInfo.getClazz(), new TransformCallback() {
30           @Override
31           public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
32               InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
33               List<MethodArgs> methodArgsList = configInfo.getMethodArgs();
34               for (MethodArgs methodArgs : methodArgsList) {
35                   InstrumentMethod method = null;
36//处理有参数方法                    
37if (methodArgs.getArgs() != null && methodArgs.getArgs().length > 0) {
38                       method = target.getDeclaredMethod(methodArgs.getMethod(), methodArgs.getArgs());
39                   } else {
40//处理无参数方法
41                       method = target.getDeclaredMethod(methodArgs.getMethod());
42                   }
43                   method.addInterceptor(GeneralConfigConstants.GENERAL_CONFIG_INTERCEPTOR);
44               }
45               return target.toBytecode();
46           }
47       });
48   }
49}

META-INF/services目录下添加插件和元数据文件

1com.navercorp.pinpoint.plugin.ProfilerPlugin
2--com.navercorp.pinpoint.plugin.general.config.GeneralConfigPlugin
3com.navercorp.pinpoint.common.trace.TraceMetadataProvider
4--com.navercorp.pinpoint.plugin.general.config.GeneralConfigMetadataProvider

当以上核心的代码写完后,将插件以jar的形式部署在准备好的agent目录中,启动项目,project.propertie中配置要拦截的方法如com.bestpay.middleware.service.StudentManagerImpl.getAllStudents。在请求的路径中能看到以下的信息。配置我们配置的方法被拦截到了。

640?wx_fmt=png

至此,我们的小插件讲解就结束了,此为抛砖引玉,Pinpoint提供了丰富的插件开发 API,如拦截异步方法、调用链跟踪、拦截器之间共享数据等,有兴趣的同学可进一步探索。

640?wx_fmt=png

目前60000+人已关注加入我们

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif

640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif 640?wx_fmt=gif

推荐阅读 盒子科技刘恒:聚合支付系统演讲
手把手教你搭建一个基于Java的分布式爬虫系统
知识付费时代,程序员,你的知识在哪里?
深入浅出分布式缓存的通用方法
蚂蚁金服开源 | 在 Spring Boot 中集成 SOFABoot 类隔离能力

640?wx_fmt=jpeg

中生代技术

每天早上,推送有营养的干货文章;

总覆盖会员60000+人;资深架构、总监等职位以上3000+人。

定期在线分享超过100期,线下技术沙龙超过70次、覆盖20多个等城市!

关注技术架构、研发管理、互联网金融、电商、大数据、区块链、人工智能等方向!


加入中生代群聊,请添加白明微信:zsdwyq,注明姓名、职称和技术方向,通过后加入中生代技术群,和群友们共同学习成长!

猜你喜欢

转载自blog.csdn.net/k6T9Q8XKs6iIkZPPIFq/article/details/81009452