发布于 

pinpoint插件原理学习

主要节选翻译自官方文档 Pinpoint Plugin Developer Guide

在pinpoint里面,一个“事务”(或者说请求?)是由一组spans构成的。每个span代表了请求经历的一个单独的逻辑节点。

一个span记录了重要方法的信息和它们的相关数据,请求,返回值等,概括它们为spanEvents. 一个span和它包含的所有spanEvents代表了一个方法调用。

PinPoint插件结构

pinpoint插件是由TraceMetadataProviderProfilerPlugin的实现组成。TraceMetadataProvider的实现提供了ServiceType和AnnotationKey给PinPoint agent/web/collector. ProfilerPlugin的实现被agent使用,用来修改目标类,记录跟踪数据。

插件放在agent的plugin目录,web和collector的WEB-INF/lib目录。插件需要在资源文件夹的META-INF/services目录中,声明自己实现的TraceMetadataProvider和ProfilerPlugin。

TraceMetadataProvider

ServiceType

每个SpanSpanEvent都包含了ServiceType, 代表了正在追踪的方法属于谁。web也需要通过serviceType来展示。

需要注意的是,每个ServiceType由name,code,description,properties组成。其中,code是不能乱用的,每个serviceType的code唯一。因此,pinpoint预留了一部分code范围供我们自己开发使用。

AnnotationKey

同样的,AnnotationKey也有自己的唯一code, 900-999是pinpoint团队为我们预留的。

ProfilerPlugin

profilerPlugin修改目标类以收集跟踪数据。

它工作的步骤是:

  1. jvm 启动时启动pinpoint agent
  2. agent加载插件目录下所有的插件
  3. agent调用每个插件的ProfilerPlugin.setup(ProfilerPluginsetupContext)方法
  4. 在setup方法里面,插件决定是否要转换类,并且注册一个transformerCallerBack
  5. 应用启动
  6. 每次类加载的时候,agent会寻找注册在该类上的TransformerCallback
  7. 如果找到了,agent会调用callback里面的doInTransform方法
  8. TransformerCallerBack会修改目标类的字节码,添加拦截,添加字段等等
  9. 修改后的字节码被返回给jvm,然后类被加载
  10. 应用继续
  11. 当一个修改后的方法被调用时,注入的拦截器beforeafter方法会被调用
  12. 拦截器纪录追踪数据

最重要的是考虑:

  1. 找出哪些方法可以足够确保追踪
  2. 注入拦截器,追踪方法
    拦截器之间甚至可能相互协作,交换上下文。这需要我们自己去考虑。

Plain method 简单方法

Top level method 顶层方法

一个节点的顶层方法是它的拦截器开始新的追踪的地方。典型的方法就是rpc的拦截器,追踪被标记为带着servicetypespan. 至于怎么记录span,取决于在这个节点之前的其他节点,“事务”是否已经开始记录。

新的事务

如果这个节点是记录本次事务的第一个节点,那么你必须声明一个新的transaction id并且纪录它。TraceContext.newTraceObject()会自动处理这个任务,调用即可。

接力事务

如果请求从另一个pinpoint agent追踪的节点过来,那么应该已经存在transaction id信息,你需要记录下面的数据给span。(大多数数据是从上一个节点发过来的,存在请求信息里)

name description
transactionId Transaction ID
parentSpanId Span ID of the previous node
parentApplicationName Application name of the previous node
parentApplicationType Application type of the previous node
rpc Procedure name (Optional)
endPoint Server(current node) address
remoteAddr Client address
acceptorHost Server address that the client used

pinpoint通过acceptorHost找到节点间的调用关系。大多数情况下,endpoint和acceptorHost应该是一样的,然而有时候会不同,比如通过代理。在这种情况下,你需要记录客户端实际发送请求去的那个地址作为acceptorHost.一般来说,客户端插件会将地址和事务信息添加到请求信息中。

methods invoking a remote node 调用远程节点的方法

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

name description
endPoint target server address
destinationId Logical name of the target
rpc invoking target procedure name(optional)
nextSpanId span id that will be used by next node’s span(if next node is traceable by pinpoint)

下一个节点是否可追踪,影响了拦截器的实现。是否可追踪在这里意味着可能性。比如,一个http客户端的下一个节点是http服务器。pinpoint并不追踪所有的http服务器,但是是有可能追踪的。在这种情况下,http客户端的下一个节点就是可追踪的。另一方面,mysql jdbc的下一个节点,mysql 数据库,是不可追踪的。

如果下一节点可追踪

如果下一个节点可追踪,那么需要传递下面的数据给下一节点。怎么传递是协议独立的,最差的情况下不能传递。

name description
transactionId Transaction ID
parentApplicationName Application name of current node
parentApplicationType Application type of current node
parentSpanId span id of trace at current node
nextSpanId Span id that will be used by the next node’s span(same value with nextSpanId of above table)

pinpoint通过匹配destinationId和acceptorHost来找到调用关系。因此,客户端插件需要记录destinationId, 服务端插件需要用同样的值记录acceptorHost。如果服务端没法自己拿到这个值,那么客户端需要将这个值传递给服务端。

拦截器记录的ServiceType必须来自RPC客户端分类。

如果下一个节点不可追踪

如果下一个节点不可追踪,ServiceType必须有TERMINAL属性。
如果你想记录destinationId, 必须有INCLUDE_DESTINATION_ID属性.如果你记录了destinationId, server map会为每个destinationId展示一个节点,即使它们的endPoint一样。

异步任务

异步任务意味着初始化任务的线程和处理任务的线程不是同一个。如果想追踪异步任务,必须给两个方法创建拦截器 1)初始化任务 2)真正处理任务

初始化方法的拦截器必须分发一个AsyncTraceId 并且传递给处理方法。至于怎么传递需要根据目标库来确定,有可能根本传不了。

处理方法需要使用这个传递过来的AsyncTraceId继续跟踪。你不需要手动处理这种传递,只需要简单地扩展SpanAsyncEventSimpleAroundInterceptor 来写拦截器就足够了。但为了初始化这个传递你需要注入一个字段,使用AsyncTraceIdAccessor,到处理方法所在的类,并设置AsyncTraceId 到这个字段,在处理方法调用之前。

案例学习:http

HTTP客户端是一个“方法调用远程节点”的例子,http服务器是一个顶层节点方法。如上所说,客户端插件必须找到办法将事务数据传递给服务端插件来继续这种追踪。

  1. 使用http头来传递事务信息。
  2. 客户端插件记录 ip:port作为服务端的destinationId
  3. 客户端插件将destinationId值作为Header.HTTP_HOST传递给server
  4. 服务端插件将Header.HTTP_HOST作为acceptorHost记录