本系列代码地址: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

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

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

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们在这一节我们将继续讲解避免链路信息丢失做的设计,主要针对获取到现有 Span 之后,如 ...

  2. SpringCloud升级之路2020.0.x版-43.为何 SpringCloudGateway 中会有链路信息丢失

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Sp ...

  3. SpringCloud升级之路2020.0.x版-45. 实现公共日志记录

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们这一节在前面实现的带有链路信息的 Publisher 的工厂的基础上,实现公共日志记录 ...

  4. SpringCloud升级之路2020.0.x版-1.背景

    本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...

  5. SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...

  6. SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...

  7. SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...

  8. SpringCloud升级之路2020.0.x版-42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 网关由于是所有外部用户请求的入口,记录这些请求中我们需要的元素,对于线上监控以及业务问题定 ...

  9. SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...

随机推荐

  1. 2020.10.23-vj个人赛补题

    B - B Polycarp loves lowercase letters and dislikes uppercase ones. Once he got a string s consistin ...

  2. MS office设置夜间模式

    点击文件 帐户 -> office主题

  3. Java(10)认识类和对象

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201574.html 博客主页:https://www.cnblogs.com/testero ...

  4. 初学python-day4 字典(已更新完)

  5. 初始HTML05

    HTML 表单控件属性 表单控件可设置以下标签属性 属性名 取值 type 设置控件类型 name 设置控件名称,最终与值一并发送给服务器 value 设置控件的值 placeholder 设置输入框 ...

  6. VS Code Remote SSH设置

    本文翻译自:5 Steps: Setup VS Code for Remote Development via SSH from Windows to Linux system 5个步骤:设置VS代码 ...

  7. BUAA_2020_软件工程_热身作业

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 热身作业要求 我在这个课程的目标 了解软件工程的技术,掌握工程化开发的能力 这个作业在哪个具体方面 ...

  8. Android DataBinding使用详解

    简介 DataBinding是一个自动绑定UI的框架. 使用DataBinding需要在app根目录的build.gradle文件中加入DataBinding配置: android { .... da ...

  9. Vue2高级原理

    <div id="app">     <input type="text" v-model="username"> ...

  10. spark搭建

    1.上传解压,配置环境变量 配置bin目录 2.修改配置文件 conf cp spark-env.sh.template spark-env.sh 增加配置 export SPARK_MASTER_I ...