1. 介绍
spring cloud sleuth是用来解决分布式中服务的跟踪。span 和trace的图解如下:
2. 实现:
- 为了实现请求跟踪,当请求发送到分布式系统的入口断点时,只需要服务跟踪框架为该请求创建一个唯一的跟踪标识,同时在分布式系统内部流转的时候,框架始终保持传递该唯一标识。直到返回给请求为止,这个ID就是TraceId。
- 为了统计各处理单元的时间延时,当请求到达各个服务组件时,或是处理逻辑到达某个状态时,也通过一个唯一标识来标记它的开始、具体过程以及结束,该标识就是Span ID。对于每个Span来说,通过记录开始Span和结束Span的时间戳,就能统计出该Span的时间延时,除了时间戳记录之外,它可以包含一些其他元数据,比如事件名称、请求信息等。
- Spring Cloud Sleuth 有一个 Sampler 策略,可以通过这个实现类来控制采样算法。采样器不会阻碍 span 相关 id 的产生,但是会对导出以及附加事件标签的相关操作造成影响。 Sleuth 默认采样算法的实现是 Reservoir sampling,具体的实现类是 PercentageBasedSampler,默认的采样比例为: 0.1(即 10%)。不过我们可以通过spring.sleuth.sampler.percentage来设置,所设置的值介于 0.0 到 1.0 之间,1.0 则表示全部采集。
2.1log实现
在日志中打印traceId
通过ThreadLocal 、MDC进行实现。
2.2 RestTemplate TraceId实现
通过实现RestTemplate拦截器接口:ClientHttpRequestInterceptor
private void setInterceptors(HttpTracing httpTracing) {
this.template.setInterceptors(Arrays.<ClientHttpRequestInterceptor>asList(
TracingClientHttpRequestInterceptor.create(httpTracing)));
}
2.3. Http请求参数传递
参数传递一般通过拦截器的形式进行注入:
- X-B3-TraceId:一条请求链路Trace的唯一标识
- X-B3-SpanId:一个工作单元(Span)的唯一标识
- X-B3-ParentSpandId:标识当前工作单元所属的上一个工作单元,Root Span(请求链路的第一个工作单元)的该值为空。
- X-Span-Name:工作单元的名称
2.4 在异步调用中Tracer传递实现
- 使用切面@Aspect进行拦截
- 封装Runable 和Callble。在封装类中引入Tracer类
举例 Scheduler:
@Aspect
public class TraceSchedulingAspect {
private static final Log log = LogFactory.getLog(TraceSchedulingAspect.class);
private final Tracer tracer;
private final Pattern skipPattern;
private final TraceKeys traceKeys;
public TraceSchedulingAspect(Tracer tracer, Pattern skipPattern,
TraceKeys traceKeys) {
this.tracer = tracer;
log.info("TraceSchedulingAspect traceId " + tracer.toString());
this.skipPattern = skipPattern;
this.traceKeys = traceKeys;
}
@Around("execution (@org.springframework.scheduling.annotation.Scheduled * *.*(..))")
public Object traceBackgroundThread(final ProceedingJoinPoint pjp) throws Throwable {
if (this.skipPattern.matcher(pjp.getTarget().getClass().getName()).matches()) {
return pjp.proceed();
}
String spanName = SpanNameUtil.toLowerHyphen(pjp.getSignature().getName());
Span span = startOrContinueRenamedSpan(spanName);
log.info("traceBackgroundThread spanId:" + span);
try(Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
span.tag(this.traceKeys.getAsync().getPrefix() +
this.traceKeys.getAsync().getClassNameKey(), pjp.getTarget().getClass().getSimpleName());
span.tag(this.traceKeys.getAsync().getPrefix() +
this.traceKeys.getAsync().getMethodNameKey(), pjp.getSignature().getName());
return pjp.proceed();
} finally {
span.finish();
}
}
private Span startOrContinueRenamedSpan(String spanName) {
Span currentSpan = this.tracer.currentSpan();
if (currentSpan != null) {
return currentSpan.name(spanName);
}
return this.tracer.nextSpan().name(spanName);
}
}
tracer.currentSpan()
/**
* Returns the current span in scope or null if there isn't one.
* 返回当前线程作用域的span,如果不存在返回null
* <p>When entering user code, prefer {@link #currentSpanCustomizer()} as it is a stable type and
* will never return null.
toSpan(currentContext);使用上下文创建一个span
*/
@Nullable public Span currentSpan() {
TraceContext currentContext = currentTraceContext.get();
return currentContext != null ? toSpan(currentContext) : null;
}
nextSpan()
/** Returns a new child span if there's a {@link #currentSpan()} or a new trace if there isn't. */
public Span nextSpan() {
TraceContext parent = currentTraceContext.get();
return parent == null ? newTrace() : newChild(parent);
}
tracer.withSpanInScope()
设置span的作用域到线程上下文中:默认实现:
/**
* Makes the given span the "current span" and returns an object that exits that scope on close.
* 返回一个特定范围的对象。在对象关闭之前,一直保存这。
* Calls to {@link #currentSpan()} and {@link #currentSpanCustomizer()} will affect this span
* until the return value is closed.
*
* <p>The most convenient way to use this method is via the try-with-resources idiom.
*
* Ex.
* <pre>{@code
* // Assume a framework interceptor uses this method to set the inbound span as current
* try (SpanInScope ws = tracer.withSpanInScope(span)) {
* return inboundRequest.invoke();
* } finally {
* span.finish();
* }
*
* // An unrelated framework interceptor can now lookup the correct parent for outbound requests
* Span parent = tracer.currentSpan()
* Span span = tracer.nextSpan().name("outbound").start(); // parent is implicitly looked up
* try (SpanInScope ws = tracer.withSpanInScope(span)) {
* return outboundRequest.invoke();
* } finally {
* span.finish();
* }
* }</pre>
*
* <p>Note: While downstream code might affect the span, calling this method, and calling close on
* the result have no effect on the input. For example, calling close on the result does not
* finish the span. Not only is it safe to call close, you must call close to end the scope, or
* risk leaking resources associated with the scope.
*
* @param span span to place into scope or null to clear the scope
*/
public SpanInScope withSpanInScope(@Nullable Span span) {
return new SpanInScope(currentTraceContext.newScope(span != null ? span.context() : null));
}
@Override public Scope newScope(@Nullable TraceContext currentSpan) {
final TraceContext previous = local.get();
local.set(currentSpan);
class DefaultCurrentTraceContextScope implements Scope {
@Override public void close() {
local.set(previous);
}
}
return new DefaultCurrentTraceContextScope();
}
}
4.多线程执行上下文传递
Runnables、@Async Support、线程池的支持:
重点TraceRunnable 类:
**
* Runnable that passes Span between threads. The Span name is
* taken either from the passed value or from the {@link SpanNamer}
* interface.
*
* @author Spencer Gibb
* @author Marcin Grzejszczak
* @since 1.0.0
*/
public class TraceRunnable implements Runnable {
/**
* Since we don't know the exact operation name we provide a default
* name for the Span
*/
private static final String DEFAULT_SPAN_NAME = "async";
private final Tracer tracer;
private final Runnable delegate;
private final Span span;
private final ErrorParser errorParser;
public TraceRunnable(Tracer tracer, SpanNamer spanNamer, ErrorParser errorParser, Runnable delegate) {
this(tracer, spanNamer, errorParser, delegate, null);
}
public TraceRunnable(Tracer tracer, SpanNamer spanNamer, ErrorParser errorParser, Runnable delegate, String name) {
this.tracer = tracer;
this.delegate = delegate;
String spanName = name != null ? name : spanNamer.name(delegate, DEFAULT_SPAN_NAME);
//在主线程中执行
this.span = this.tracer.nextSpan().name(spanName);
this.errorParser = errorParser;
}
@Override
public void run() {
//在子线程中执行,进行传递
Throwable error = null;
try (SpanInScope ws = this.tracer.withSpanInScope(this.span.start())) {
this.delegate.run();
} catch (RuntimeException | Error e) {
error = e;
throw e;
} finally {
this.errorParser.parseErrorTags(this.span.customizer(), error);
this.span.finish();
}
}
}
this.tracer.nextSpan()
/** Returns a new child span if there's a {@link #currentSpan()} or a new trace if there isn't. */
public Span nextSpan() {
TraceContext parent = currentTraceContext.get();
return parent == null ? newTrace() : newChild(parent);
}
传递Span Context
在上下文传递时,Trace和Span ID是必传项,Baggage是个可选性。Buggage是一个key,value的map结构。在HTTP的请求头中,如果请求头开始:baggage-,则Spring sleuth可以理解。
Span initialSpan = this.tracer.nextSpan().name("span").start();
ExtraFieldPropagation.set(initialSpan.context(), "foo", "bar");
ExtraFieldPropagation.set(initialSpan.context(), "UPPER_CASE", "someValue");
}
tags
Span的中的tag是对特定span的tag。tag不是共享的,即不去传递。
参考:
https://www.baeldung.com/spring-cloud-sleuth-single-application