Sleuth详解

385 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

分布式跟踪解决方案。
Spring Cloud Sleuth能够跟踪您的请求和消息,以便您能够将通信与相应的日志条目关联起来。
您还可以将跟踪信息导出到外部系统,以可视化延迟。Spring Cloud Sleuth直接支持OpenZipkin兼容的系统。

术语

  • Span:基本工作单元,发送一个请求和发送一个响应,都是一个span,每个span还包括 descriptions, timestamped events, key-value annotations (tags)span id进程id(通常是IP地址)。一旦创建了一个span,就必须在将来的某个时刻停止它,形成一个闭环;
  • Trace:树形结构的一组span。
  • Annotation/Event:用于记录每个时间点的事件
    • cs:client sent。客户端发起一个请求,标记一个span的开始;
    • sr:server received。服务端接收到一个请求并开始处理这个请求,用sr的timestamp减去cs的timestamp,表示网络延迟的时间;
    • ss:server sent。服务端完成处理请求,把处理结果返回给客户端时,ss的timestamp减去sr的timestamp,表示服务端处理的时间;
    • cr:client received。客户端接收到服务端响应后记录的时间,标记span的结束。cr的timestamp减去cs的timestamp,表示整个请求话费的时间。

使用

Span 生命周期

最常用的接口:

  • org.springframework.cloud.sleuth.Tracer:使用 Tracer,您可以创建捕获请求关键路径的root span
  • org.springframework.cloud.sleuth.Span:span是需要启动和停止的单个工作单元。包含定时信息、事件和标记

Span生命周期的操作:

  • start:当您开始一个span时,将分配它的名称并记录开始时间戳
  • end:span结束
  • continue:span被继续,比如在另一个线程中继续
  • create with explicit parent:创建一个新的span,并设置它的父节点

创建和结束 spans

可以使用 Tracer 手动创建 spans:

// 创建一个新的span,如果这个线程中已经有一个span,它将成为新span的父线程
Span newSpan = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(newSpan.start())) {
    // ...
    // You can tag a span
    newSpan.tag("taxValue", taxValue);
    // ...
    // You can log an event on a span
    newSpan.event("taxCalculated");
}
finally {
    // Once done remember to end the span. This will allow collecting
    // the span to send it to a distributed tracing system e.g. Zipkin
    newSpan.end();
}

span的名字要明确,但不要超过50个字符,要简明最好

继续使用 spans

有时不用创建新的span,而是继续使用:

Span spanFromThreadX = this.tracer.nextSpan().name("calculateTax");
try (Tracer.SpanInScope ws = this.tracer.withSpan(spanFromThreadX.start())) {
    executorService.submit(() -> {
        // Pass the span from thread X
        Span continuedSpan = spanFromThreadX;
        // ...
        // You can tag a span
        continuedSpan.tag("taxValue", taxValue);
        // ...
        // You can log an event on a span
        continuedSpan.event("taxCalculated");
    }).get();
}
finally {
    spanFromThreadX.end();
}

使用明确的父span,创建一个span

当一个父span A运行在一个线程中,而你想在一个新的线程中创建另一个span B时,可以调用 Tracer.nextSpan(),使得A成为B的父节点。

// 在线程Y中接收到线程X中新建的"initialSpan”,“initialSpan”将成为线程Y中新建span的父span
Span newSpan = null;
try (Tracer.SpanInScope ws = this.tracer.withSpan(initialSpan)) {
    newSpan = this.tracer.nextSpan().name("calculateCommission");
    // ...
    // You can tag a span
    newSpan.tag("commissionValue", commissionValue);
    // ...
    // You can log an event on a span
    newSpan.event("commissionCalculated");
}
finally {
    // Once done remember to end the span. This will allow collecting
    // the span to send it to e.g. Zipkin. The tags and events set on the
    // newSpan will not be present on the parent
    if (newSpan != null) {
        newSpan.end();
    }
}

你也可以使用Tracer.nextSpan(Span parentSpan)版本显式提供父跨度

命名spans

span的默认命名规则:

  • controller-method-name:接收到来自controllerMethodName时使用的命名格式
  • asyc:当Callable和Runnable完成的操作使用,幸运的是,可以显示的命名
  • @Scheduled:方法被该注解注释时使用的名字

@SpanName注解

可以使用@SpanName注释显式地命名span:

@SpanName("calculateTax")
class TaxCountingRunnable implements Runnable {
    @Override
    public void run() {
        // perform logic
    }
}

以下处理时,span的名字是 calculateTax

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new TaxCountingRunnable());
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

toString() 方法

有时使用Runnable和Callable执行异步方法是,都是匿名的,没办法使用 @SpanName注解匿名方法,这时候可以使用 toString()方法:

Runnable runnable = new TraceRunnable(this.tracer, spanNamer, new Runnable() {
    @Override
    public void run() {
        // perform logic
    }

    @Override
    public String toString() {
        return "calculateTax";
    }
});
Future<?> future = executorService.submit(runnable);
// ... some additional logic ...
future.get();

使用注解管理spans

创建新的span

除了使用前文的方式创建新的span,还可以使用注解@NewSpan注解,同时,可以使用@SpanTag注解添加标记。

// 命名默认和方法名相同,即 testMethod
@NewSpan 
void testMethod();

// 也可以指定名字
@NewSpan("customNameOnTestMethod4")
void testMethod4();

可以组合指定 tag和span标记:

// 定义span,名字是 customNameOnTestMethod5
// 定义tag,key是 testTag
@NewSpan(name = "customNameOnTestMethod5")
void testMethod5(@SpanTag("testTag") String param);

// 执行方法时,参数test会成为tag的value
this.testBean.testMethod5("test");

可以在接口和实现类的方法上都使用span,如果同时存在,实现类上的生效。

@NewSpan(name = "customNameOnTestMethod3")
@Override
public void testMethod3() {
}

继续传递span

如果想在已经存在的span上添加tags或者注解,可以使用 @ContinueSpan,例如:

// 给span添加log日志testMethod11.before和testMethod11.after,如果返回异常,会创建日志 testMethod11.afterFailure
// 添加tag,key是testTag11
@ContinueSpan(log = "testMethod11")
void testMethod11(@SpanTag("testTag11") String param);

// tag的value成为test
this.testBean.testMethod11("test");
this.testBean.testMethod13();

高级span设置

通过@SpanTag提供的三种创建的标签方式,优先级:

  1. 尝试使用TagValueResolver类型的bean和提供的名称
  2. 如果未指定名称,则通过SPEL表达式分析
  3. 如果表达式不满足要求,则使用toString()返回的值