[源码分析] 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            <-------+
                          |  +----------------------------| |
                          +---------------------------------+

下图是为了手机观看。

0x02 示例代码

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

  • beforeClass 起到了系统启动的作用。
    • 首先定义了一个tracer(这里是MockTracer,真正使用时候会用到其他Tracer)。
    • 然后使用这个Tracer来构建一个代理类 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 以备后续操作(这是另一个代理类)。

  • TracingRList 实现了 org.redisson.api.RList 接口。
  • 在构建 TracingRList 会把 TracingRedissonHelper 作为参数传递进去。
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 list; 数据类型是 org.redisson.api.RList,实现了真正的 Redis功能。
  • TracingRedissonHelper 完成 Tracing 功能。

在具体 add 函数中:

  • 在执行具体的命令前先通过 tracingRedissonHelper.buildSpan 构建 Span 进行埋点操作。
  • 然后添加 Tag。
  • 最后通过 tracingRedissonHelper.decorate 进行实际 Redis 操作。

具体代码如下:

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 supplier 是 java.util.function.Supplier 类型。

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

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

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

  • tracer.scopeManager().activate(span) 来激活当前span。
  • 返回对象,执行Redis操作,我们例子就是 () -> 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

posted @ 2020-09-12 14:32  罗西的思考  阅读(1206)  评论(0编辑  收藏  举报