[原始碼分析] OpenTracing之跟蹤Redis

羅西的思考發表於2020-09-12

[原始碼分析] 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
  • 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

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

相關文章