Pinpoint插件开发手册

你可以编写Pinpoint的Profiler 插件去扩展Profiling目标的覆盖。在进入插件开发之前,最好去查看下Pinpoint插件的跟踪数据记录。

1. 追踪数据

在Pinpoint中,一个事务(Transaction)包含一组Span。每一个Span代表事务(Transaction)经过的单个逻辑点的跟踪。
为了更形象的表达,假设下面有这样一个系统。前端(FrontEnd)服务器接收到用户的请求,然后发送给后端(BackEnd)查询DB数据库,在这些节点中,假设前端(FrontEnd)后端(BackEnd)服务器有配置Pinpoint Agent

链路调用

当一个请求到达后端(BackEnd)时,Pinpoint Agent会生成一个新的事务(Transaction)以及创建一个Span。为了处理请求,前端(FontEnd)会调用后端(BackEnd)服务器。这时候,Pinpoint Agent向调用消息注入事务ID(Transaction ID)(附加一些其他用于传播的数据)。当后端(BackEnd)接收到消息,它将会提取事务ID(Transaction ID)(及其他附加信息)去创建一个Span。其结果就是,所有的Span将会在一个事务(Transaction)中共享其事务ID(Transaction ID)
一个Span记录着重要的方法调用及相关的数据(参数,返回值,及其他),然后将它们封装为SpanEvents,类似于调用栈的表示方式。Span本身以及每个SpanEvents表示一个方法调用。
SpanSpanEvent有很多字段,但是大多数字段都是内部处理的通过Pinpoint Agent及大部分插件开发者不需要关心它们。但是对于这些字段,开发者必须处理的信息如下:

2. Pinpoint 插件结构

Pinpoint PluginTraceMetadataProviderProfilerPlugin组成实现。

  • TraceMetadataProvider :实现提供ServiceTypeAnnotationKeyPinpoint AgentWeb采集器(Collector)
  • ProfilerPlugin:实现通过Pinpoint Agent转化成目标类进行记录跟踪数据。

对于的文件如下:

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

类似如下:


文件目录树

2.1 TraceMetadataProvider

TraceMetadataProvider实现规定ServiceTypesAnnotationKeys

2.1.1 ServiceType

每一个SpanSpanEvent都包含ServiceTypeServiceType表示跟踪的方法属于哪个类库,以及如何处理SpanSpanEvent
下面显示ServiceType的属性。

属性 描述
name ServiceType的名称,必须是唯一的
code short类型的值,必须是唯一的
desc 描述信息
properties 属性

ServiceType值必须使用适当的范围归类它,下面这张表展示了其分类和对应的value范围:

分类 范围
内部使用 0~999
服务 1000~1999
数据库客户端 2000~2999
缓存客户端 8000~8999
RPC客户端 9000~9999
其他 5000~7999

ServiceType码必须唯一,所以,当你想写一个插件并将它公布出去,你必须联系Pinpoint开发团队去分配一个ServiceType码定义。如果你的插件仅是私自的使用,你可以随意使用下边表的数值

种类 范围
Server 1900~1999
DB Client 2900~2999
Cache Client 8900~8999
RPC Client 9900~9999
其他 7500~7999

ServiceType可以具有以下属性:

属性 描述
TERMINAL Span或者SpanEvent调用远程节点,但是该远程节点无法用Pinpoint跟踪
INCLUDE_DESTINATION_ID Span或者SpanEvent记录目的端的iddestination id但远程服务不是可追踪类型
RECORD_STATISTICS Pinpoint Collector应该收集这个SpanSpanEvent的执行时间统计信息

2.1.2 AnnotationKey

你可以给Spans或者SpanEvents标记更多的信息。一个Annotation是一个键值对,其中键是一个AnnotationKey类型,值是一个原始类型,字符串或字节数组。对于常用的Annotation类型,有预先定义了AnnotationKey,但是如果觉得不够,可以在TraceMetadataProvider中定义你自己的键。

属性 描述
name AnnotationKey的名称
code AnnotationKey的唯一整型编号
properties 属性

如果你的公共插件要添加一个新的AnnotationKey,你必须联系Pinpoint 开发团队来分配一个AnnotationKey代码。如果你的插件是私人使用的,你可以选择一个介于900到999之间的值作为AnnotationKey代码。
下面的表展示了AnnotationKey属性:

属性 描述
VIEW_IN_RECORD_SET 展示注解在call tree上
ERROR_API_METADATA 该属性不是插件的

你也可以通过ServiceType来传递AnnotationKeyMatcher (TraceMetadata.addServiceType(ServiceType, AnnotationKeyMatcher) 。如果你通过这种方式传递一个AnnotationKeyMatcher,那么当ServiceTypeSpanSpanEvent在事务调用树中显示时,匹配的Annotation将显示为典型的Annotation

2.2 ProfilerPlugin

ProfilerPlugin修改目标库类来完成采集追踪数据。
ProfilerPlugin按照如下顺序进行:

  1. 在JVM启动时,Pinpoint Agent启动
  2. Pinpoint Agent将会加载plugin目录下的全部插件
  3. Pinpoint Agent将会在每个插件调用ProfilerPlugin.setup(ProfilerPluginSetupContext)
  4. setup()方法中,插件定义了需要转换的类,并注册了一个TransformerCallback
  5. 目标应用启动。
  6. 每当装载一个类时,Pinpoint Agent就会查找为该类注册的TransformerCallback
  7. 如果注册了TransformerCallback,代理将调用它的doInTransform()方法。
  8. TransformerCallback修改目标类的字节代码。(例如,添加拦截器、添加字段等)
  9. 修改后的字节代码返回给JVM,并用返回的字节代码加载类。
  10. 应用继续运行。
  11. 当调用修改的方法时,将调用注入的拦截器的beforeafter方法。
  12. 拦截器记录追踪的数据。

最重要的要考虑的点可以归纳为:

  • 确定哪些方法足够必须,需要去跟踪。
  • 注入拦截器去追踪这些方法。

这些拦截器用来在发送到Collector之前的数据提取,存储和传递跟踪数据。拦截器甚至可以互相协作,共享上下文。插件还可以向目标类添加getter甚至自定义字段帮助去帮助跟踪,以便拦截器可以在执行期间访问它们。Pinpoint插件示例展示了TransformerCallback如何修改类,以及注入的拦截器如何跟踪方法
现在我们将描述拦截器必须做什么来跟踪不同类型的方法。

2.2.1 Plain method

纯方法指的不是节点的顶级方法,或者与远程或异步调用无关的任何方法。示例2展示了如何跟踪这些普通方法。

2.2.2 Top level method of a node

节点的顶级方法是其拦截器在节点中开始新跟踪的方法。这些方法通常是rpc的接受器,跟踪记录为一个Span, ServiceType分类为服务器。
如何记录Span取决于事务是否已经在之前的任何节点上开始。

2.2.2.1 New transaction

如果当前节点是记录事务的第一个节点,则必须发出一个新的事务id并记录它。newtraceobject()将自动处理此任务,因此只需调用它。

2.2.2.2 Continue Transaction

如果请求来自Pinpoint 代理的另一个节点,那么事务将已经发出一个事务id;您必须将下面的数据记录到Span中。(这些数据中的大多数是从前面的节点发送的,通常打包在请求消息中)

名称 描述
transactionId Transaction ID
parentSpanId 父节点的Span ID
parentApplicationName 父节点的应用名
parentApplicationType 父节点的应用类型
rpc 程序名(可选)
endPoint 服务器当前节点地址
remoteAddr 调用者的地址
acceptorHost 客户端使用的服务器地址

Pinpoint使用acceptorHost查找节点之间的调用者-被调用者关系。在大多数情况下,acceptorHostendPoint是相同的。然而,客户机发送请求到的地址有时可能与服务器接收请求(代理)的地址不同。要处理这种情况,您必须记录实际地址的客户端发送请求作为acceptorHost。通常,客户端插件会将这个地址添加到请求消息中,并与事务数据一起添加。
此外,还必须使用上一个节点发出和发送的span id。
有时,前一个节点标记不跟踪的事务。在这种情况下,你不能跟踪事务。
正如你所看到的,客户端插件必须向服务器插件传递许多数据。如何做到这一点取决于协议。
你可以在这里找到一个顶级方法服务器拦截器的示例。

2.2.3 Methods invoking a remote node

调用远程节点的方法的拦截器必须记录以下数据:

名称 描述
endPoint 目的的服务端地址
destinationId 目标的逻辑名称
rpc 调用的程序名(可选)
nextSpanId 下个节点的Span Id(如果需要跟踪通过Pinpoint)

2.2.3.1 If the next node is traceable

如果下一个节点是可跟踪的,那么拦截器必须将以下数据传播到下一个节点。如何传递它们取决于协议,在最坏的情况下可能根本不可能传递它们。

名称 描述
transactionId Transaction ID
parentSpanId 当前节点的Span ID
parentApplicationName 当前节点的应用名
parentApplicationType 当前节点的应用类型

通过匹配客户端跟踪的destinationId和服务器跟踪的acceptorHost,查明调用者和被调用者之间的关系。因此客户端插件必须记录destinationId,而服务器插件必须记录相同值的acceptorHost。如果服务器自己无法获取该值,客户端插件必须将其传递给服务器。
拦截器记录的ServiceType必须来自RPC client类别。
可以在这里找到这些拦截器的示例。

2.2.3.2 If the next node is not traceable

如果下一个节点是不可跟踪的,那么ServiceType必须具有TERMINAL属性。
如果你想记录destinationId,它还必须有INCLUDE_DESTINATION_ID属性。如果你记录destinationId,服务器映射将显示每个destinationId节点,即使它们有相同的端点。
另外,ServiceType必须是一个DB客户端或缓存客户端类别。注意,你不需要担心术语“DB”或“缓存”,因为任何插件跟踪一个客户端库与不可跟踪的目标服务器可能会使用它们。“DB”和“Cache”之间的唯一区别是响应时间直方图的时间范围(“Cache”在直方图中间隔更小)。

2.3.4 Asynchronous task

跟踪对象被绑定到第一次通过ThreadLocal创建它们的线程,每当执行跨越线程边界时,跟踪对象就会丢失到新线程。因此,为了跨线程边界跟踪任务,必须负责将当前跟踪上下文传递给新线程。这是通过将AsyncContext注入到由调用线程和执行线程共享的对象中来实现的。
调用线程从当前跟踪创建一个AsyncContext,并将其注入一个将被传递给执行线程的对象中。然后,执行线程从对象中检索AsyncContext,从中创建一个新的跟踪,并将其绑定到自己的ThreadLocal
因此,必须为两个方法创建拦截器:

  • 一个用于初始化任务(调用线程)。
  • 另一个用于实际处理任务(执行线程)。
    初始化方法的拦截器必须发出一个AsyncContext并将其传递给处理方法。如何传递这个值取决于目标库。在最坏的情况下,可能根本无法通过它。
    然后,处理方法的拦截器必须使用传播的AsyncContext继续跟踪,并将其绑定到自己的线程。但是,强烈建议简单地扩展AsyncContextSpanEventSimpleAroundInterceptor,这样不必手动处理了。
    请记住,因为共享对象必须能够将AsyncContext注入其中,所以必须在类转换期间使用AsyncContextAccessor添加字段。可以在这里找到一个跟踪异步任务的示例。

2.3.5 Case Study: HTTP

HTTP客户机是调用远程节点(客户机)的方法的示例,而HTTP服务器是节点(服务器)的顶级方法的示例。如前所述,客户端插件必须有将事务数据传递给服务器插件的方法来继续跟踪。注意,这个实现是依赖于协议的,HttpClient3插件的HttpMethodBaseExecuteMethodInterceptor和Tomcat插件的StandardHostValveInvokeInterceptor显示了一个HTTP的工作示例:

  1. 将事务数据作为HTTP头传递。您可以在这里找到标题名称
  2. 客户端插件记录服务器的IP:PortdestinationId
  3. 客户端插件将destinationId值作为头传递给服务器作为Header.HTTP_HOST头。
  4. 服务器插件Header.HTTP_HOST头值作为acceptorHost
    必须记住的另一件事是,使用相同协议的所有客户机和服务器必须以相同的方式传递事务数据,以确保兼容性。因此,如果你正在编写其他HTTP客户端或服务器的插件,你的插件必须如上所述记录和传递交易数据。

你可能感兴趣的:(Pinpoint插件开发手册)