Skywalking插件开发指南

醉酒当歌 提交于 2020-02-27 14:46:08

此文章翻译了skywalking官方的Java-Plugin-Development-Guide.md

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

概念

Span(跨度)

在分布式的链路追踪系统里面Span是一个重要而又普遍的概念。我们可以从Google Dapper Paper OpenTracing学习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");
      CarrierItem next = contextCarrier.items();
      while (next.hasNext()) {
          next = next.next();
          httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());
      }
  1. Tomcat 7 服务端插件

    ContextCarrier contextCarrier = new ContextCarrier();
    CarrierItem next = 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.让子线程通过方法参数或由现有参数携带的任何方式访问ContextSnapshot 3.在子线程中使用ContextManager#continued

 

 

Core APIs(核心API)

ContextManager

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

  1. 创建EntrySpan

public static AbstractSpan createEntrySpan(String endpointName, ContextCarrier carrier)

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

  1. 创建LocalSpan

public static AbstractSpan createLocalSpan(String endpointName)

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

  1. 创建ExitSpan

public static AbstractSpan createExitSpan(String endpointName, ContextCarrier carrier, String remotePeer)

通过操作名(比如服务名、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.
     */
    AbstractSpan setComponent(Component component);

    /**
     * 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.
     */
    AbstractSpan setComponent(String componentName);

    AbstractSpan setLayer(SpanLayer layer);

    /**
     * Set a key:value tag on the Span.
     *
     * @return this Span instance, for chaining
     */
    AbstractSpan tag(String key, String value);

    /**
     * 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
     */
    AbstractSpan log(Throwable t);

    AbstractSpan errorOccurred();

    /**
     * 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
     */
    AbstractSpan log(long timestamp, Map<String, ?> event);

    /**
     * Sets the string name for the logical operation this span represents.
     *
     * @return this Span instance, for chaining
     */
    AbstractSpan setOperationName(String endpointName);

除了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
     */
    AbstractSpan prepareForAsync();

    /**
     * Notify the span, it could be finished.
     *
     * The execution times of {@link #prepareForAsync} and {@link #asyncFinish()} must match.
     *
     * @return the current span
     */
    AbstractSpan asyncFinish();
  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. 定义目标类名称

protected abstract ClassMatch enhanceClass();

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
protected ClassMatch enhanceClassName() {
    return byName("org.apache.catalina.core.StandardEngineValve");    
}         
  1. 定义一个实例方法的拦截点:

public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints();

public interface InstanceMethodsInterceptPoint {
    /**
     * class instance methods matcher.
     *
     * @return methods matcher
     */
    ElementMatcher<MethodDescription> getMethodsMatcher();

    /**
     * @return represents a class name, the class instance must instanceof InstanceMethodsAroundInterceptor.
     */
    String getMethodsInterceptor();

    boolean isOverrideArgs();
}

还可以使用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
 */
public interface InstanceMethodsAroundInterceptor {
    /**
     * called before target method invocation.
     *
     * @param result change this result, if you want to truncate the method.
     * @throws Throwable
     */
    void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        MethodInterceptResult result) throws Throwable;

    /**
     * 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
     */
    Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Object ret) throws Throwable;

    /**
     * called when occur exception.
     *
     * @param t the exception occur.
     */
    void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Throwable t);
}

beforeafterexception环境使用这些核心API

 

启动类增强机制

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

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

public class URLInstrumentation extends ClassEnhancePluginDefine {
    private static String CLASS_NAME = "java.net.URL";

    @Override protected ClassMatch enhanceClass() {
        return byName(CLASS_NAME);
    }

    @Override public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return new ConstructorInterceptPoint[] {
            new ConstructorInterceptPoint() {
                @Override public ElementMatcher<MethodDescription> getConstructorMatcher() {
                    return any();
                }

                @Override public String getConstructorInterceptor() {
                    return "org.apache.skywalking.apm.plugin.jre.httpurlconnection.Interceptor2";
                }
            }
        };
    }

    @Override public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[0];
    }

    @Override public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
        return new StaticMethodsInterceptPoint[0];
    }

    @Override public boolean isBootstrapInstrumentation() {
        return true;
    }
}

注意,仅在必要时进行引导检测,但大多数情况下会影响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接受插件。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!