[原始碼分析] 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來做說明。我們可以看到有兩個代理類 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
- 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
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 埋點機制剖析
https://github.com/opentracing/opentracing-java