你可以编写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
表示一个方法调用。Span
和SpanEvent
有很多字段,但是大多数字段都是内部处理的通过Pinpoint Agent
及大部分插件开发者不需要关心它们。但是对于这些字段,开发者必须处理的信息如下:
2. Pinpoint 插件结构
Pinpoint Plugin
由TraceMetadataProvider
和ProfilerPlugin
组成实现。
-
TraceMetadataProvider
:实现提供ServiceType
和AnnotationKey
对Pinpoint Agent
,Web
,采集器(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
实现规定ServiceTypes
和AnnotationKeys
。
2.1.1 ServiceType
每一个Span
和SpanEvent
都包含ServiceType
。ServiceType
表示跟踪的方法属于哪个类库,以及如何处理Span
和SpanEvent
。
下面显示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应该收集这个Span 或SpanEvent 的执行时间统计信息 |
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
,那么当ServiceType
的Span
或SpanEvent
在事务调用树中显示时,匹配的Annotation
将显示为典型的Annotation
。
2.2 ProfilerPlugin
ProfilerPlugin
修改目标库类来完成采集追踪数据。
ProfilerPlugin
按照如下顺序进行:
- 在JVM启动时,Pinpoint Agent启动
- Pinpoint Agent将会加载
plugin
目录下的全部插件 - Pinpoint Agent将会在每个插件调用
ProfilerPlugin.setup(ProfilerPluginSetupContext)
。 - 在
setup()
方法中,插件定义了需要转换的类,并注册了一个TransformerCallback
。 - 目标应用启动。
- 每当装载一个类时,Pinpoint Agent就会查找为该类注册的
TransformerCallback
。 - 如果注册了
TransformerCallback
,代理将调用它的doInTransform()
方法。 -
TransformerCallback
修改目标类的字节代码。(例如,添加拦截器、添加字段等) - 修改后的字节代码返回给JVM,并用返回的字节代码加载类。
- 应用继续运行。
- 当调用修改的方法时,将调用注入的拦截器的
before
和after
方法。 - 拦截器记录追踪的数据。
最重要的要考虑的点可以归纳为:
- 确定哪些方法足够必须,需要去跟踪。
- 注入拦截器去追踪这些方法。
这些拦截器用来在发送到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
查找节点之间的调用者-被调用者关系。在大多数情况下,acceptorHost
与endPoint
是相同的。然而,客户机发送请求到的地址有时可能与服务器接收请求(代理)的地址不同。要处理这种情况,您必须记录实际地址的客户端发送请求作为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的工作示例:
- 将事务数据作为HTTP头传递。您可以在这里找到标题名称
- 客户端插件记录服务器的
IP:Port
为destinationId
。 - 客户端插件将
destinationId
值作为头传递给服务器作为Header.HTTP_HOST
头。 - 服务器插件
Header.HTTP_HOST
头值作为acceptorHost
。
必须记住的另一件事是,使用相同协议的所有客户机和服务器必须以相同的方式传递事务数据,以确保兼容性。因此,如果你正在编写其他HTTP客户端或服务器的插件,你的插件必须如上所述记录和传递交易数据。
网友评论