我們在這一節首先分析下 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 中:
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: