skywalking源码--探针插件开发

本源码来自于skywalking-agent 8.9.0版本
本节主要讲解skywalking-agent的轻量级队列内核,该实现在datacarrier模块主要用于进行数据发送OAP服务端的实现,主要采用缓存批量异步发送的方式进行发送。
注:本篇文章主要是作为自己看书后的总结,内容有可能会存在一些个人理解上的偏差,如果有网友找出问题欢迎提出,感谢!!!如果我理解上的错误误导了您,在此表示抱歉!!!

本篇主要介绍一些skywalking-agent中的基本概念然后介绍插件开发的一些规则,主要包括 Span、Trace Segment、ContextCarrier、ContextSnapshot、ContextManager。其中Span、Trace Segment是trace数据存储的核心;ContextCarrier、ContextSnapshot是trace数据在进程间、线程间传递的核心;ContextManager提供了对 Trace Segment、ContextCarrier、ContextSnapshot的相关操作。掌握这些类的功能、作用有利于在工作中对skywalking进行二次开发、扩展。

Span

这是分布式追踪系统中一个非常重要的概念,是trace数据的最小粒度、基本单元,可以理解为一次方法的调用、一个程序块的调用、数据库访问。用于记录调用的发生时间、结束时间、调用深度(当深度为0时trace上下文可以回收)、调用日志(发生异常时可以记录日志)、操作名、span类型等。

在skywalking中对span粗略分为:LocalSpan和RemoteSpan。

LocalSpan是一次本地的调用或者记录跨线程后的执行情况,与跨进程无关。

RemoteSpan是一次远程调用,可细分为:EntrySpan、ExitSpan。

EntrySpan:代表一个应用服务的提供端或者服务端的入口端,如接口入口、MQ消费端入口。在涉及到跨进程、跨线程时会和 ContextCarrier、ContextSnapshot 配合使用。

ExitSpan:代表一个应用服务的客户端或者消息队列的生产者,如 接口请求响应、MQ生产端请求响应。在涉及到跨进程、跨线程时会和 ContextCarrier、ContextSnapshot 配合使用。

Trace Segment

这是skywalking中特有的概念。指的是 一个请求在一个进程中的所有span的集合,这些span具有相同的segmentId。为什么skywalking中会有这么一个特殊的概念?这是因为为了把span按照请求+进程进行分解这样有利于OAP进行数据的聚合、分析。
在Trace Segment中会记录一些相关信息,如下:
TraceSegmentId:当前Trace Segment的唯一ID(TraceSegmentId)。

Refs:指向上一个Trace Segment的TraceSegmentId引用,通常情况下Refs是一个值,但是如果当前Trace Segment是批量MQ消费,那么 Refs就是多个TraceSegmentId。

Spans:用于存储从属于这个Trace Segment的所有span集合。

relatedGlobalTraceId:指向上一个Trace Segment的traceId引用,通常情况下relatedGlobalTraceId是一个值,但是如果当前Trace Segment是批量MQ消费,那么 relatedGlobalTraceId就是多个TraceId。

ignore:当前Trace Segment是否可以忽略,为true则可以不上传到OAP

isSizeLimited:当前Trace Segment的span集合的数量是否已经达到限制。默认false,未达到限制。

createTime:当前Trace Segment的创建时间。

ContextCarrier

该类主要用于处理skywalking在跨进程间的数据传递。如:客户端A调用服务端B,那么会进行如下的步骤:
1、客户端A调用前先创建 ContextCarrier
2、通过 ContextManager#inject将当前需要传递的数据放入 新的 ContextCarrier 进行后续传递
3、使用 ContextCarrier#items将步骤1的ContextCarrier内部的数据放入 CarrierItemHead,用于后续在 HTTP HEAD、Dubbo attachments 、MQ的HEAD 中进行传递
4、服务端B接收到请求后提取信息存入自己创建的 ContextCarrier 中
5、通过 ContextManager#createEntrySpan将ContextCarrier中的信息进行使用并做关联

ContextSnapshot

该类主要用于处理skywalking在跨线程间的数据传递。如:主线程调用中执行了子线程逻辑,那么会发生如下的步骤:
1、调用子线程前会先执行 ContextManager#capture用来创建一个ContextSnapshot
2、子线程执行前会执行 ContextManager#continued 将父线程给的 ContextSnapshot 传入自己的 TraceSegmentRef 中。

ContextManager

ContextManager见名见意,该类用于管理上下文。下面直接以它内部的方法进行介绍。
getOrCreate:用于获取或者创建一个上下文,是个私有方法,不能被外部调用。在 createEntrySpan、createLocalSpan、createExitSpan中会使用来获取当前的上下文。
get:用于获取当前上下文,是个私有方法,不能被外部调用,在getOrCreate方法中会调用来获取当前上下文,如果获取不到那么就会创建新的上下文。
getGlobalTraceId:获取当前上下文的traceId,是个public修饰的方法,可以被外部调用来获取当前traceId。如果当前是IgnoredTracerContext上下文,那么没有真是的traceId,返回的是“N/A”。
createEntrySpan:创建一个进入的span。
createLocalSpan:创建一个local的span。
createExitSpan:创建一个退出的span。
inject:在跨进程前调用。
extract:在跨进程后调用。
capture:在跨线程前调用。
continued:在跨线程的子线程内调用。
awaitFinishAsync:等待子线程结束的回调。
activeSpan:获取当前存活的span。
stopSpan:关闭span。
isActive:判断当前上下文是否存在。因为span深度为0后上下文会被回收。
getRuntimeContext:获取正在运行的上下文
getCorrelationContext:获取跨进程传递的数据。

插件开发简述

追踪的基本方法是拦截 Java 方法, 使用字节码操作技术和 AOP 概念. SkyWalking 包装了字节码操作技术, 并追踪上下文的传播. 所以你只需要定义拦截点(换句话说就是 Spring 的切面).

拦截方式

SkyWalking 提供两类通用的定义去拦截构造方法, 实例方法和类方法.

ClassInstanceMethodsEnhancePluginDefine 定义了构造方法 Contructor 拦截点和 instance method 实例方法拦截点.
ClassStaticMethodsEnhancePluginDefine 定义了类方法 class method 拦截点.
当然, 您也可以继承 ClassEnhancePluginDefine 去设置所有的拦截点, 但这不常用.

实现插件

下文, 我将通过扩展 ClassInstanceMethodsEnhancePluginDefine 来演示如何实现一个插件

1、定义目标类的名称

protected abstract ClassMatch enhanceClass();

ClassMatch 表示如何去匹配目标类, 这里有四种方法:

byName, 通过类的全限定名( 即 包名 + . + 类名,如:redis.clients.jedis.JedisPool,代表的是拦截redis.clients.jedis包下的JedisPool类)。
byClassAnnotationMatch, 根据目标类是否存在某些注解(如:org.nutz.mvc.annotation.At,代表目标类上存在 @At注解)。
byMethodAnnotationMatch, 根据目标类的方法是否存在某些注解(如:org.apache.skywalking.apm.toolkit.kafka.KafkaPollAndInvoke代表目标方法上存在 @KafkaPollAndInvoke 注解)。
byHierarchyMatch, 根据目标类的父类或接口

注意事项:

禁止使用 .class.getName() 去获取类名, 建议你使用文本字符串, 这是为了避免 ClassLoader 的问题.
by
AnnotationMatch 不支持从父类继承来的注解.
除非确实必要, 否则不建议使用 byHierarchyMatch, 因为使用它可能会触发拦截许多预期之外的方法, 会导致性能问题和不稳定.

    public static final String JEDIS_POOL_CLASS_NAME = "redis.clients.jedis.JedisPool";

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

2、定义实例方法拦截点

    public static final String INVOKE_INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.jedis.pool.v1.JedisPoolInterceptor";


    @Override
    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[]{
                new InstanceMethodsInterceptPoint() {
                    @Override
                    public ElementMatcher<MethodDescription> getMethodsMatcher() {
                        return named("getResource");
                    }

                    @Override
                    public String getMethodsInterceptor() {
                        return INVOKE_INTERCEPT_CLASS;
                    }

                    @Override
                    public boolean isOverrideArgs() {
                        return false;
                    }
                }
        };
    }

也可以使用 Matcher 来设置目标方法. 如果要在拦截器中更改引用参数, 请在 isOverrideArgs 中返回 true.

以下部分将告诉您如何实现拦截器.

3、在文件 skywalking-plugin.def 中添加插件定义


jedis-pool-1.x=org.apache.skywalking.apm.plugin.jedis.pool.v1.define.JedisPoolInstrumentation

实现一个拦截器

作为一个实例方法的拦截器, 需要实现 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);
}

在方法调用前, 调用后以及异常处理阶段使用核心 API。


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.apache.skywalking.apm.plugin.jedis.pool.v1;

import com.meitu.commons.redis.CustomJedisPool;
import io.micrometer.core.instrument.ImmutableTag;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.skywalking.apm.agent.core.logging.api.ILog;
import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
import org.apache.skywalking.apm.agent.core.metric.MetricsConfig;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import redis.clients.jedis.JedisPool;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class JedisPoolInterceptor implements InstanceMethodsAroundInterceptor {

    private static final ILog LOGGER = LogManager.getLogger(JedisPoolInterceptor.class);
    private final static String INTERNAL_POOL = "internalPool";

    private static Map<String, JedisPool> POOL_MAP = new ConcurrentHashMap<>();

    @Override
    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable {

    }

    @Override
    public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {
        return ret;
    }

    @Override
    public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {

    }

    @Override
    public void onAfterMethod(Long startTime, EnhancedInstance objInst, Method method, Object[] allArguments, Object ret, Throwable t) {
        if (null == objInst) {
            return;
        }
        try {
            JedisPool jedisPool = (JedisPool) objInst;
            String key = jedisPool.hashCode() + "";
            if (null != POOL_MAP.get(key)) {
                return;
            }

            Class<?> superclass;
            if (objInst instanceof CustomJedisPool) {
                superclass = jedisPool.getClass().getSuperclass().getSuperclass();
            } else {
                superclass = jedisPool.getClass().getSuperclass();
            }
            Field internalPoolField = superclass.getDeclaredField(INTERNAL_POOL);
            internalPoolField.setAccessible(true);
            GenericObjectPool genericObjectPool = (GenericObjectPool) internalPoolField.get(jedisPool);

            Field factoryField = genericObjectPool.getClass().getDeclaredField("factory");
            factoryField.setAccessible(true);
            PooledObjectFactory pooledObjectFactory = (PooledObjectFactory) factoryField.get(genericObjectPool);

            Field hostAndPortField = pooledObjectFactory.getClass().getDeclaredField("hostAndPort");
            hostAndPortField.setAccessible(true);
            String hostAndPort = hostAndPortField.get(pooledObjectFactory).toString();
            String[] split = hostAndPort.split("\\:");
            String host = split[0];
            String port = split[1];

            List<Tag> tags = buildPoolTagList(host, port, "jedisPool");

            Metrics.gauge(MetricsConfig.SERVICE_REDIS_CLI_POOL_ACTIVE_CONN, tags, jedisPool, JedisPool::getNumActive);
            Metrics.gauge(MetricsConfig.SERVICE_REDIS_CLI_POOL_MAX_CONN, tags, genericObjectPool, GenericObjectPool::getMaxTotal);
            Metrics.gauge(MetricsConfig.SERVICE_REDIS_CLI_POOL_WAIT_CONN, tags, jedisPool, JedisPool::getNumWaiters);
            Metrics.gauge(MetricsConfig.SERVICE_REDIS_CLI_POOL_IDLE_CONN, tags, jedisPool, JedisPool::getNumIdle);
            POOL_MAP.put(key, jedisPool);
        } catch (Exception e) {
            LOGGER.error("JedisPoolInterceptor metrics error", e);
        }
    }

    private List<Tag> buildPoolTagList(String host, String port, String type) {
        List<Tag> tags = new ArrayList<>();
        Tag typeTag = new ImmutableTag("pool_type", type);
        Tag hostTag = new ImmutableTag("host", host);
        Tag portTag = new ImmutableTag("port", port);
        tags.add(typeTag);
        tags.add(hostTag);
        tags.add(portTag);
        return tags;
    }

    @Override
    public void onHandleMethodException(Long startTime, EnhancedInstance objInst, Method method, Object[] allArguments, Throwable t) {

    }
}


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值