Skywalking插件开发指南

编程

这篇文档主要介绍理解,开发和贡献插件

概念

Span(跨度)

在分布式的链路追踪系统里面Span是一个重要而又普遍的概念。我们可以从Google Dapper PaperOpenTracing学习span的相关知识

Skywalking从2017年就支持OpenTracing和OpenTracing-Java API。我们的Span概念和OpenTracing以及google的论文里面的概念非常相思。而且我们也扩展了Span。

这里有三中类型的Span

1.1 EntrySpan

EntrySpan代表了一个服务提供者,也就是服务端。作为一个APM系统,我们关注应用服务器。所以几乎所有的服务和MQ的消费端都是EntrySpan。

1.2 LocalSpan

LocalSpan可以理解为一个和远程服务无关的普通的Java方法,且这个Java方法即不是MQ的生产者,也不是消费者,更不是一个HTTP服务的生产者和消费者。

1.3 ExitSpan

ExitSpan代表服务的客户端或者MQ的生产者,在Skywalking的早期版本里面名字是LeafSpan。例如通过JDBC访问DB,从Redis或者Memcached读取数据都被归类为ExitSpan。

ContextCarrier(上下文载体)

为了实现分布式的链路追踪,跨进城的追踪需要被绑定,上下文环境需要跨进程传播,这就是ContextCarrier的职责。

一下步骤是在一个A->B的分布式调用中,如何去使用ContextCarrier

  1. 在客户端创建一个空的ContextCarrier

  2. 通过 ContextManager#createExitSpan 方法创建一个新的ExitSpan或者使用ContextManager#inject去初始化ContextCarrier

  3. 将所有的ContextCarrier的信息放入到head(Http head),attachments(Dobbo RPC框架)或者messages(Kafka)中。

  4. 通过服务调用,ContextCarrier传播到服务端。

  5. 在服务端可以通过heads/attachments/messages获取到ContextCarrier的所有信息。

  6. ContextManager#createEntrySpan方法会创建一个EntrySpan或者使用 ContextManager#extract方法来将客户端和服务器绑定到一起。

让我们用Apache HTTPComponent client端插件和Tomcat7 server插件来演示一下。

  1. Apache HTTPComponent客户端插件

      span=ContextManager.createExitSpan("/span/operation/name", contextCarrier, "ip:port");

     CarrierItemnext=contextCarrier.items();

     while (next.hasNext()) {

         next=next.next();

         httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());

    }

  1. Tomcat 7 服务端插件

    ContextCarriercontextCarrier=newContextCarrier();

    CarrierItemnext=contextCarrier.items();

    while (next.hasNext()) {

       next=next.next();

       next.setHeadValue(request.getHeader(next.getHeadKey()));

    }

    span=ContextManager.createEntrySpan(/span/operation/name”, contextCarrier);

     

ContextSnapshot(上下文快照)

除了跨进程,跨线程也需要得到支持,因为异步执行(内存中的MQ)和批处理在Java中很常见。 跨进程和跨线程是相似的,因为它们都是关于传播上下文。 唯一的区别是,跨线程不需要序列化。

这是跨线程传播的三个步骤:

1.使用ContextManager#capture获取ContextSnapshot对象。2.让子线程通过方法参数或由现有参数携带的任何方式访问ContextSnapshot3.在子线程中使用ContextManager#continued

 

 

Core APIs(核心API)

ContextManager

ContextManager提供了所有主要的和几本的API。

  1. 创建EntrySpan

publicstaticAbstractSpancreateEntrySpan(StringendpointName, ContextCarriercarrier)

创建EntrySpan通过操作名(比如服务名、URI等) 和 ContextCarrier

  1. 创建LocalSpan

publicstaticAbstractSpancreateLocalSpan(StringendpointName)

通过操作名称(比如方法的全限定名等)创建LoalSpan

  1. 创建ExitSpan

publicstaticAbstractSpancreateExitSpan(StringendpointName, ContextCarriercarrier, StringremotePeer)

通过操作名(比如服务名、URI等)、new一个ContextCarrier 地址信息(比如ip+port,或者hostname+port)和创建ExitSpan

AbstractSpan

    /**

    * Set the component id, which defines in {@link ComponentsDefine}

    *

    * @param component

    * @return the span for chaining.

    */

   AbstractSpansetComponent(Componentcomponent);

   /**

    * Only use this method in explicit instrumentation, like opentracing-skywalking-bridge.

    * It it higher recommend don"t use this for performance consideration.

    *

    * @param componentName

    * @return the span for chaining.

    */

   AbstractSpansetComponent(StringcomponentName);

   AbstractSpansetLayer(SpanLayerlayer);

   /**

    * Set a key:value tag on the Span.

    *

    * @return this Span instance, for chaining

    */

   AbstractSpantag(Stringkey, Stringvalue);

   /**

    * Record an exception event of the current walltime timestamp.

    *

    * @param t any subclass of {@link Throwable}, which occurs in this span.

    * @return the Span, for chaining

    */

   AbstractSpanlog(Throwablet);

   AbstractSpanerrorOccurred();

   /**

    * Record an event at a specific timestamp.

    *

    * @param timestamp The explicit timestamp for the log record.

    * @param event the events

    * @return the Span, for chaining

    */

   AbstractSpanlog(longtimestamp, Map<String, ?>event);

   /**

    * Sets the string name for the logical operation this span represents.

    *

    * @return this Span instance, for chaining

    */

   AbstractSpansetOperationName(StringendpointName);

除了operation nametagslogs还有两个属性需要设置,即componentlayer

SpanLayer是span的一种,有五种类型:

  1. UNKNOWN (default)

  2. DB

  3. RPC_FRAMEWORK(RPC框架,不是通常的HTTP)

  4. HTTP

  5. MQ

Component IDs被skywalking项目定义和保留。

对于component name/ID扩展,请遵循 Component library definition and extension 文档。

Advanced APIs

Async Span APIs(异步Span API)

Span中有一组高级API,这些API专用于异步方案。 当span的标签,日志,属性(包括结束时间)需要在另一个线程中设置,则应使用这些API。

    /**

    * The span finish at current tracing context, but the current span is still alive, until {@link #asyncFinish}

    * called.

    *

    * This method must be called<br/>

    * 1. In original thread(tracing context).

    * 2. Current span is active span.

    *

    * During alive, tags, logs and attributes of the span could be changed, in any thread.

    *

    * The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.

    *

    * @return the current span

    */

   AbstractSpanprepareForAsync();

   /**

    * Notify the span, it could be finished.

    *

    * The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.

    *

    * @return the current span

    */

   AbstractSpanasyncFinish();

  1. 在原始上下文中调用#prepareForAsync

  2. 完成当前线程中的工作后,在原始上下文中执行ContextManager#stopSpan

  3. 将跨度传播到任何其他线程。

  4. 完成所有设置后,在任何线程中调用#asyncFinish

  5. 跟踪上下文将完成,并在所有跨度的#prepareForAsynsc完成时向后端报告(由API执行次数判断)。

 

Develop a plugin(开发插件)

Abstract

追踪的几本方法是使用字节码技术或者AOP来拦截Java方法。

Skywalking封装了字节码操作并追踪上下午传播,所以你只需要定义拦截点(在Spring中也称为切入点)即可。

Intercept

Skywalking提供了两种通用的拦截构造函数的方法:实例方法和类方法。

  • 扩展ClassInstanceMethodsEnhancePluginDefine类,定义Constructor拦截点和instance method拦截点。

  • 扩展ClassStaticMethodsEnhancePluginDefine定义类方法拦截点。

当然,你也可以扩展ClassEnhancePluginDefine,来设置所有的拦截点,但是一般不这么用。

Implement plugin

下面演示如何通过扩展ClassInstanceMethodsEnhancePluginDefine实现一个插件。

  1. 定义目标类名称

protectedabstractClassMatchenhanceClass();

ClassMatch表示如何匹配目标类,有四种方式:

  • byName,通过类的全限定名称(包名+ . + 类名

  • byClassAnnotationMath,通过类存在的特定注解

  • byMethodAnnotationMatch,通过类方法存在的特定注解

  • byHierarchyMatch,通过类的父类或者接口

 

注意:

  • 在增强定义中,永远不要使用ThirdPartyClass.class,比如takesArguments(ThirdPartyClass.class),或者 byName(ThirdPartyClass.class.getName()),因为在目标应用中并不一定存在ThirdPartyClass,且这会破坏Agent。 我们在CI中有import检查来帮助校验,但是它并不涵盖此限制的所有情况,因此切勿尝试通过使用完全限定的类名(FQCN)之类的方法来解决此限制,例如:takesArguments(full.qualified.ThirdPartyClass.class)byName(full.qualified.ThirdPartyClass.class.getName())将通过CI检查,但是在agent的代码中仍然无效,请使类的全限定名

  • 即使你完全确定要拦截的类也存在于目标应用程序中(例如JDK的类),仍然不要使用*.class.getName()来获取类的String名称,建议使符串。 这是为了避免ClassLoader带来的问题。

  • by*AnnotationMatch 不支持继承的注解

  • 不推荐使用 byHierarchyMatch,除非必须要用的时候。因为可能会触发拦截许多不是自己想拦截的方法,这会导致性能问题。

例如:

@Override

protectedClassMatchenhanceClassName() {

   returnbyName("org.apache.catalina.core.StandardEngineValve");

}      

  1. 定义一个实例方法的拦截点:

publicInstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints();

publicinterfaceInstanceMethodsInterceptPoint {

   /**

    * class instance methods matcher.

    *

    * @return methods matcher

    */

   ElementMatcher<MethodDescription>getMethodsMatcher();

   /**

    * @return represents a class name, the class instance must instanceof InstanceMethodsAroundInterceptor.

    */

   StringgetMethodsInterceptor();

   booleanisOverrideArgs();

}

还可以使用Matcher设置目标方法。 如果想要在拦截器中修改参数引用,则需要在isOverrideArgs中返回true。

以下各章节将讲述如何实现拦截器。

3.将插件定义添加到skywalking-plugin.def文件中

tomcat-7.x/8.x=TomcatInstrumentation

Implement an interceptor

实现一个实例方法拦截器,需要实现接口org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor

/**

* A interceptor, which intercept method"s invocation. The target methods will be defined in {@link

* ClassEnhancePluginDefine}"s subclass, most likely in {@link ClassInstanceMethodsEnhancePluginDefine}

*

* @author wusheng

*/

publicinterfaceInstanceMethodsAroundInterceptor {

   /**

    * called before target method invocation.

    *

    * @param result change this result, if you want to truncate the method.

    * @throws Throwable

    */

   voidbeforeMethod(EnhancedInstanceobjInst, Methodmethod, Object[] allArguments, Class<?>[] argumentsTypes,

       MethodInterceptResultresult) throwsThrowable;

   /**

    * called after target method invocation. Even method"s invocation triggers an exception.

    *

    * @param ret the method"s original return value.

    * @return the method"s actual return value.

    * @throws Throwable

    */

   ObjectafterMethod(EnhancedInstanceobjInst, Methodmethod, Object[] allArguments, Class<?>[] argumentsTypes,

       Objectret) throwsThrowable;

   /**

    * called when occur exception.

    *

    * @param t the exception occur.

    */

   voidhandleMethodException(EnhancedInstanceobjInst, Methodmethod, Object[] allArguments, Class<?>[] argumentsTypes,

       Throwablet);

}

beforeafterexception环境使用这些核心API

 

启动类增强机制

SkyWalking已将引导程序方法打包在agent-core里面。 通过在Instrumentation定义,很容易启用。

重写方法 public boolean isBootstrapInstrumentation() 且返回true,如下所示:

publicclassURLInstrumentationextendsClassEnhancePluginDefine {

   privatestaticStringCLASS_NAME="java.net.URL";

   @OverrideprotectedClassMatchenhanceClass() {

       returnbyName(CLASS_NAME);

  }

   @OverridepublicConstructorInterceptPoint[] getConstructorsInterceptPoints() {

       returnnewConstructorInterceptPoint[] {

           newConstructorInterceptPoint() {

               @OverridepublicElementMatcher<MethodDescription>getConstructorMatcher() {

                   returnany();

              }

               @OverridepublicStringgetConstructorInterceptor() {

                   return"org.apache.skywalking.apm.plugin.jre.httpurlconnection.Interceptor2";

              }

          }

      };

  }

   @OverridepublicInstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {

       returnnewInstanceMethodsInterceptPoint[0];

  }

   @OverridepublicStaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {

       returnnewStaticMethodsInterceptPoint[0];

  }

   @OverridepublicbooleanisBootstrapInstrumentation() {

       returntrue;

  }

}

注意,仅在必要时进行引导检测,但大多数情况下会影响JRE core(rt.jar),并可能意料之外的结果和副作用。

贡献插件到Apache SkyWalking仓库

我们欢迎大家贡献插件。

请按照以下步骤操作:

  1. 提交一个有关您要贡献哪些插件的问题,包括支持的版本。

  2. apm-sniffer/apm-sdk-plugin 或者apm-sniffer/optional-plugins模块下创建自模块,插件项目的名称需要包含支持的库的名称和版本

  3. 按照本指南进行开发。 确保提供了注释和测试用例。

  4. 开发和测试。

  5. 提供自动测试用例。如何编写插件测试用例,可以参考此文档

  6. 发送pr并申请review

  7. 插件提交者审批通过提交的插件,插件CI-with-IT,e2e和插件测试通过。

  8. SkyWalking接受插件。

以上是 Skywalking插件开发指南 的全部内容, 来源链接: utcz.com/z/513414.html

回到顶部