[源码分析] OpenTracing之跟踪Redis
Posted 罗西的思考
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[源码分析] OpenTracing之跟踪Redis相关的知识,希望对你有一定的参考价值。
[源码分析] OpenTracing之跟踪Redis
0x00 摘要0x01 总体逻辑1.1 相关概念1.2 埋点插件1.3 总体逻辑0x02 示例代码0x03 Redis代理3.1 Client 代理类3.2 List 代理类0x04 Tracing功能类4.1 配置类4.2 Tracing基础功能类4.3 Redission专用Tracing功能类4.3.1 测试代码4.3.2 TracingRedissonHelper4.4 TracingRMap代理类的异步处理0x05 spring-cloud-redis5.1 Bean5.2 拦截规则5.2 埋点0xFF 参考
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来做说明。我们可以看到有两个代理类 TracingRedissonClient
和 TracingRList
。
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<V> 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.TracingHelper
是OpenTracing 通用的 Redis Tracing 功能类,我们看到里面有 Tracer 变量(就是TracingConfiguration之中的Tracer),也有 SpanBuilder 这样的helper 函数。
业务逻辑具体在 decorate 函数中有体现。参数 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
以上是关于[源码分析] OpenTracing之跟踪Redis的主要内容,如果未能解决你的问题,请参考以下文章
将 JDBC 查询信息添加到 opentracing/Jaeger 跨度