分布式追踪工具 Spring cloud sleuth

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值