这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战
本系列代码地址:github.com/JoJoTec/spr…
在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Spring Cloud Gateway 中可能发生链路信息丢失的问题。
主要冲突 - Project Reactor 与 Java Logger MDC 之间的设计冲突
Poject Reactor 是基于异步响应式设计的编程模式的实现,它的主要实现思路是先编写执行链路,最后 sub 执行整个链路。但是链路的每一部分,究竟是哪个线程执行的,是不确定的。
Java 的日志框架设计,其上下文 MDC(Mapped Diagnostic Context)信息,是基于线程设计的,其实可以简单理解为一个 ThreadLocal 的 Map。日志的链路信息,是保存在这个 MDC 中的。
这样其实可以看出 Project Reactor 与日志框架的 MDC 默认是不兼容的,只要发生异步线程切换,这个 MDC 就变了。Spring Cloud Sleuth 为此加了很多粘合代码,但是智者千虑必有一失,Project Reactor 应用场景和库也在不断发展和壮大,Spring Cloud Sleuth 也可能会漏掉一些场景导致链路信息丢失。
一种 Spring Cloud Gateway 常见的链路信息丢失的场景
我们编写一个简单的测试项目(项目地址):
引入依赖:
1 | xml复制代码<parent> |
对所有路径开启 AdaptCachedBodyGlobalFilter
:
1 | typescript复制代码@Configuration(proxyBeanMethods = false) |
配置(我们只有一个路由,将请求转发到 httpbin.org 这个 http 请求测试网站):
1 | yaml复制代码server: |
添加两个全局 Filter,一个在 AdaptCachedBodyGlobalFilter
之前,一个在 AdaptCachedBodyGlobalFilter
之后。这两个 Filter 非常简单,只是打一行日志。
1 | java复制代码@Log4j2 |
最后指定 Log4j2 的输出格式中包含链路信息,就像系列文章开头中指定的那样。
启动这个应用,之后访问 http://127.0.0.1:8181/httpbin/anything
,查看日志,发现 PostLogFilter 中的日志,没有链路信息了:
1 | ini复制代码2021-09-08 06:32:35.457 INFO [service-apiGateway,51063d6f1fe264d0,51063d6f1fe264d0] [30600] [reactor-http-nio-2][?:]: before AdaptCachedBodyGlobalFilter |
Spring Cloud Sleuth 是如何增加链路信息
通过系列之前的源码分析,我们知道,在最开始的 TraceWebFilter,我们将 Mono 封装成了一个 MonoWebFilterTrace,它的核心源码是:
1 | java复制代码@Override |
WebFilterTraceSubscriber 干了些什么呢?出现异常,以及 http 请求结束的时候,我们可能想将响应信息,异常信息记录进入 Span 中,就是通过这个类封装实现的。
经过 MonoWebFilterTrace 的封装,由于 Spring-WebFlux 处理请求,其实就是封装成我们上面得出的 Mono 之后进行 subscribe 处理的请求,所以这样,整个内部 Mono 的 publish 链路以及 subscribe 链路,就被 WebFilterTraceSubscriber 中的 scope 包裹起来了。只要我们自己不在 GatewayFilter 中转换成某些强制异步的 Mono 或者 Flux 导致切换线程,链路信息是不会丢失的。
为何上面的测试项目中链路信息会丢失
我们来看经过 AdaptCachedBodyGlobalFilter 之后,我们前面拼的 Mono 链路会变成什么样:
1 | less复制代码return Mono.defer(() -> |
其中 DataBufferUtils.join(exchange.getRequest().getBody())
其实是一个 FluxReceive,这里我们可以理解为:提交一个尝试读取请求 Body 的任务,将之后的 GatewayFilter 的链路处理加到在读取完 Body 之后的回调当中,提交这个任务后,立刻返回。这么看可能比较复杂,我们用一个类似的例子类比下:
1 | scala复制代码//首先我们创建一个新的 Span |
Mono.delay
和 FluxReceive 表现类似,都是异步切换线程池执行。执行上面的代码,我们可以从日志上面就能看出来:
1 | ini复制代码2021-09-08 07:12:45.236 INFO [service-apiGateway,7b2f5c190e1406cb,7b2f5c190e1406cb] [31868] [reactor-http-nio-2][?:]: first |
在 Spring Cloud Gateway 中,Request Body 的 FluxReceive 使用的线程池和调用 GatewayFilter 的是同一个线程池,所以可能线程还是同一个,但是由于 Span 已经结束,从 ThreadLocal 的 Map 中已经移除了链路信息,所以日志中还是没有链路信息。
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:
本文转载自: 掘金