Pinpoint源码解析(三)

一、技术概述

1.1、架构组成

Pinpoint 主要由 3 个组件外加 Hbase 数据库组成,三个组件分别为:Agent、Collector 和 Web UI。

1.2、系统特色

分布式交易追踪,追踪分布式系统中穿梭的消息

自动侦测应用程序拓扑,以帮助指明应用程序的配置

横向扩展以支持大规模的服务器组

提供代码级别的可见性,以方便识别故障点和瓶颈

使用字节码注入技术,无需修改代码就可以添加新功能

1.3、分布式追踪系统如何工作

无论 Pinpoint 还是 Zipkin,其实现都是基于 Google Dapper 的论文。其核心思想就是在服务各节点彼此调用的时候,记录并传递一个应用级别的标记,这个标记可以用来关联各个服务节点之间的关系。比如两个节点之间使用 HTTP 作为请求协议的话,那么这些标记就会被加入到 HTTP 头中。因此如何传递这些标记是与节点之间使用的通讯协议有关的,有些协议就很容易加入这样的内容,但有些协议就相对困难甚至不可能,因此这一点就直接决定了实现分布式追踪系统的难度。

1.4、Pinpoint 的数据结构

Pinpoint 消息的数据结构主要包含三种类型 Span,Trace 和 TraceId。

Span 是最基本的调用追踪单元,当远程调用到达的时候,Span 指代处理该调用的作业,并且携带追踪数据。为了实现代码级别的可见性,Span 下面还包含一层 SpanEvent 的数据结构。每个 Span 都包含一个 SpanId。

Trace 是一组相互关联的 Span 集合,同一个 Trace 下的 Span 共享一个 TransactionId,而且会按照 SpanId 和 ParentSpanId 排列成一棵有层级关系的树形结构。

TraceId 是 TransactionId、SpanId 和 ParentSpanId 的组合。TransactionId(TxId)是一个交易下的横跨整个分布式系统收发消息的 ID,其必须在整个服务器组中是全局唯一的。也就是说 TransactionId 识别了整个调用链;SpanId(SpanId)是处理远程调用作业的 ID,当一个调用到达一个节点的时候随即产生;ParentSpanId(pSpanId)顾名思义,就是产生当前 Span 的调用方 Span 的 ID。如果一个节点是交易的最初发起方,其 ParentSpanId 是 -1,以标志其是整个交易的根 Span。下图能够比较直观的说明这些 ID 结构之间的关系。

1.5、如何使用 Java Agent

Pinpoint 的优势就在于其使用 Java Agent 的方式向节点应用注入字节码,而不是直接修改源代码。因此部署一个节点就变得非常容易,只需要在程序启动的时候加入如下一些启动参数:

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar-Dpinpoint.agentId=-Dpinpoint.applicationName=

1.6、代码注入是如何工作的

Pinpoint 对代码注入的封装非常类似 AOP,当一个类被加载的时候会通过 Interceptor 向指定的方法前后注入 before 和 after 逻辑,在这些逻辑中可以获取系统运行的状态,并通过 TraceContext 创建 Trace 消息,并发送给 Pinpoint 服务器。但与 AOP 不同的是,Pinpoint 在封装的时候考虑到了更多与目标代码的交互能力,因此用 Pinpoint 提供的 API 来编写代码会比 AOP 更加容易和专业。(这些内容后面会有更详细说明)

1.7、Pinpoint 的应用实例

下图展现了两个 Tomcat 服务器应用了 Pinpoint 之后,被收集到的追踪数据。

二、Agent 插件开发

开发 Pinpoint Agent 只需要关注两个接口:TraceMetadataProvider 和 ProfilerPlugin,实现类通过 Java 的服务发现机制进行加载。

2.1、ServiceLoader 配置

Pinpoint 的插件是以 jar 包的形式部署的,为了使得 Pinpoint Agent 能够定位到 TraceMetadataProvider 和 ProfilerPlugin 两个接口的实现,需要在 META-INF/services 目录下创建两个文件:

META-INF/services/com.navercorp.pinpoint.common.trace.TraceMetadataProvider

META-INF/services/com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin

这两个文件中的每一行都写明对应实现类的全名称即可。

2.2、TraceMetadataProvider

TraceMetadataProvider 提供了对 ServiceType 和 AnnotationKey 的管理。

2.2.1、ServiceType

每个 Span 和 SpanEvent 都包含一个 ServiceType,用来标明他们属于哪一个库(Jetty、MySQL JDBC Client 或者 Apache HTTP Client 等),以及追踪此类型服务的 Span 和 SpanEvent 该如何被处理。ServiceType 的数据结构如下:

属性描述

nameServiceType 的名称,必须唯一

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

desc描述

properties附加属性

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

ServiceType 的 code 必须全局唯一,为了避免冲突,Pinpoint 官方对这个映射表进行了严格的管理,如果所开发的插件想要声明新的映射关系,需要通知 Pinpoint 团队,以便对此映射表进行更新和发布。与私有 IP 地址段一样,Pinpoint 团队也保留了一段私有区域可供开发内部服务的时候使用。具体的 ID 范围参照表如下:

ServiceType Code 全部范围

类型范围

Internal Use0 ~ 999

Server1000 ~ 1999

DB Client2000 ~ 2999

Cache Client8000 ~ 8999

RPC Client9000 ~ 9999

Others5000 ~ 7999

ServiceType Code 私有区域范围

类型范围

Server1900 ~ 1999

DB Client2900 ~ 2999

Cache Client8900 ~ 8999

RPC Client9900 ~ 9999

Others7500 ~ 7999

2.2.2、AnnotationKey

Annotation 是包含在 Span 和 SpanEvent 中的更详尽的数据,以键值对的形式存在,键就是 AnnotationKey,值可以是字符串或字节数组。Pinpoint 内置了很多的 AnnotationKey,如果不够用的话也可以通过 TraceMetadataProvider 来自定义。AnnotationKey 的数据结构如下:

属性描述

nameAnnotationKey 的名称

codeAnnotationKey 的编码,整形,必须唯一

properties附加属性

同 ServiceType 的 code 字段一样,AnnotationKey 的 code 也是全局唯一 的,Pinpoint 团队给出的私有区域范围是 900 到 999。

2.2.3、TraceMetadataProvider 接口

TraceMetadataProvider 接口只有一个 setup 方法,此方法接收一个 TraceMetadataSetupContext 类型的参数,该类型有三个方法:

方法描述

addServiceType(ServiceType)注册 ServiceType

addServiceType(ServiceType, AnnotationKeyMatcher)注册 ServiceType,并将匹配 AnnotationKeyMatcher 的 AnnotationKey 作为此 ServiceType 的典型注解,这些典型注解会显示在瀑布视图的 Argument 列中

addAnnotationKey(AnnotationKey)注册 AnnotationKey,这里注册的 AnnotationKey 会被标记为 VIEW_IN_RECORD_SET,显示在瀑布视图中是以单独一行显示的,且前面有一个蓝色的 i 图标

详细使用方法可以参考官方提供的样例文件SampleTraceMetadataProvider

2.3、ProfilerPlugin

ProfilerPlugin 通过字节码注入的方式拦截目标代码以实现跟踪数据的收集。

2.3.1、插件的工作原理

Pinpoint Agent 随 JVM 一起启动

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

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

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

目标应用启动

当类被加载的时候,Pinpoint Agent 会寻找注册到该类的 TransformerCallback

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

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

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

应用程序继续

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

拦截器记录追踪数据

Pinpoint 插件的工作原理看似跟 AOP 非常相似,但还是有一些区别和自身的特色:

因为 Pinpoint 需要处理的注入场景比较单一,因此他提供的注入 API 相对简单;而 AOP 为了要处理各种可能的切面情况,Pointcut 被设计得非常复杂。

Pinpoint 插件拦截是通过拦截器的 before 和 after 方法实现的,很像 around 切面,如果不想执行其中一个方法,可以通过 @IgnoreMethod 注解来忽略。

Pinpoint 的拦截器可以任意拦截方法,因此被拦截的方法之间可能会有调用关系,这会导致追踪数据被重复收集,因此 Pinpoint 提供了 Scope 和 ExecutionPolicy 功能。在一个 Scope 内,可以定义拦截器的执行策略:是每次都执行(ExecutionPolicy.ALWAYS),还是在没有更外层的拦截器存在的时候执行(ExecutionPolicy.BOUNDARY),或者必须在有外层拦截器存在的时候执行(ExecutionPolicy.INTERNAL)。具体请参考这个样例

在一个 Scope 内的拦截器彼此还可以传递数据。同一个 Scope 内的拦截器共享一个 InterceptorScopeInvocation 对象,可以使用他来交换数据。参考样例

除了拦截方法以外,Pinpoint 还可以向目标类中注入字段以及 getter 和 setter 方法,可以使用它们来保存一些上下文的数据。

通过上述内容可以了解,如果要编写一个 Pinpoint 的插件,除了要对目标代码的调用逻辑有较深入的理解,还必须得设计好上下文数据如何存储、如何传递,以及如何通过 Scope 避免信息被重复收集等问题。这些问题在 AOP 的场景下也会存在,只是 Pinpoint 提供了更加一致和便捷的解决方案,而 AOP 的就要自己去考虑这些问题了。

2.3.2、方法拦截

如前文所述,Pinpoint 插件需要实现 ProfilerPlugin 接口,该接口只有一个 setup(ProfilerPluginSetupContext) 方法。为了更容易的操作 Pinpoint 的代码注入 API,还需要实现一个 TransformTemplateAware 的接口,该接口会注入 TransformTemplate 类。

publicclassSamplePluginimplementsProfilerPlugin,TransformTemplateAware{privateTransformTemplatetransformTemplate;@Overridepublicvoidsetup(ProfilerPluginSetupContextcontext){  }@OverridepublicvoidsetTransformTemplate(TransformTemplate  transformTemplate){this.transformTemplate = transformTemplate;  }}

ProfilerPluginSetupContext 有两个方法:getConfig() 和 addApplicationTypeDetector(ApplicationTypeDetector…)。第一个方法用来获取 ProfilerConfig 对象,该对象保存了所有插件的配置信息,而第二个方法用来添加 ApplicationTypeDetector。ApplicationTypeDetector 是用来自动检测节点所运行服务的类型的。例如在 pinpoint-tomcat-plugin 项目中,有 TomcatDetector 类,这个类的作用是通过如下检测来确定当前服务为 Tomcat 的:

检查 main class 是不是 org.apache.catalina.startup.Bootstrap

检查是否有系统变量 catalina.home

检查是否存在某个指定的类,这里也是 org.apache.catalina.startup.Bootstrap

如果这三个条件都满足,就把当前节点的 ServiceType 设置为 Tomcat。

TransformTemplate 只有一个方法 transform(String, TransformCallback),第一个参数是需要被转换的类的全名称,而第二个参数就是 4.3.1 章节中提到的 TransformCallback 接口,这个接口也只有一个方法叫 doInTransform,所有的注入逻辑都在这里完成。

publicbyte[] doInTransform(Instrumentorinstrumentor,        ClassLoaderclassLoader,        String className,Class classBeingRedefined,        ProtectionDomainprotectionDomain,byte[] classfileBuffer)throwsInstrumentException {// 1. Get InstrumentClass of the target classInstrumentClasstarget = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);// 2. Get InstrumentMethod of the target method.InstrumentMethodtargetMethod = target.getDeclaredMethod("targetMethod","java.lang.String");// 3. Add interceptor. The first argument is FQN of the interceptor class,// followed by arguments for the interceptor's constructor.targetMethod.addInterceptor("com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor", va(SamplePluginConstants.MY_SERVICE_TYPE));// 4. Return resulting byte code.returntarget.toBytecode();}

注入过程是从获取 InstrumentClass 类开始的。

如果想要拦截一个方法,或者是添加字段以及 getter、setter 方法,就可以调用 InstrumentClass 对应的 API 来实现,这里是获取了一个签名为 targetMethod(String) 的方法,返回的对象是 InstrumentMethod 类型。

调用 InstrumentMethod 的 addInterceptor 方法注入拦截器,所有跟踪信息的收集行为都是在拦截器中实现的,这里添加的拦截器是 com.navercorp.pinpoint.bootstrap.interceptor.BasicMethodInterceptor,这是 Pinpoint 框架默认提供的一个现成的拦截器,里面收集了一些 targetMethod 的调用信息。后面的 va 是一个静态方法,即可变参数列表,va 中给出的参数会传递到 BasicMethodInterceptor 的构造方法中。

调用 InstrumentClass.toBytecode() 方法即可返回注入后的字节码,剩下的事情就是 Pinpoint Agent 自己来完成的了。

BasicMethodInterceptor 类仅提供了对方法调用信息的简单收集,只收集方法的名称、参数、返回值以及是否产生异常等等。在某些复杂的场景下,我们会需要收集更多的信息,如当前登录用户、线程池、数据库查询语句以及任何跟中间件功能有关的信息,这就需要我们定义自己的 Interceptor 类。

以上内容请参考该样例

Interceptor 是一个标记接口,真正有意义的是 AroundInterceptor 接口,该接口定义了如下两个方法:

publicinterfaceAroundInterceptorextendsInterceptor{voidbefore(Objecttarget,Object[] args);voidafter(Objecttarget,Object[] args,Objectresult, Throwablethrowable);}

为了应对被拦截方法的不同个数的参数列表,AroundInterceptor 还有若干子接口:AroundInterceptor0, AroundInterceptor1,…,AroundInterceptor5,分别对应没有参数,一个参数,到 5 个参数的方法。实现 Interceptor 接口的时候要提供一个如下的构造方法:

publicRecordArgsAndReturnValueInterceptor(TraceContexttraceContext,        MethodDescriptordescriptor) {this.traceContext = traceContext;this.descriptor = descriptor;}

TraceContext 和 MethodDescriptor 会被 Pinpoint Agent 运行时注入进来,当然也可以添加额外的参数,这些额外的参数,需要在 addInterceptor 的时候指定,就像上文中关于 va 的描述那样。

有了 TraceContext 对象,就可以开始收集信息了。调用 traceContext.getCurrentTraceObject() 方法可以获取当前的 Trace,再调用 trace.traceBlockBegin() 就开始记录一个新的 Trace 块了(这里我理解应该就是 Span 了)。在 traceBlockBegin 以后,可以 调用 currentSpanEventRecorder 获取 SpanEventRecorder 对象,这个对象提供了诸如 recordServiceType、recordApi、recordException 和 recordAttribute 等方法,可以记录方法的有关信息。但是 SpanEventRecorder 并没有提供 recordReturnValue 这样的方法,只能通过 recordAttribute 来记录。所有自己扩展的信息也是通过 recordAttribute 来记录的。最后所有信息记录完成就调用 traceBlockEnd() 方法关闭区块。

以上内容请参考该样例

三、总结

其实 Pinpint 的插件开发 API 还提供了非常丰富的能力,如拦截异步方法、调用链跟踪、拦截器之间共享数据等等,但原理都是基于上述这些内容,只是调用了更复杂的 API 而已。具体代码可以参考官方提供的样例项目,里面有非常详尽的代码及注释,相信理解了上面的内容,再看这个代码就不会有任何困难了。

转自:http://www.tuicool.com/articles/uQZB7nV

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,455评论 0 4
  • 文/疯狂稻草人 (一) 我有小小心事一抹 摊开,却比满天星辰还多 (二) 心事,是夜里出没的鬼 压在心头,挣不脱 ...
    疯狂稻草人阅读 285评论 4 2
  • 总是觉得卫衣就是运动服,潮则潮矣,跟优雅甜美沾不上边?如果你这么想,那么你就out啦!卫衣不仅能够和裙子搭配,还能...
    meimeida200阅读 237评论 0 1
  • “前人的思考,我们的阶梯。”本系列为得到APP中《5分钟商学院》专栏笔记,主讲人刘润,号称中国最贵的商业顾问之一。...
    海涛笔记阅读 3,511评论 0 2