专栏目录
31. FeignClient 实现断路器以及线程隔离限流的思路 15.UnderTow 订制 19.Eureka的服务端设计与配置 5.所有项目的parent与spring-framework-common说明 16.Eureka架构和核心概念 36. 验证断路器正确性 28.OpenFeign的生命周期-进行调用 4.maven依赖回顾以及项目框架结构 40. spock 单元测试封装的 WebClient(上) 38. 实现自定义 WebClient 的 NamedContextFactory 37. 实现异步的客户端封装配置管理的意义与设计 1. 背景 35. 验证线程隔离正确性 29.Spring Cloud OpenFeign 的解析(1) 41. SpringCloudGateway 基本流程讲解(2) 44.避免链路信息丢失做的设计(1) 14.UnderTow AccessLog 配置介绍 40. spock 单元测试封装的 WebClient(下) 17.Eureka的实例配置 7.从Bean到SpringCloud 6.微服务特性相关的依赖说明 12.UnderTow 简介与内部原理 11.Log4j2 监控相关 21.Spring Cloud LoadBalancer简介 20. 启动一个 Eureka Server 集群 23.订制Spring Cloud LoadBalancer 22.Spring Cloud LoadBalancer核心源码 30. FeignClient 实现重试 24.测试Spring Cloud LoadBalancer 8.理解 NamedContextFactory 43.为何 SpringCloudGateway 中会有链路信息丢失 41. SpringCloudGateway 基本流程讲解(1) 39. 改造 resilience4j 粘合 WebClient 44.避免链路信息丢失做的设计(2) 18.Eureka的客户端核心设计和配置 27.OpenFeign的生命周期-创建代理 42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷 25.OpenFeign简介与使用 10.使用Log4j2以及一些核心配置 26.OpenFeign的组件 33. 实现重试、断路器以及线程隔离源码 13.UnderTow 核心配置 32. 改进负载均衡算法 3.Eureka Server 与 API 网关要考虑的问题 45. 实现公共日志记录 2.微服务框架需要考虑的问题 9.如何理解并定制一个Spring Cloud组件 34.验证重试配置正确性

44.避免链路信息丢失做的设计(1)

干货满满张哈希
• 阅读 261

44.避免链路信息丢失做的设计(1)

本系列代码地址: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 都能保持链路信息。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

44.避免链路信息丢失做的设计(1)

点赞
收藏
评论区
推荐文章

暂无数据