19

[源码分析] OpenTracing之跟踪Redis

 3 years ago
source link: http://www.cnblogs.com/rossiXYZ/p/13656264.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[源码分析] OpenTracing之跟踪Redis

目录

  • [源码分析] OpenTracing之跟踪Redis

0x00 摘要

本文将通过研究OpenTracing的Redis埋点插件来进一步深入理解OpenTracing。

0x01 总体逻辑

1.1 相关概念

Tracer是用来管理Span的统筹者,负责创建span和传播span。它表示从头到尾的一个请求的调用链,是一次完整的跟踪,从请求到服务器开始,服务器返回response结束,跟踪每次rpc调用的耗时。它的标识符是“traceID”。

Span是一个更小的单位,表示一个RPC调用过程。一个“trace”包含有许多跨度(span),每个跨度捕获调用链内的一个工作单元(系统或服务节点),并由“spanId”标识。 每个跨度具有一个父跨度,并且一个“trace”的所有跨度形成有向无环图(DAG)。

1.2 埋点插件

对一个应用的跟踪要关注的无非就是 客户端——>web 层——>rpc 服务——>dao 后端存储、cache 缓存、消息队列 mq 等这些基础组件 。OpenTracing 插件的作用实际上也就是对不同组件进行埋点,以便基于这些组件采集应用的链路数据。

不同组件有不同的应用场景和扩展点,因此针对不同的框架,需要开发对应的OpenTracing API 插件用来实现自动埋点。

对于Redis来说各种插件更是层出不穷,所以OpenTracing 对与 Redis 各种插件也做了不同处理,比如 Jedis,Redisson,Spring Data Redis 2.x。本文主要是以 Redisson 为例说明,最后用spring-cloud-redis进行补充对照**。

1.3 总体逻辑

总体思路是使用代理模式 。因为 Redis 并没有提供像 Servlet 那样的过滤器或者拦截器,所以 Redis OpenTracing 插件没有进行常规埋点,而是通过组合的方式自定义若干代理类,比如 TracingRedissonClient 和 TracingRList .....。

  • TracingRedissonClient 代理了 Redis Client。
  • TracingRList 代理了Redis List 数据结构。
  • 还有其他类代理其他Redis数据结构,比如TracingRMap。

这些代理类将具体完成Tracing 功能。比如代理类 TracingRedissonClient 包含了两个成员变量:

  • private final RedissonClient redissonClient; 是真正的 Redis Client。
  • private final TracingRedissonHelper tracingRedissonHelper; 是具体针对 Redission 的Tracing 功能类,比如构建Span。

最后各种代理对 Redis 进行拦截:

  • 在执行具体的连接操作之前创建相关的 Span。
  • 在操作结束之后结束 Span,并进行上报。

具体可以见下图

+--------------------------+ +-------------------------+ +-------------------------+
|  TracingRedissonClient   | |       TracingRMap       | |      TracingRList       |
| +----------------------+ | | +---------------------+ | | +---------------------+ |
| |   RedissonClient     | | | |       RMap          | | | |      RList          | | ....
| |                      | | | |                     | | | |                     | |
| | TracingRedissonHelper| | | |TracingRedissonHelper| | | |TracingRedissonHelper| |
| +----------------------+ | | +---------------------+ | | +---------------------+ |
+--------------------------+ +-------------------------+ +-------------------------+
            |                             |                            |
            |                             |                            |
            |                             |                            |
            |                             |                            |
            |                             v                            |
            |             +---------------+-----------------+          |
            +-----------> |      TracingRedissonHelper      | <--------+
                          | +-----------------------------+ |
                          | |         Tracer              +-----+
                          | +-----------------------------+ |   |
                          +---------------------------------+   |
                                                                |
                          +---------------------------------+   |
                          |       TracingConfiguration      |   |
                          |  +----------------------------+ |   |
                          |  |        Tracer            <-------+
                          |  +----------------------------| |
                          +---------------------------------+

下图是为了手机观看。

yuER3mF.png!mobile

0x02 示例代码

我们使用代码自带的test来做说明。我们可以看到有两个代理类 TracingRedissonClientTracingRList

  • beforeClass 起到了系统启动的作用。
    TracingRedissonClient
    
  • 后续各种测试操作都是使用这个client在进行Redis操作。
    • 会通过代理类 TracingRedissonClient 得到一个 org.redisson.api.RList 以备后续操作。这个 RList 实际是OpenTracing 进行修改的另一个代理类 TracingRList
    • 会对这个 TracingRList 进行操作 : list.add("key");
    • 针对 Redisson 的异步操作,也进行了操作测试。

具体代码如下:

public class TracingRedissonTest {
  private static final MockTracer tracer = new MockTracer();
  private static RedisServer redisServer;
  private static RedissonClient client;
  
  @BeforeClass
  public static void beforeClass() {
    redisServer = RedisServer.builder().setting("bind 127.0.0.1").build();
    redisServer.start();

    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379");

    client = new TracingRedissonClient(Redisson.create(config),
        new TracingConfiguration.Builder(tracer).build());
  }
  
  @Test
  public void test_list() {
    RList<Object> list = client.getList("list");

    list.add("key");
    assertTrue(list.contains("key"));

    List<MockSpan> spans = tracer.finishedSpans();
    assertEquals(2, spans.size());
    checkSpans(spans);
    assertNull(tracer.activeSpan());
  }  
  
  @Test
  public void test_config_span_name() throws Exception {
    ......

    final MockSpan parent = tracer.buildSpan("test").start();
    try (Scope ignore = tracer.activateSpan(parent)) {
      RMap<String, String> map = customClient.getMap("map_config_span_name");
      map.getAsync("key").get(15, TimeUnit.SECONDS);
    }
    parent.finish();

    ......
  }  
}

0x03 Redis代理

前面我们提到了对于Redis是使用了代理来完成功能,下面我们具体来讲解。

3.1 Client 代理类

TracingRedissonClient 实现了 Redis Client 代理功能,其包含两个成员变量。

  • RedissonClient redissonClient; 是真正的Redis Client,代理类最终是通过此Client进行Redis操作。
  • TracingRedissonHelper tracingRedissonHelper; 完成了Tracing 功能。

具体在使用中,比如在测试代码会通过 Client 代理类得到一个 TracingRList 以备后续操作(这是另一个代理类)。

org.redisson.api.RList
RList<Object> list = client.getList("list");

具体代码如下:

public class TracingRedissonClient implements RedissonClient {
  private final RedissonClient redissonClient;
  private final TracingRedissonHelper tracingRedissonHelper;

  public TracingRedissonClient(RedissonClient redissonClient, TracingConfiguration configuration) {
    this.redissonClient = redissonClient;
    this.tracingRedissonHelper = new TracingRedissonHelper(configuration);
  }
  
  @Override
  public <V> RList<V> getList(String name) {
    // 通过代理生成
    return new TracingRList<>(redissonClient.getList(name), tracingRedissonHelper);
  }

  // 其他操作
  ......
}

3.2 List 代理类

TracingRList 是Redis List代理类(Redis插件还有其他代理类,代理其他Redis数据结构)。里面也是两个变量:

  • RList
  • TracingRedissonHelper 完成 Tracing 功能。

在具体 add 函数中:

tracingRedissonHelper.buildSpan
tracingRedissonHelper.decorate

具体代码如下:

public class TracingRList<V> extends TracingRExpirable implements RList<V> {
  private final RList<V> list;
  private final TracingRedissonHelper tracingRedissonHelper;
  
  @Override
  public boolean add(V element) {
    Span span = tracingRedissonHelper.buildSpan("add", list);
    span.setTag("element", nullable(element));
    return tracingRedissonHelper.decorate(span, () -> list.add(element));
  }

	// 其他操作
  .....
}

0x04 Tracing功能类

前面一直在提TracingRedissonHelper是Tracing功能类,下面我们就深入研究下 tracingRedissonHelper.decorate(span, () -> list.add(element)); 做了什么。

4.1 配置类

在初始化 Redis Client时候,生成了 TracingConfiguration。

client = new TracingRedissonClient(Redisson.create(config),
    new TracingConfiguration.Builder(tracer).build());

TracingConfiguration 之中就定义了 io.opentracing.Tracer 以及其他配置项。

具体类定义如下:

public class TracingConfiguration {
  static final int DEFAULT_KEYS_MAX_LENGTH = 100;
  private final Tracer tracer;
  private final boolean traceWithActiveSpanOnly;
  private final int keysMaxLength;
  private final Function<String, String> spanNameProvider;
  private final Map<String, String> extensionTags;
  
  // 其他操作
  ......
}

4.2 Tracing基础功能类

io.opentracing.contrib.redis.common.TracingHelperOpenTracing 通用的 Redis Tracing 功能类 ,我们看到里面有 Tracer 变量(就是TracingConfiguration之中的Tracer),也有 SpanBuilder 这样的helper 函数。

业务逻辑具体在 decorate 函数中有体现。参数 Supplier

return tracingRedissonHelper.decorate(span, () -> list.add(object));

Supplier 是 JAVA8 提供的接口,这个接口是一个提供者的意思,只有一个get的抽象类,没有默认的方法以及静态的方法。get方法返回一个泛型T,这就是一个创建对象的工厂。

所以decorate的作用在我们这里就是:

tracer.scopeManager().activate(span)
() -> list.add(element)
span.finish();

测试代码执行流程图如下:

TracingRList                  TracingHelper
      +                            +
  +---+--+                         |
  |  add | begin                   |
  +---+--+                         |
      |                            |
      |      invoke                |
      |                            v
      | ---------------->  +-------+------+
      |                    |   buildSpan  |
      | <---------------+  +-------+------+
      |       Return               |
      |                            |
  +---+-------+                    |
  |span.setTag|                    |
  +---+-------+                    |
      |                            |
      |                            |
      |                            |
      |     invoke   +-------------v-------------------------+
      | -----------> |decorate(span, () -> list.add(element))|
      |              +-------------+-------------------------+
      |                            |
      |                            |
      |                            |
      |                            v  begin tracing
      |              +-------------+----------------------+
      |              |tracer.scopeManager().activate(span)|
      |              +-------------+----------------------+
      |                            |
      |                            |
      |                            |
      |                            v  Real Redis action
+-----+------------+  <----+ +-----+--------+
| list.add(element)|         |supplier.get()|
+-----+------------+  +----> +-----+--------+
      |                            |
      |                            |
      |                            v  end tracing
      |  decorate Return     +-----+-------+
      |  <----------------+  |span.finish()|
      |                      +-------------+
   +--+---+
   | add  | end
   +--+---+
      |
      |
      |
      v

具体 TracingHelper 代码如下:

public class TracingHelper {

  public static final String COMPONENT_NAME = "java-redis";
  public static final String DB_TYPE = "redis";
  protected final Tracer tracer;
  private final boolean traceWithActiveSpanOnly;
  private final Function<String, String> spanNameProvider;
  private final int maxKeysLength;
  private final Map<String, String> extensionTags;
  
  public Span buildSpan(String operationName) {
    if (traceWithActiveSpanOnly && tracer.activeSpan() == null) {
      return NoopSpan.INSTANCE;
    } else {
      return builder(operationName).start();
    }
  }
  
  private SpanBuilder builder(String operationName) {
    SpanBuilder sb = tracer.buildSpan(spanNameProvider.apply(operationName))
        .withTag(Tags.COMPONENT.getKey(), COMPONENT_NAME)
        .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
        .withTag(Tags.DB_TYPE.getKey(), DB_TYPE);
    extensionTags.forEach(sb::withTag);
    return sb;
  }  
  
  public <T> T decorate(Span span, Supplier<T> supplier) {
    try (Scope ignore = tracer.scopeManager().activate(span)) { // 激活span
      return supplier.get();  // 执行Redis操作
    } catch (Exception e) {
      onError(e, span);
      throw e;
    } finally {
      span.finish();// 完成了结束操作,如果采样就会上报。
    }
  }
  
  // 其他操作
  .....
}

4.3 Redission专用Tracing功能类

TracingRedissonHelper 是具体实现了Redission 的 Tracing 功能,主要是针对异步操作。

4.3.1 测试代码

官方测试代码如下

final MockSpan parent = tracer.buildSpan("test").start();
try (Scope ignore = tracer.activateSpan(parent)) {
  RMap<String, String> map = customClient.getMap("map_config_span_name");
  map.getAsync("key").get(15, TimeUnit.SECONDS); // Redis异步操作
}
parent.finish();

能看到,测试的思路是:

  • 生成一个parent Span
  • 然后使用redis map进行 异步操作 ,getAsync这里会生成一个 client span。
  • parent span结束

具体下面我们会讲解。

4.3.2 TracingRedissonHelper

TracingRedissonHelper 需要针对 Redisson 来进行特殊设置,就是 因为Redisson同时还为分布式锁提供了异步执行的相关方法

所以需要对异步操作进行处理。其中:

  • RFuture 是 org.redisson.api 包下面的类,
  • CompletableRFuture 是 io.opentracing.contrib.redis.redisson 包下面的类,针对 RFuture 做了特殊处理。

prepareRFuture函数是执行Redis具体操作的函数,其作用如下:

  • 通过 futureSupplier.get(); 获取redisFuture( prepareRFuture的参数span是之前 getAsync 生成的 child span )。
  • 设置 redisFuture 的 whenComplete函数,在whenComplete函数中会对传入的Span进行 finish操作 ,这个span 其实是child span。 这样异步的Tracing通过Client Span完成
  • 继续操作, 恢复parent span ,在redisFuture基础上生成CompletableRFuture,然后继续设置redisFuture.whenComplete,如果redisFuture完成,则调用 customRedisFuture.complete。
  • 返回,外界测试函数会finish parent span

针对官方测试代码,执行流程图如下:

+------------+
| Parent Span|
+-----+------+
      |
      v
 TracingRMap                          TracingRedissonHelper
      +                                      +
      |                                      |
      |                                      v
 +----+-----+       Invoke              +----+------+
 | getAsync | +-----------------------> | buildSpan |create Child Span
 +----+-----+                           +----+------+
      |                                      |
      |                                      v
      |                                +-----+--------+
      |                                |prepareRFuture|
      |                                +-----+--------+
      |                                      |
      |                 Real redis action    v
+-----+----------------+  <--------+ +-------+-------------+
|() -> map.getAsync(key|             | futureSupplier.get()|
+-----+----------------+  +--------> +-------+-------------+
      |                     Future           |
      |                                      |
      |                              +-------v---------+
      |                              |setCompleteAction|
      |                              +-------+---------+
      |                                      |
      |                                      |
      |                               +------v-------+
      |                               | whenComplete |
      |                               +------+-------+
      |                                      |
      |                                      v
      |                               +------+-------+
      |                               | span.finish()|  Child Span
      |                               +------+-------+
      |                                      |
      |                                      v
      |                             +--------+----------+
      |                             | continueScopeSpan |
      |                             +--------+----------+
      |                                      |
      |                                      v
      |                            +---------+----------+
      |                            | tracer.activeSpan()|
      |                            +---------+----------+
      |                                      | Parent Span
      |                                      |
      |                                      v
      |                                +-----+--------+
      |                                |activate(span)|
      |                                +-----+--------+
      |                                      |
      |                                      |
      |                                      v
      |      return             +------------+-----------------+
      |  <--------------------  | customRedisFuture.complete(v)|
      |                         +------------------------------+
      |
 +----v----------+
 |parent.finish()|
 +---------------+

具体代码如下:

class TracingRedissonHelper extends TracingHelper {

  TracingRedissonHelper(TracingConfiguration tracingConfiguration) {
    super(tracingConfiguration);
  }

  Span buildSpan(String operationName, RObject rObject) {
    return buildSpan(operationName).setTag("name", rObject.getName());
  }

  private <T> RFuture<T> continueScopeSpan(RFuture<T> redisFuture) {
    Span span = tracer.activeSpan();
    CompletableRFuture<T> customRedisFuture = new CompletableRFuture<>(redisFuture);
    redisFuture.whenComplete((v, throwable) -> {
      try (Scope ignored = tracer.scopeManager().activate(span)) {
        if (throwable != null) {
          customRedisFuture.completeExceptionally(throwable);
        } else {
          customRedisFuture.complete(v);
        }
      }
    });
    return customRedisFuture;
  }

  private <V> RFuture<V> setCompleteAction(RFuture<V> future, Span span) {
    future.whenComplete((v, throwable) -> {
      if (throwable != null) {
        onError(throwable, span);
      }
      span.finish();
    });

    return future;
  }

  <V> RFuture<V> prepareRFuture(Span span, Supplier<RFuture<V>> futureSupplier) {
    RFuture<V> future;
    try {
      future = futureSupplier.get();
    } catch (Exception e) {
      onError(e, span);
      span.finish();
      throw e;
    }

    return continueScopeSpan(setCompleteAction(future, span));
  }
}

4.4 TracingRMap代理类的异步处理

TracingRMap 实现了 org.redisson.api.RMap 。这里就使用了上述的异步相关的功能,比如 getAsync。

所以调用了 prepareRFuture 的功能。

public class TracingRMap<K, V> extends TracingRExpirable implements RMap<K, V> {
  private final RMap<K, V> map;
  private final TracingRedissonHelper tracingRedissonHelper;  
  
  @Override
  public RFuture<V> getAsync(K key) {
    Span span = tracingRedissonHelper.buildSpan("getAsync", map);
    span.setTag("key", nullable(key));
    return tracingRedissonHelper.prepareRFuture(span, () -> map.getAsync(key));
  }  
  
  // 其他操作
  ......
}

0x05 spring-cloud-redis

opentracing-spring-cloud-redis-starter 实现了对 spring-cloud-redis 的Tracing功能。

Spring Cloud 埋点实现主要实现原理是利用Spring AOP切片技术抽象埋点行为 ,比如TraceAsyncAspect 切面类,使用@Around 声明拦截规则,后面的逻辑与手动埋点类似,创建一个span,将业务逻辑包围起来即可。

5.1 Bean

首先, 利用注解生成一些Bean ,比如。

@Configuration
@AutoConfigureAfter({TracerRegisterAutoConfiguration.class, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.class})
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnProperty(name = "opentracing.spring.cloud.redis.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(RedisTracingProperties.class)
public class RedisAutoConfiguration {
  @Bean
  public RedisAspect openTracingRedisAspect(Tracer tracer, RedisTracingProperties properties) {
    return new RedisAspect(tracer, properties);
  }
}

5.2 拦截规则

其次,使用 @Around 和 @Pointcut 声明拦截规则。具体是通过一层代理来是实现的拦截 。对所有的 Redis connection 都通过 TracingRedisConnection 进行了一层包装

@Aspect
public class RedisAspect {

  private final Tracer tracer;

  private final RedisTracingProperties properties;

  RedisAspect(Tracer tracer, RedisTracingProperties properties) {
    this.tracer = tracer;
    this.properties = properties;
  }

  @Pointcut("target(org.springframework.data.redis.connection.RedisConnectionFactory)")
  public void connectionFactory() {}

  @Pointcut("execution(org.springframework.data.redis.connection.RedisConnection *.getConnection(..))")
  public void getConnection() {}

  @Pointcut("execution(org.springframework.data.redis.connection.RedisClusterConnection *.getClusterConnection(..))")
  public void getClusterConnection() {}

  @Around("getConnection() && connectionFactory()")
  public Object aroundGetConnection(final ProceedingJoinPoint pjp) throws Throwable {
    final RedisConnection connection = (RedisConnection) pjp.proceed();

    final String prefixOperationName = this.properties.getPrefixOperationName();
    final TracingConfiguration tracingConfiguration = new TracingConfiguration.Builder(tracer)
        .withSpanNameProvider(RedisSpanNameProvider.PREFIX_OPERATION_NAME(prefixOperationName))
        .build();

    return new TracingRedisConnection(connection, tracingConfiguration);
  }

  @Around("getClusterConnection() && connectionFactory()")
  public Object aroundGetClusterConnection(final ProceedingJoinPoint pjp) throws Throwable {
    final RedisClusterConnection clusterConnection = (RedisClusterConnection) pjp.proceed();

    final String prefixOperationName = this.properties.getPrefixOperationName();
    final TracingConfiguration tracingConfiguration = new TracingConfiguration.Builder(tracer)
        .withSpanNameProvider(RedisSpanNameProvider.PREFIX_OPERATION_NAME(prefixOperationName))
        .build();

    return new TracingRedisClusterConnection(clusterConnection, tracingConfiguration);
  }	
}

5.2 埋点

在执行具体的命令前后通过自己提供的 API 进行埋点操作,基本上就是: redisTemplate的操作会在每个操作中调用connect做操作,比如 set操作中调用 connection.set(rawKey, rawValue) ,所以就通过 TracingRedisConnection来做一个封装,在做真正connection操作前后进行tracing

流程图如下:

redisTemplate                             TracingRedisConnection
               +                                            +
               |                                            |
               |                                            |
               v                                            |
+--------------+-----------------+                          |
|redisTemplate.opsForValue().set |                          |
+--------------+-----------------+                          |
               |                                            |
               |                                            |
               |                                            |
               |                                            |
               v                                            |
   +-----------+-----------+                                |
   | RedisTemplate.execute |                                |
   +-----------+-----------+                                |
               |                                            |
               |                                            |
               v                                            v
 +-------------+-------------+        invoke              +-+---+
 | DefaultValueOperations.set|  +---------------------->  | set |
 +-------------+-------------+                            +-+---+
               |                                            |
               |                                            |
               |                                            v  begin tracing
               |                                  +---------+--------------+
               |                                  | TracingHelper.doInScope|
               |                                  +---------+--------------+
               |                                            |
               |                                            v
               |                                        +---+-----+
               |                                        |buildSpan|
               |                                        +---+-----+
               |                                            |
               |                                            v
               |                                  +---------+-----------+
               |                                  |activateAndCloseSpan |
               |                                  +---------+-----------+
               |                                            |
               |                                            |
               v  Real redis action                         |
+--------------+-------------------+   <-----------------   |
| () -> connection.set(key, value) |                        |
+--------------+-------------------+   +----------------->  |
               |                                            |
               |                                            |  end tracing
               |         return                      +------v--------+
               | <--------------------------------+  |span.finish(); |
               |                                     +---------------+
               |
               |
               v

代码如下:

public class TracingRedisConnection implements RedisConnection {
  private final RedisConnection connection;
  private final TracingConfiguration tracingConfiguration;
  private final TracingHelper helper;

  public TracingRedisConnection(RedisConnection connection,
      TracingConfiguration tracingConfiguration) {
    this.connection = connection;
    this.tracingConfiguration = tracingConfiguration;
    this.helper = new TracingHelper(tracingConfiguration);
  }

// 在 span 的生命周期内执行具体命令
  @Override
  public Object execute(String command, byte[]... args) {
    // 执行命令
    return helper.doInScope(command, () -> connection.execute(command, args));
  }
  
  // 其他操作
  .....
}

具体Span是在TracingHelper中完成。

public class TracingHelper {
    public static final String COMPONENT_NAME = "java-redis";
    public static final String DB_TYPE = "redis";
    protected final Tracer tracer;
    private final boolean traceWithActiveSpanOnly;
    private final Function<String, String> spanNameProvider;
    private final int maxKeysLength;
    private final Map<String, String> extensionTags;

    public <T> T doInScope(String command, Supplier<T> supplier) {
        Span span = this.buildSpan(command);
        return this.activateAndCloseSpan(span, supplier);
    }
  
    // 其他操作
  	.....
}

0xFF 参考

分布式链路组件 SOFATracer 埋点机制解析

蚂蚁金服开源分布式链路跟踪组件 SOFATracer 埋点机制剖析

https://github.com/opentracing/opentracing-java

https://github.com/opentracing-contrib/java-redis-client

opentracing-spring-cloud-redis-starter


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK