0 x00 the

This article will further understand OpenTracing by examining the Redis plug-in for buried points.

0x01 General Logic

1.1 Related Concepts

Tracer is the co-ordinator used to manage spans, responsible for creating and propagating spans. It represents the invocation chain of a request from beginning to end. It is a complete trace, starting from the request to the server and ending with the server returning a response, tracking the time of each RPC call. Its identifier is “traceID”.

Span is a smaller unit that represents an RPC call procedure. A “trace” contains a number of spans, each of which captures a unit of work (system or service node) within the call chain and is identified by a “spanId.” Each span has a parent span, and all spans of a “trace” form a directed acyclic graph (DAG).

1.2 Buried point plug-in

Tracing an application is all about the client side — > Web layer — > RPC services — > DAO backend storage, cache caching, message queue MQ, and other basic components. The purpose of the OpenTracing plug-in is to bury different components so that application link data can be collected based on these components.

Different components have different application scenarios and extension points. Therefore, corresponding OpenTracing API plug-ins need to be developed for different frameworks to realize automatic burying points.

For Redis, a variety of plug-ins emerge endlessly, so OpenTracing also makes different processing for various plug-ins with Redis, such as Jedis, Redisson, and Spring Data Redis 2.x. This paper mainly uses Redisson as an example to illustrate, and finally uses Spring-cloud-Redis to supplement the comparison.

1.3 Overall Logic

The general idea is to use the proxy pattern. Because Redis does not provide filters or interceptors like servlets, the Redis OpenTracing plug-in does not do regular burial. Instead, it uses a combination of custom proxy classes. Examples include TracingRedissonClient and TracingRList….. .

  • TracingRedissonClient represents the Redis Client.
  • TracingRList proxies the Redis List data structure.
  • There are other classes that proxy other Redis data structures, such as TracingRMap.

These proxy classes will specifically perform the Tracing function. For example, the proxy class TracingRedissonClient contains two member variables:

  • private final RedissonClient redissonClient; Is the real Redis Client.
  • private final TracingRedissonHelper tracingRedissonHelper; Tracing is a Tracing feature class specific to Redission, such as building spans.

Finally, various agents intercept Redis:

  • Create relevant spans before performing specific connection operations.
  • End Span after the operation and report.

See the following figure for details

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

The image below is for mobile viewing.

0x02 Sample code

We use the test that comes with the code to illustrate this. We can see that there are two proxy classes TracingRedissonClient and TracingRList.

  • BeforeClass starts the system.
    • First, you define a tracer (in this case, MockTracer; you use other tracers when you actually use them).
    • Then use this Tracer to build a proxy classTracingRedissonClient.
  • All subsequent test operations use this client to perform Redis operations.
    • Will go through the proxy classTracingRedissonClientTo get aorg.redisson.api.RListFor subsequent operations. This RList is actually another proxy class modified by OpenTracingTracingRList.
    • On thisTracingRListTo operate:list.add("key");
    • Operational tests were also conducted for Redisson’s asynchronous operation.

The specific code is as follows:

public class TracingRedissonTest {
  private static final MockTracer tracer = new MockTracer();
  private static RedisServer redisServer;
  private static RedissonClient client;
  
  @BeforeClass
  public static void beforeClass(a) {
    redisServer = RedisServer.builder().setting("The 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(a) {
    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(a) 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(); . }}Copy the code

0 x03 Redis agent

We mentioned earlier that Redis uses proxies to complete the function, so let’s explain in detail.

3.1 Client Agent Class

TracingRedissonClient implements the Redis Client proxy and contains two member variables.

  • RedissonClient redissonClient; Is a real Redis Client through which the proxy class ultimately performs Redis operations.
  • TracingRedissonHelper tracingRedissonHelper; The Tracing function is completed.

In practice, for example, the test code gets a TracingRList from the Client proxy class for subsequent operations (which is another proxy class).

  • TracingRList realizedorg.redisson.api.RListInterface.
  • When you build a TracingRList, you pass TracingRedissonHelper as a parameter.
RList<Object> list = client.getList("list");
Copy the code

The specific code is as follows:

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) {
    // Generated by proxy
    return new TracingRList<>(redissonClient.getList(name), tracingRedissonHelper);
  }

  // Other operations. }Copy the code

3.2 List proxy class

TracingRList is the Redis List proxy class (Redis plug-ins and other proxy classes that proxy other Redis data structures). There are also two variables inside:

  • RList list; The data type is org.redisson.api.rlist, which implements true Redis functionality.
  • TracingRedissonHelper Displays the Tracing function.

In the specific add function:

  • Pass before executing a specific commandtracingRedissonHelper.buildSpanBuild a Span for burying operations.
  • Then add the Tag.
  • At last,tracingRedissonHelper.decoratePerform the actual Redis operation.

The specific code is as follows:

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));
  }

	// Other operations. }Copy the code

0x04 Tracing feature class

Front has been TracingRedissonHelper is Tracing function class, let’s study the TracingRedissonHelper. Decorate (span, () – > list. Add element ()); Did something.

4.1 configuration class

When initializing the Redis Client, the TracingConfiguration is generated.

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

TracingConfiguration defines IO. Opentracing.Tracer and other configuration items.

The concrete class is defined as follows:

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;
  
  // Other operations. }Copy the code

4.2 Tracing Basic feature classes

Io.opentracing.contrib.redis.com mon. TracingHelper is OpenTracing general Redis Tracing function class, We see Tracer variables (the Tracer in the TracingConfiguration) and helper functions like SpanBuilder.

The business logic is embodied in the Decorate function. Parameters: Supplier: Supplier is Java. The util. Function. : Supplier type.

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

Supplier is an interface provided by JAVA8. This interface is an abstract class of get with no default or static methods. The get method returns a generic T, which is a factory for creating objects.

So that’s what we do here:

  • withtracer.scopeManager().activate(span)To activate the current SPAN.
  • Return the object and perform the Redis operation, as in our example() -> list.add(element)
  • callspan.finish();The end operation is complete and will be reported if sampled.

The test code execution flow chart is as follows:

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 +--+---+ | | | vCopy the code

The TracingHelper code is as follows:

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 {
      returnbuilder(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)) { / / activate the span
      return supplier.get();  // Perform the Redis operation
    } catch (Exception e) {
      onError(e, span);
      throw e;
    } finally {
      span.finish();// Complete the end operation, if the sample is reported.}}// Other operations. }Copy the code

4.3 Redission Dedicated Tracing function classes

TracingRedissonHelper specifically implements the Tracing function of Redission, mainly for asynchronous operations.

4.3.1 Test code

The official test code is as follows

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 async operation
}
parent.finish();
Copy the code

As you can see, the idea is:

  • Generate a parent Span
  • The redis Map is then used for asynchronous operations, getAsync where a Client span is generated.
  • The parent span over

And we’ll talk about that.

4.3.2 TracingRedissonHelper

TracingRedissonHelper requires special Settings for Redisson because Redisson also provides methods for distributed locks to execute asynchronously.

So asynchronous operations need to be handled. Among them:

  • RFuture is a class under the org.redisson. API package,
  • CompletableRFuture is IO. Opentracing. Contrib. Redis. Redisson pack the following class, RFuture against doing a special treatment.

The prepareRFuture functions are functions that perform Redis specific operations and do the following:

  • Through futureSupplier. The get (); Get redisFuture (the prepareRFuture argument span is the Child Span generated by getAsync earlier).
  • Set up the whenComplete function of redisFuture, which finishes the passed Span, which is actually a Child Span. This asynchronous Tracing is done through the Client Span.
  • Go ahead, restore parent Span, generate CompletableRFuture based on redisFuture, and continue setting redisFuture. WhenComplete. If redisFuture is done, Call the customRedisFuture.com plete.
  • The external test function finishes Parent Span.

For the official test code, the execution flow chart is as follows:

+------------+ | 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()|
 +---------------+

Copy the code

The specific code is as follows:

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;
    }

    returncontinueScopeSpan(setCompleteAction(future, span)); }}Copy the code

4.4 Asynchronous Processing of the TracingRMap Agent Class

TracingRMap implements org.redisson.api.rmap. This is where the asynchron-related functions described above, such as getAsync, are used.

So the capabilities of prepareRFuture are called.

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));
  }  
  
  // Other operations. }Copy the code

0x05 spring-cloud-redis

The opentracing spring-cloud-redis starter implements the Tracing function of Spring-cloud-redis.

The main implementation principle of Spring Cloud burying point implementation is to use Spring AOP slicing technology to abstract burying point behavior, such as TraceAsyncAspect section class, using @around to declare interception rules, the following logic is similar to manual burying point, create a SPAN, Surround the business logic.

5.1 the Bean

First, generate some beans using annotations, such as.

@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 newRedisAspect(tracer, properties); }}Copy the code

5.2 Interception Rules

Second, declare interception rules using @around and @pointcut. Specifically, interception is implemented through a layer of proxies. All Redis connections are wrapped with a layer of 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(a) {}

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

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

  @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 newTracingRedisClusterConnection(clusterConnection, tracingConfiguration); }}Copy the code

5.2 buried point

Before and after executing a specific command, the API provided by the user is used for burying operations, basically: The redisTemplate operation calls connect in each operation, such as connection.set(rawKey, rawValue) in the set operation, so TracingRedisConnection is a wrapper. Tracing before and after the actual connection operation.

The flow chart is as follows:

        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
Copy the code

The code is as follows:

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);
  }

// Execute specific commands during the span lifecycle
  @Override
  public Object execute(String command, byte[]... args) {
    // Execute the command
    return helper.doInScope(command, () -> connection.execute(command, args));
  }
  
  // Other operations. }Copy the code

The Span is done in 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);
    }
  
    // Other operations. }Copy the code

0xEE Personal information

★★★★ Thoughts on life and technology ★★★★★

Wechat official account: Rosie’s Thoughts

If you want to get a timely news feed of personal articles, or want to see the technical information of personal recommendations, please pay attention.

0 XFF reference

Analysis of distributed link component SOFATracer buried point mechanism

Ant Financial open source distributed link tracking component SOFATracer buried point mechanism analysis

Github.com/opentracing…

Github.com/opentracing…

opentracing-spring-cloud-redis-starter