在再探pinpoint Agent(一)中介绍了运行的基本流程以及关键代码,本节主要讲解Agent中Plugin的相关内容
1.官方给出的介绍
1.1Trace Data
在pinpoint中,一个transaction包括一组的Spans,每一个Span代表事务进过的单个逻辑节点的跟踪,为了有助于可视化,我们假设有如下系统。前端服务器接收来自用户的请求,然后将请求发送到后端服务器,后者查询数据库。在这些节点中,我们假设只有前端和后端服务器由pinpoint Agent进行概要分析。
当请求到达前端服务器时,pinpoint Agent将生成一个新的事务ID,并用它创建一个Span。为了处理请求,前端服务器随后调用后端服务器。此时,pinpoint Agent将事务ID(加上一些其他传播值)注入到调用消息中。当后端服务器收到此消息时,它从消息中提取事务ID(和其他值),并用它们创建一个新的Span。因此,单个事务中的Spans共享相同的事务ID。
span记录重要的方法调用及其相关数据(参数、返回值等),然后将它们封装为类似调用堆栈的表示形式中的spanEvents。span本身及其每个spanEvents表示一个方法调用.
span和spanEvent有许多字段,但大多数字段是由pinpoint Agent在内部处理的,大多数插件开发人员不必担心它们。但是插件开发人员必须处理的字段和数据将在本指南中列出。
1.2 Pinpoint Plugin Structure
1.2.1ServiceType
每个span和spanEvent都包含一个serviceType。ServiceType表示跟踪方法所属的库,以及应该如何处理SPAN和SPAN事件。
下表显示了ServiceType的属性。
property | description |
---|---|
name | name of the ServiceType. Must be unique |
code | short type code value of the ServiceType. Must be unique |
desc | description |
properties | properties |
ServiceType代码必须使用其相应类别中的值。下表显示了这些类别及其代码范围。
category | range |
---|---|
Internal Use | 0 ~ 999 |
Server | 1000 ~ 1999 |
DB Client | 2000 ~ 2999 |
Cache Client | 8000 ~ 8999 |
RPC Client | 9000 ~ 9999 |
Others | 5000 ~ 7999 |
ServiceType必须是独一无二的。因此,如果你写了一个将共享公告的插件,你必须与Pinpoint Dev联系。团队获得指定的服务代码。如果您的插件是私人使用,您可以自由地从下面的服务器类型代码提取一个值。
category | range |
---|---|
Server | 1900 ~ 1999 |
DB Client | 2900 ~ 2999 |
Cache Client | 8900 ~ 8999 |
RPC Client | 9900 ~ 9999 |
Others | 7500 ~ 7999 |
ServiceTypes可以具有以下属性
property | description |
---|---|
TERMINAL | This Span or SpanEvent invokes a remote node but the target node is not traceable with Pinpoint |
QUEUE | This Span or SpanEvent consumes/produces a message from/to a message queue. |
INCLUDE_DESTINATION_ID | This Span or SpanEvent records a destination id and remote server is not a traceable type. |
RECORD_STATISTICS | Pinpoint Collector should collect execution time statistics of this Span or SpanEvent |
1.2.2 AnnotationKey
您可以使用更多信息注解spans和span events。注解是键-值对,其中键是AnnotationKey类型,值是基元类型、字符串或字节[]。对于常用的注释类型,有预定义的annotationkey,但如果它们不够,则可以在type-provider.yml中定义自己的键。
property | description |
---|---|
name | Name of the AnnotationKey |
code | int type code value of the AnnotationKey. Must be unique. |
properties | properties |
如果您正在为公共用途编写插件,并且希望添加新的annotationkey,则必须联系pinpoint开发团队以获得分配的annotationkey代码。如果您的插件是专用的,您可以选择一个介于900到999之间的值作为注释键代码。下表显示AnnotationKey属性。
property | description |
---|---|
VIEW_IN_RECORD_SET | Show this annotation in transaction call tree. |
ERROR_API_METADATA | This property is not for plugins. |
1.3 ProfilerPlugin
profilerplugin修改目标库类以收集跟踪数据。
ProfilerPlugin按以下步骤工作:
1)pinpoint Agent在JVM启动时启动。
2)pinpoint Agent加载插件目录下的所有插件。
3)pinpoint Agent为每个加载的插件调用profilerPlugin.setup(profilerPluginsetupContext)。
4)在设置方法中,插件将TransformerCallback注册到所有要转换的类。
5)目标应用程序启动。
6)每次加载类时,pinpoint Agent都会查找注册到该类的TransformerCallback。
7)如果已注册TransformerCallback,代理将调用它的doInTransform方法。
8)TransformerCallback修改目标类的字节代码。(例如,添加拦截器、添加字段等)
9)修改后的字节代码将返回到JVM,类将与返回的字节代码一起加载。
10)应用程序继续运行。
11)当调用修改后的方法时,将调用注入的拦截器的before和after方法。
12)拦截器记录跟踪数据。
编写插件时要考虑的最重要的一点是:1)找出哪些方法足够有趣,可以进行跟踪;2)注入拦截器来实际跟踪这些方法。这些拦截器用于在将跟踪数据发送到收集器之前提取、存储和传递跟踪数据。拦截器甚至可以相互合作,共享上下文。插件还可以通过向目标类添加getter甚至自定义字段来帮助跟踪,以便拦截器可以在执行期间访问它们。pinpoint Agent示例向您展示TransformerCallback如何修改类,以及注入的拦截器如何跟踪方法。
现在我们将描述拦截器必须做什么来跟踪不同的方法。
1.3.1 Plain method
plain 方法指的是任何不是节点的顶级方法,或者与远程或异步调用无关的方法
1.3.2 Top level method of a node
节点的顶级方法是一种方法,其中拦截器在节点中开始新的跟踪。这些方法通常是RPC的接受者,跟踪记录为一个服务类型分类为服务器的SPAN。如何记录跨度取决于事务是否已经在以前的任何节点上开始。
1.3.3 New transaction
如果当前节点是第一个记录事务的节点,则必须发出新的事务ID并记录它。newTraceObject()将自动处理此任务,因此只需调用它。
1.3.4 Continue Transaction
如果请求来pinpoint Agent跟踪的另一个节点,那么该事务将已经发出了事务ID;并且您必须将下面的数据记录到该范围中。(这些数据大部分是从上一个节点发送的,通常打包在请求消息中)
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查找节点之间的调用方被调用方关系。在大多数情况下,AcceptorHost与Endpoint相同。但是,客户端发送请求的地址有时可能与服务器接收请求的地址(代理)不同。要处理这种情况,您必须记录客户机作为接受者主机向其发送请求的实际地址。通常,客户机插件会将这个地址连同事务数据一起添加到请求消息中。
此外,还必须使用上一个节点发出和发送的跨度ID。有时,前一个节点会将事务标记为不可跟踪。在这种情况下,您不能跟踪事务。如您所见,客户机插件必须能够将跟踪数据传递给服务器插件,并且如何这样做取决于协议。您可以在这里找到顶级方法服务器拦截器的示例。
1.3.5 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) |
下一个节点是否可以通过pinpoint进行跟踪,会影响拦截器的实现方式。这里的术语“可追踪”是关于可能性的。例如,HTTP客户机的下一个节点是HTTP服务器。精确定位不会跟踪所有的HTTP服务器,但是可以跟踪它们(并且已经有了HTTP服务器插件)。在这种情况下,HTTP客户机的下一个节点是可跟踪的。另一方面,mysql jdbc的下一个节点mysql数据库服务器是不可跟踪的。
1)If the next node is traceable
如果下一个节点是可跟踪的,拦截器必须将以下数据传播到下一个节点。如何通过它们取决于协议,在最坏的情况下可能根本不可能通过它们。
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 next node's span (If next node is traceable by Pinpoint) |
Pinpoint通过匹配客户端跟踪的DestinationID和服务器跟踪的AcceptorHost来查找调用方被调用方关系。因此客户端插件必须记录destinationid,服务器插件必须记录具有相同值的acceptorhost。如果服务器本身无法获取该值,则客户端插件必须将其传递给服务器。侦听器的录制服务类型必须来自RPC客户端类别。
2)If the next node is not traceable
如果下一个节点不可跟踪,则ServiceType必须具有Terminal属性。如果要记录destination id,它还必须具有include_destination_id属性。如果记录destinationid,则服务器映射将在每个destinationid中显示一个节点,即使它们具有相同的端点。
此外,servicetype必须是db client或cache client类别。请注意,您不需要关心术语“db”或“cache”,因为使用不可跟踪目标服务器跟踪客户机库的任何插件都可能使用它们。“db”和“cache”之间的唯一区别是响应时间柱状图的时间范围(“cache”的柱状图间隔较小)。
1.3.6 Asynchronous task
跟踪对象绑定到首先通过threadlocal创建它们的线程,并且每当执行跨越线程边界时,跟踪对象都会丢失到新线程。因此,为了跨线程边界跟踪任务,必须注意将当前跟踪上下文传递给新线程。这是通过将AsyncContext注入调用线程和执行线程共享的对象来实现的。
调用线程从当前跟踪创建AsyncContext,并将其注入将传递给执行线程的对象中。然后,执行线程从对象中检索AsyncContext,从中创建一个新的跟踪,并将其绑定到它自己的threadLocal。因此,您必须为两个方法创建拦截器:i)一个方法启动任务(调用线程),ii)另一个方法实际处理任务(执行线程)。
启动方法的拦截器必须发出AsyncContext并将其传递给处理方法。如何传递此值取决于目标库。在最坏的情况下,你可能根本无法通过。然后,处理方法的拦截器必须使用传播的AsyncContext继续跟踪,并将其绑定到自己的线程。但是,强烈建议您只扩展AsyncContextSpanEventSimplearoundInterceptor,这样您就不必手动处理这个问题。请记住,由于共享对象必须能够注入AsyncContext,因此在类转换期间必须使用AsyncContextAccessor添加字段。您可以在这里找到跟踪异步任务的示例。
1.3.7 Case Study: HTTP
HTTP客户机是调用远程节点(客户机)的方法的示例,HTTP服务器是节点(服务器)的顶级方法的示例。如前所述,客户端插件必须有一种将事务数据传递给服务器插件的方法才能继续跟踪。注意,实现依赖于协议,httpclient3插件的httpmethodbaseexecuteMethodinterceptor和tomcat插件的standardhostvalveinvokeInterceptor显示了http的一个工作示例:
1)将事务数据作为HTTP头传递。您可以在这里找到标题名称
2)客户端插件将服务器的IP:端口记录为destinationID。
3)客户端插件将destinationid值作为header.http_主机头传递给服务器。
4)服务器插件将header.http_主机头值记录为acceptorhost。
您还需要记住的一点是,所有使用相同协议的客户机和服务器必须以相同的方式传递事务数据,以确保兼容性。因此,如果您正在编写其他HTTP客户机或服务器的插件,那么您的插件必须按照上面的描述记录和传递事务数据。
2.查看源码
下面就以tomcat插件源码为例介绍整个过程。
首先是resoureces下的两个文件
com.navercorp.pinpoint.bootstrap.plugin.ProfilerPlugin //Plugin
com.navercorp.pinpoint.common.trace.TraceMetadataProvider //Provider
在Agent初始化时会调用TraceMetadataProvider的setup方法
@Override
public void setup(TraceMetadataSetupContext context) {
context.addServiceType(TomcatConstants.TOMCAT); //添加ServiceType
context.addServiceType(TomcatConstants.TOMCAT_METHOD);
}
在依赖注入时,会调用TraceMetadataProvider的setup方法
@Override
public void setup(ProfilerPluginSetupContext context) {
final TomcatConfig config = new TomcatConfig(context.getConfig());
if (logger.isInfoEnabled()) {
logger.info("TomcatPlugin config:{}", config);
}
if (!config.isTomcatEnable()) {
logger.info("TomcatPlugin disabled");
return;
}
TomcatDetector tomcatDetector = new TomcatDetector(config.getTomcatBootstrapMains());
context.addApplicationTypeDetector(tomcatDetector);
if (shouldAddTransformers(config)) {
logger.info("Adding Tomcat transformers");
addTransformers(config); //添加TransformerCallback
} else {
logger.info("Not adding Tomcat transfomers");
}
}
private void addTransformers(TomcatConfig config) {
if (config.isTomcatHidePinpointHeader()) {
addRequestFacadeEditor();
}
addRequestEditor();
addStandardHostValveEditor();
addStandardServiceEditor();
addTomcatConnectorEditor();
addWebappLoaderEditor();
addAsyncContextImpl();
}
以addStandardHostValveEditor为例介绍:
private void addStandardHostValveEditor() {
transformTemplate.transform("org.apache.catalina.core.StandardHostValve", new TransformCallback() {
@Override
public byte[] doInTransform(Instrumentor instrumentor, ClassLoader classLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws InstrumentException {
InstrumentClass target = instrumentor.getInstrumentClass(classLoader, className, classfileBuffer);
InstrumentMethod method = target.getDeclaredMethod("invoke", "org.apache.catalina.connector.Request", "org.apache.catalina.connector.Response");
if (method != null) {
method.addInterceptor("com.navercorp.pinpoint.plugin.tomcat.interceptor.StandardHostValveInvokeInterceptor"); //添加拦截器
}
return target.toBytecode();
}
});
}
在StandardHostValve加载的时候会执行transform方法,transform主要是为类添加拦截器,当调用Request方法时前后执行拦截器中的方法,StandardHostValveInvokeInterceptor类中的before和after方法定义如下:
@Override
public void before(Object target, Object[] args) {
if (isDebug) {
logger.beforeInterceptor(target, args);
}
try {
final Trace trace = createTrace(target, args);
if (trace == null) {
return;
}
// TODO STATDISABLE this logic was added to disable statistics tracing
if (!trace.canSampled()) {
return;
}
// ------------------------------------------------------
SpanEventRecorder recorder = trace.traceBlockBegin(); //开始跟踪
recorder.recordServiceType(TomcatConstants.TOMCAT_METHOD);//记录ServiceType
} catch (Throwable th) {
if (logger.isWarnEnabled()) {
logger.warn("BEFORE. Caused:{}", th.getMessage(), th);
}
}
}
可以看到在before中主要是执行开始跟踪,方法定义如下:
@Override
public SpanEventRecorder traceBlockBegin(final int stackId) {
if (closed) {
if (isWarn) {
stackDump("already closed trace");
}
final SpanEvent dummy = newSpanEvent(stackId);
return wrappedSpanEventRecorder(this.wrappedSpanEventRecorder, dummy);
}
// Set properties for the case when stackFrame is not used as part of Span.
final SpanEvent spanEvent = newSpanEvent(stackId);
this.callStack.push(spanEvent); //将spanEvent添加到调用栈中
return wrappedSpanEventRecorder(this.wrappedSpanEventRecorder, spanEvent);
}
@Override
public void after(Object target, Object[] args, Object result, Throwable throwable) {
if (isDebug) {
logger.afterInterceptor(target, args, result, throwable);
}
final Trace trace = traceContext.currentRawTraceObject();
if (trace == null) {
return;
}
// TODO STATDISABLE this logic was added to disable statistics tracing
if (!trace.canSampled()) {
traceContext.removeTraceObject();
return;
}
// ------------------------------------------------------
try {
SpanEventRecorder recorder = trace.currentSpanEventRecorder();
if (this.isTraceRequestParam) {
final HttpServletRequest request = (HttpServletRequest) args[0];
if (!excludeProfileMethodFilter.filter(request.getMethod())) {
final String parameters = getRequestParameter(request, 64, 512);
if (StringUtils.hasLength(parameters)) {
recorder.recordAttribute(AnnotationKey.HTTP_PARAM, parameters); //记录Attribute
}
}
}
recorder.recordApi(methodDescriptor);//记录API
recorder.recordException(throwable);//记录Exception
} catch (Throwable th) {
if (logger.isWarnEnabled()) {
logger.warn("AFTER. Caused:{}", th.getMessage(), th);
}
} finally {
traceContext.removeTraceObject(); //移除跟踪对象
deleteTrace(trace, target, args, result, throwable); //删除跟踪
}
}
下面跟踪一下 deleteTrace(trace, target, args, result, throwable);这句
deleteTrace(trace, target, args, result, throwable);
->trace.close();
->logSpan(span);
-> this.storage.store(span);
-> this.dataSender.send(span); //发送
可以看出,在跟踪结束后,会将span发送出去,当然,应该发送到了Collector中以便存储到Hbase中去。