SpringCloud升级之路2020.0.x版-44.避免链路信息丢失做的设计(1)

简介: SpringCloud升级之路2020.0.x版-44.避免链路信息丢失做的设计(1)

image.png


本系列代码地址: https://github.com/JoJoTec/spring-cloud-parent

我们在这一节首先分析下 Spring Cloud Gateway 一些其他可能丢失链路信息的点,之后来做一些可以避免链路信息丢失的设计,之后基于这个设计去实现我们需要的一些定制化的 GlobalFilter


Spring Cloud Gateway 其他的可能丢失链路信息的点


经过前面的分析,我们可以看出,不止这里,还有其他地方会导致 Spring Cloud Sleuth 的链路追踪信息消失,这里举几个大家常见的例子:

1.在 GatewayFilter 中指定了异步执行某些任务,由于线程切换了,并且这时候可能 Span 已经结束了,所以没有链路信息,例如

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  return chain.filter(exchange).publishOn(Schedulers.parallel()).doOnSuccess(o -> {
      //这里就没有链路信息了
            log.info("success");
  });
}

2.将 GatewayFilter 中继续链路的chain.filter(exchange)放到了异步任务中执行,上面的 AdaptCachedBodyGlobalFilter 就属于这种情况,这样会导致之后的 GatewayFilter 都没有链路信息,例如:

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  return Mono.delay(Duration.ofSeconds(1)).then(chain.filter(exchange));
}


Java 并发编程模型与 Project Reactor 编程模型的冲突思考


Java 中的很多框架,都用到了 ThreadLocal,或者通过 Thread 来标识唯一性。例如:

  • 日志框架中的 MDC,一般都是 ThreadLocal 实现。
  • 所有的锁、基于 AQS 的数据结构,都是通过 Thread 的属性来唯一标识谁获取到了锁的。
  • 分布式锁等数据结构,也是通过 Thread 的属性来唯一标识谁获取到了锁的,例如 Redisson 中分布式 Redis 锁的实现。

但是放到 Project Reactor 编程模型,这就显得格格不入了,因为 Project Reactor 异步响应式编程就是不固定线程,没法保证提交任务和回调能在同一个线程,所以 ThreadLocal 的语义在这里很难成立。Project Reactor 虽然提供了对标 ThreadLocal 的 Context,但是主流框架还没有兼容这个 Context,所以给 Spring Cloud Sleuth 粘合这些链路追踪带来了很大困难,因为 MDC 是一个 ThreadLocal 的 Map 实现,而不是基于 Context 的 Map。这就需要 Spring Cloud Sleuth 在订阅一开始,就需要将链路信息放入 MDC,同时还需要保证运行时不切换线程。

运行不切换线程,这样其实限制了 Project Reactor 的灵活调度,是有一些性能损失的。我们其实想尽量就算加入了链路追踪信息,也不用强制运行不切换线程。但是 Spring Cloud Sleuth 是非侵入式设计,很难实现这一点。但是对于我们自己业务的使用,我们可以定制一些编程规范,来保证大家写的代码不丢失链路信息


可以从哪里获取当前请求的 Span


Spring Cloud Sleuth 的链路信息核心即 Span,在之前的源码分析中,我们知道,在入口的 WebFilter 中,TraceWebFilter 生成 Span 并将其放入本次 HTTP 请求响应抽象的 ServerWebExchange 的 attributes 中:

TraceWebFilter.java

protected static final String TRACE_REQUEST_ATTR = Span.class.getName();
private Span findOrCreateSpan(Context c) {
  Span span;
  AssertingSpan assertingSpan = null;
  //如果当前 Reactor 的上下文中有 Span,就用这个 Span
  if (c.hasKey(Span.class)) {
    Span parent = c.get(Span.class);
    try (Tracer.SpanInScope spanInScope = this.tracer.withSpan(parent)) {
      span = this.tracer.nextSpan();
    }
    if (log.isDebugEnabled()) {
      log.debug("Found span in reactor context" + span);
    }
  }
  else {
      //如果当前请求中本身包含 span 信息,就用这个 span 启动一个新的子 span
    if (this.span != null) {
      try (Tracer.SpanInScope spanInScope = this.tracer.withSpan(this.span)) {
        span = this.tracer.nextSpan();
      }
      if (log.isDebugEnabled()) {
        log.debug("Found span in attribute " + span);
      }
    }
    //从当前所处的上下文中获取 span
    span = this.spanFromContextRetriever.findSpan(c);
    //没获取到就新生成一个
    if (this.span == null && span == null) {
      span = this.handler.handleReceive(new WrappedRequest(this.exchange.getRequest()));
      if (log.isDebugEnabled()) {
        log.debug("Handled receive of span " + span);
      }
    }
    else if (log.isDebugEnabled()) {
      log.debug("Found tracer specific span in reactor context [" + span + "]");
    }
    assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span);
    //将 span 放入 `ServerWebExchange` 的 attributes 中
    this.exchange.getAttributes().put(TRACE_REQUEST_ATTR, assertingSpan);
  }
  if (assertingSpan == null) {
    assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span);
  }
  return assertingSpan;
}

这样可以看出,我们在编写 GlobalFilter 的时候可以通过读取 ServerWebExchange 的 attributes 获取当前链路信息的 Span。但是 TRACE_REQUEST_ATTR 是 protected 的,我们可以下面这个工具类将其暴露出来。

package com.github.jojotech.spring.cloud.apigateway.common;
import org.springframework.cloud.sleuth.CurrentTraceContext;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.cloud.sleuth.http.HttpServerHandler;
import org.springframework.cloud.sleuth.instrument.web.TraceWebFilter;
public class TraceWebFilterUtil extends TraceWebFilter {
  public static final String TRACE_REQUEST_ATTR = TraceWebFilter.TRACE_REQUEST_ATTR;
  //仅仅为了暴露 TraceWebFilter 的 TRACE_REQUEST_ATTR 使用的工具类
  private TraceWebFilterUtil(Tracer tracer, HttpServerHandler handler, CurrentTraceContext currentTraceContext) {
    super(tracer, handler, currentTraceContext);
  }
}

下一节,我们将继续讲解避免链路信息丢失做的设计,主要针对获取到现有 Span 之后,如何保证每个 GlobalFilter 都能保持链路信息。

相关文章
|
30天前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
103 5
|
3月前
|
负载均衡 Java API
【Spring Cloud生态】Spring Cloud Gateway基本配置
【Spring Cloud生态】Spring Cloud Gateway基本配置
66 0
|
5月前
|
Java Spring
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
【Azure Spring Cloud】Spring Cloud Azure 4.0 调用Key Vault遇见认证错误 AADSTS90002: Tenant not found.
|
5月前
|
Java Spring 容器
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
【Azure Spring Cloud】在Azure Spring Apps上看见 App Memory Usage 和 jvm.menory.use 的指标的疑问及OOM
|
5月前
|
存储 Java Spring
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
【Azure Spring Cloud】Azure Spring Cloud服务,如何获取应用程序日志文件呢?
|
5月前
|
SQL Java 数据库连接
【Azure Spring Cloud】Azure Spring Cloud connect to SQL using MSI
【Azure Spring Cloud】Azure Spring Cloud connect to SQL using MSI
|
5月前
|
Java 开发工具 Spring
【Azure Spring Cloud】使用azure-spring-boot-starter-storage来上传文件报错: java.net.UnknownHostException: xxxxxxxx.blob.core.windows.net: Name or service not known
【Azure Spring Cloud】使用azure-spring-boot-starter-storage来上传文件报错: java.net.UnknownHostException: xxxxxxxx.blob.core.windows.net: Name or service not known
|
5月前
|
NoSQL Java Redis
【Azure Spring Cloud】Java Spring Cloud 应用部署到Azure上后,发现大量的 java.lang.NullPointerException: null at io.lettuce.core.protocol.CommandHandler.writeSingleCommand(CommandHandler.java:426) at ... 异常
【Azure Spring Cloud】Java Spring Cloud 应用部署到Azure上后,发现大量的 java.lang.NullPointerException: null at io.lettuce.core.protocol.CommandHandler.writeSingleCommand(CommandHandler.java:426) at ... 异常
|
5月前
|
Java Spring
【Azure 应用服务】记一次Azure Spring Cloud 的部署错误 (az spring-cloud app deploy -g dev -s testdemo -n demo -p ./hellospring-0.0.1-SNAPSHOT.jar --->>> Failed to wait for deployment instances to be ready)
【Azure 应用服务】记一次Azure Spring Cloud 的部署错误 (az spring-cloud app deploy -g dev -s testdemo -n demo -p ./hellospring-0.0.1-SNAPSHOT.jar --->>> Failed to wait for deployment instances to be ready)
|
5月前
|
Java Maven Python
【Azure Spring Cloud】部署Azure spring cloud 失败
【Azure Spring Cloud】部署Azure spring cloud 失败