[源码分析] OpenTracing之跟踪Redis

微信扫一扫,分享到朋友圈

[源码分析] OpenTracing之跟踪Redis

[源码分析] 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 起到了系统启动的作用。
    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

面试必问亿级流量优化策略之JVM调优,文档视频面试,还不收藏

上一篇

直播带货:折射了库存电商的另一张面孔

下一篇

你也可能喜欢

[源码分析] OpenTracing之跟踪Redis

长按储存图像,分享给朋友