SpringCloud升級之路2020.0.x版-44.避免鏈路資訊丟失做的設計(1)

乾貨滿滿張雜湊 發表於 2021-11-29
Spring

SpringCloud升級之路2020.0.x版-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

SpringCloud升級之路2020.0.x版-44.避免鏈路資訊丟失做的設計(1)