Spring Cloud Gateway 沒有鏈路資訊,我 TM 人傻了(上)

乾貨滿滿張雜湊 發表於 2021-09-24
Spring

本系列是 我TM人傻了 系列第五期[捂臉],往期精彩回顧:

image

本篇文章涉及底層設計以及原理,以及問題定位和可能的問題點,非常深入,篇幅較長,所以拆分成上中下三篇:

  • :問題簡單描述以及 Spring Cloud Gateway 基本結構和流程以及底層原理
  • :Spring Cloud Sleuth 如何在 Spring Cloud Gateway 加入的鏈路追蹤以及為何會出現這個問題
  • :現有 Spring Cloud Sleuth 的非侵入設計帶來的效能問題,其他可能的問題點,以及如何解決

我們的閘道器使用的是 Spring Cloud Gateway,並且加入了 spring-cloud-sleuth 的依賴,用於鏈路追蹤。並且通過 log4j2 的配置,將鏈路資訊輸出到日誌中,相關的佔位符是:

%X{traceId},%X{spanId}

但是最近發現,日誌中鏈路資訊出現丟失的情況,這是怎麼回事呢?

Spring Cloud Gateway 的基本流程與實現

首先簡單介紹一下 Spring Cloud Gateway 的基本結構,以及 Spring Cloud Sleuth 是如何在其中嵌入鏈路追蹤相關程式碼的。加入 Spring Cloud Sleuth 以及 Prometheus 相關依賴之後, Spring Cloud Gateway 的處理流程如下所示:

image

Spring Cloud Gateway 是基於 Spring WebFlux 開發的非同步響應式閘道器,非同步響應式程式碼比較難以理解和閱讀,我這裡給大家分享一種方法去理解,通過這個流程來理解 Spring Cloud Gateway 的工作流程以及底層原理。其實可以理解為,上圖這個流程,就是拼出來一個完整的 Mono(或者 Flux)流,最後 subscribe 執行。

當收到一個請求的時候,會經過 org.springframework.web.server.handler.DefaultWebFilterChain,這是 WebFilter 的呼叫鏈,這個鏈路包括三個 WebFilter:

  • org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter:新增 Prometheus 相關依賴之後,會有這個 MetricsWebFilter,用於記錄請求處理耗時,採集相關指標。
  • org.springframework.cloud.sleuth.instrument.web.TraceWebFilter:新增 Spring Cloud Sleuth 相關依賴之後,會有這個 TraceWebFilter。
  • org.springframework.cloud.gateway.handler.predicate.WeightCalculatorWebFilter:Spring Cloud Gateway 路由權重相關配置功能相關實現類,這個我們這裡不關心。

在這個 DefaultWebFilterChain 會形成這樣一個 Mono,我們依次將他們標記出來,首先是入口程式碼 org.springframework.web.server.handler.DefaultWebFilterChain#filter

public Mono<Void> filter(ServerWebExchange exchange) {
	return Mono.defer(() ->
	        // this.currentFilter != null 代表 WebFilter 鏈還沒有結束
	        // this.chain != null 代表 WebFilter 鏈不為空
			this.currentFilter != null && this.chain != null ?
					//在 WebFilter 鏈沒有結束的情況下,呼叫 WebFilter
					invokeFilter(this.currentFilter, this.chain, exchange) :
					//在 WebFilter 結束的情況下,呼叫 handler 
					this.handler.handle(exchange));
}

對於我們這裡的 WebFilter 鏈的第一個 MetricsWebFilter,假設啟用了對應的採集統計的話,這時候生成的 Mono 就是:

return Mono.defer(() ->
	chain.filter(exchange).transformDeferred((call) -> {
		long start = System.nanoTime();
		return call
				//成功時,記錄響應時間
				.doOnSuccess((done) -> MetricsWebFilter.this.onSuccess(exchange, start))
				//失敗時,記錄響應時間和異常
				.doOnError((cause) -> MetricsWebFilter.this.onError(exchange, start, cause));
	});
);

這裡為了方便,我們對程式碼做了簡化,由於我們要將整個鏈路的所有 Mono 和 Flux 拼接在一起行程完整鏈路,所以原本是 MetricsWebFilter中的 onSuccess(exchange, start)方法,被改成了 MetricsWebFilter.this.onSuccess(exchange, start) 這種虛擬碼。

接著,根據DefaultWebFilterChain 的原始碼分析,chain.filter(exchange) 會繼續 WebFilter 鏈路,到達下一個 WebFilter,即 TraceWebFilter。經過 TraceWebFilter,Mono 就會變成:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, chain.filter(exchange), TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

可以看出,在 TraceWebFilter 中,整個內部 Mono (chain.filter(exchange) 後續的結果)都被封裝成了一個 MonoWebFilterTrace,這也是保持鏈路追蹤資訊的關鍵實現。

繼續 WebFilter 鏈路,經過最後一個 WebFilter WeightCalculatorWebFilter; 這個 WebFilter 我們不關心,裡面對路由權重做了一些計算操作,我們這裡直接忽略即可。這樣我們就走完了所有 WebFilter 鏈路,來到了最後的呼叫 DefaultWebFilterChain.this.handler,這個 handler 就是 org.springframework.web.reactive.DispatcherHandler。在 DispatcherHandler 中,我們會計算出路由併傳送請求到符合條件的 GatewayFilter。經過 DispatcherHandler,Mono 會變成:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		Flux.fromIterable(DispatcherHandler.this.handlerMappings) //讀取所有的 handlerMappings
			.concatMap(mapping -> mapping.getHandler(exchange)) //按順序呼叫所有的 handlerMappings 的 getHandler 方法,如果有對應的 Handler 會返回,否則返回 Mono.empty();
			.next() //找到第一個返回不是 Mono.empty() 的 Handler
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //呼叫對應的 Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //處理結果
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

handlerMappings 包括:

  • org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndPointHandlerMapping:由於我們專案中新增了 Actuator 相關依賴,所以這裡有這個 HandlerMapping。Actuator 相關路徑對映,不是我們這裡關心的。但是可以看出,Actuator 相關路徑優先於 Spring Cloud Gateway 配置路由
  • org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping:由於我們專案中新增了 Actuator 相關依賴,所以這裡有這個 HandlerMapping。使用 @ControllerEndpoint 或者 @RestControllerEndpoint 註解標註的 Actuator 相關路徑對映,不是我們這裡關心的。
  • org.springframework.web.reactive.function.server.support.RouterFunctionMapping:在 Spring-WebFlux 中,你可以定義很多不同的 RouterFunction 來控制路徑路由,但這也不是我們這裡關心的。但是可以看出,自定義的 RouterFunction 會優先於 Spring Cloud Gateway 配置路由
  • org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping:針對 @RequestMapping 註解的路徑的 HandlerMapping,不是我們這裡關心的。但是可以看出,如果你在 Spring Cloud Gateway 中指定 RequestMapping 路徑,會優先於 Spring Cloud Gateway 配置路由
  • org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping:這個是 Spring Cloud Gateway 的 HandlerMapping,會讀取 Spring Cloud Gateway 配置並生成路由。這個是我們這裡要詳細分析的。

其實這些 handlerMappings,我們這裡肯定走的是 RoutePredicateHandlerMapping 的相關邏輯,所以我們的 Mono 又可以簡化成:

 return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.getHandler(exchange)
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //呼叫對應的 Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //處理結果
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

我們來看 RoutePredicateHandlerMapping,首先這些 handlerMapping 都是繼承了抽象類 org.springframework.web.reactive.handler.AbstractHandlerMapping, 前面我們拼接的 Mono 裡面的 getHandler 的實現其實就在這個抽象類中:

 public Mono<Object> getHandler(ServerWebExchange exchange) {
	//呼叫抽象方法 getHandlerInternal 獲取真正的 Handler
	return getHandlerInternal(exchange).map(handler -> {
		//這裡針對 handler 做一些日誌記錄
		if (logger.isDebugEnabled()) {
			logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
		}
		// 跨域處理
		ServerHttpRequest request = exchange.getRequest();
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
			CorsConfiguration config = (this.corsConfigurationSource != null ?
					this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
			config = (config != null ? config.combine(handlerConfig) : handlerConfig);
			if (config != null) {
				config.validateAllowCredentials();
			}
			if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
				return NO_OP_HANDLER;
			}
		}
		return handler;
	});
}

可以看出,其實核心就是每個實現類的 getHandlerInternal(exchange) 方法,所以在我們拼接的 Mono 中,我們會忽略抽象類中的針對 handler 之後的 map 處理。

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.getHandlerInternal(exchange)
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //呼叫對應的 Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //處理結果
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

接下來經過 RoutePredicateHandlerMappinggetHandlerInternal(exchange) 方法,我們的 Mono 變成了:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根據請求尋找路由
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //將路由放入 Attributes 中,後面我們還會用到
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
				}).switchIfEmpty( //如果為 Mono.empty(),也就是沒找到路由
					Mono.empty() //返回 Mono.empty()
					.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之後,記錄日誌
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.flatMap(handler -> DispatcherHandler.this.invokeHandler(exchange, handler)) //呼叫對應的 Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //處理結果
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

RoutePredicateHandlerMapping.this.lookupRoute(exchange) 根據請求尋找路由,這個我們就不詳細展開了,其實就是根據你的 Spring Cloud Gateway 配置,找到合適的路由。接下來我們來看呼叫對應的 Handler,即 FilteringWebHandler。DispatcherHandler.this.invokeHandler(exchange, handler) 我們這裡也不詳細展開,我們知道其實就是呼叫 Handler 的 handle 方法,即 FilteringWebHandler 的 handle 方法,所以 我們的 Mono 變成了:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根據請求尋找路由
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //將路由放入 Attributes 中,後面我們還會用到
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
				}).switchIfEmpty( //如果為 Mono.empty(),也就是沒找到路由
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之後,記錄日誌
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.then(FilteringWebHandler.this.handle(exchange).then(Mono.empty())) //呼叫對應的 Handler
			.flatMap(result -> DispatcherHandler.this.handleResult(exchange, result)), //處理結果
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

由於呼叫對應的 Handler,最後返回的是 Mono.empty(),所以後面的 flatMap 其實不會執行了。所以我們可以將最後的處理結果這一步去掉。所以我們的 Mono 就變成了:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根據請求尋找路由
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //將路由放入 Attributes 中,後面我們還會用到
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
				}).switchIfEmpty( //如果為 Mono.empty(),也就是沒找到路由
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之後,記錄日誌
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.then(FilteringWebHandler.this.handle(exchange).then(Mono.empty()))), //呼叫對應的 Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

FilteringWebHandler.this.handle(exchange) 其實就是從 Attributes 中取出路由,從路由中取出對應的 GatewayFilters,與全域性 GatewayFilters 放到同一個 List 中,並按照這些 GatewayFilter 的順序排序(可以通過實現 org.springframework.core.Ordered 介面來制定順序),然後生成 DefaultGatewayFilterChain 即 GatewayFilter 鏈路。對應的原始碼是:

public Mono<Void> handle(ServerWebExchange exchange) {
	//從 Attributes 中取出路由,從路由中取出對應的 GatewayFilters
	Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
	List<GatewayFilter> gatewayFilters = route.getFilters();
	//與全域性 GatewayFilters 放到同一個 List 中
	List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
	combined.addAll(gatewayFilters);
	//按照這些 GatewayFilter 的順序排序(可以通過實現 `org.springframework.core.Ordered` 介面來制定順序)
	AnnotationAwareOrderComparator.sort(combined);
	
	if (logger.isDebugEnabled()) {
		logger.debug("Sorted gatewayFilterFactories: " + combined);
	}
	//生成呼叫鏈
	return new DefaultGatewayFilterChain(combined).filter(exchange);
}

這個 GatewayFilter 呼叫鏈和 WebFilter 呼叫鏈類似,參考 DefaultGatewayFilterChain 的原始碼:

public Mono<Void> filter(ServerWebExchange exchange) {
	return Mono.defer(() -> {
		//如果鏈路沒有結束,則繼續鏈路
		if (this.index < filters.size()) {
			GatewayFilter filter = filters.get(this.index);
			//這裡將 index + 1,也就是呼叫鏈路中的下一個 GatewayFilter
			DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);
			//每個 filter 中如果想要繼續鏈路,則會呼叫 chain.filter(exchange),這也是我們開發 GatewayFilter 的時候的使用方式
			return filter.filter(exchange, chain);
		}
		else {
			//到達末尾,鏈路結束
			return Mono.empty(); // complete
		}
	});
}

所以,經過 DefaultGatewayFilterChain 後,我們的 Mono 就會變成:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根據請求尋找路由
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //將路由放入 Attributes 中,後面我們還會用到
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
				}).switchIfEmpty( //如果為 Mono.empty(),也就是沒找到路由
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之後,記錄日誌
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.then(new DefaultGatewayFilterChain(combined).filter(exchange).then(Mono.empty()))), //呼叫對應的 Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

再繼續展開 DefaultGatewayFilterChain 的鏈路呼叫,可以得到:

return Mono.defer(() ->
	new MonoWebFilterTrace(source, 
		RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根據請求尋找路由
				.flatMap((Function<Route, Mono<?>>) r -> {
					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //將路由放入 Attributes 中,後面我們還會用到
					return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
				}).switchIfEmpty( //如果為 Mono.empty(),也就是沒找到路由
					Mono.empty() 
					.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之後,記錄日誌
						if (logger.isTraceEnabled()) {
							logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
					}
				})))
			.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果沒有返回不為 Mono.empty() 的 handlerMapping,則直接返回 404
			.then(
				Mono.defer(() -> {
					//如果鏈路沒有結束,則繼續鏈路
					if (DefaultGatewayFilterChain.this.index < DefaultGatewayFilterChain.this.filters.size()) {
						GatewayFilter filter = DefaultGatewayFilterChain.this.filters.get(DefaultGatewayFilterChain.this.index);
						//這裡將 index + 1,也就是呼叫鏈路中的下一個 GatewayFilter
						DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(DefaultGatewayFilterChain.this, DefaultGatewayFilterChain.this.index + 1);
						//每個 filter 中如果想要繼續鏈路,則會呼叫 chain.filter(exchange),這也是我們開發 GatewayFilter 的時候的使用方式
						return filter.filter(exchange, chain);
					}
					else {
						return Mono.empty(); //鏈路完成
					}
				})
				.then(Mono.empty()))
			), //呼叫對應的 Handler
	TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
		//MetricsWebFilter 相關的處理,在前面的程式碼中給出了,這裡省略
	});
);

這樣,就形成了 Spring Cloud Gateway 針對路由請求的完整 Mono 呼叫鏈。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

Spring Cloud Gateway 沒有鏈路資訊,我 TM 人傻了(上)