前言
关于Spring Webflux链路跟踪的问题,在几年前我这篇博客讨论过,在那个时候并没有什么好的方式,所以里面提到的方式并不优雅。几年过去了,社区关于这个问题也做了很多努力让它更好用,并且提供了新的实现方式,这篇博客就介绍一下新的方式。建议看到最后,最优雅最简单的方式在最后描述。
传统Servlet中实现链路跟踪
实现单例中链路跟踪,在使用Spring Boot传统Servlet编程模式时,我们只需要在日志配置文件的日志输出格式中增加[%X{tid}]
,然后使用拦截器在请求到达时在MDC中设置tid
就可以轻松实现。在Servlet中能如此轻松实现的前提是它的单线程处理单个请求的模型。而MDC存储tid
这样的变量底层使用的是ThreadLocal。所以在Servlet中如果在一个请求中切面线程去处理逻辑也会出现tid
丢失的问题。不过就一个应用的整体而言,在一个请求中大部分情况都不会切换线程,所以这也就不是什么大问题。可以在响应式中,一个请求可能需要切换好几次线程,并且是每个请求会切线程,这就是大问题了。
以前Spring WebFlux中实现链路跟踪
为了避免在响应式编程线程切换导致tid
丢失的问题,所以借助了Reactor3中专门用来替换ThreadLocal的Context特性。在几年的博客中,我提到实现的方式就是基于Reactor3的Context来传递tid
,然后在打印日志时手动去Context中取出数据然后打印。显而易见并没有Servlet中那么优雅,甚至可以说很笨。
全新的Spring WebFlux中实现链路跟踪
为了更优雅的实现链路跟踪,在Reactor 3.5.3这个版本中引入了Hooks.enableAutomaticContextPropagation()
这个特性。从名称就能推测这个特性是用来自动传递Context的。下面是这个特性的全部说明注释。
Globally enables automatic context propagation to ThreadLocals.
It requires the context-propagation library to be on the classpath to have an effect. Using the implicit global ContextRegistry it reads entries present in the modified Context using Flux.contextWrite(ContextView) (or Mono.contextWrite(ContextView)) and Flux.contextWrite(Function) (or Mono.contextWrite(Function)) and restores all ThreadLocals associated via same keys for which ThreadLocalAccessors are registered.
The ThreadLocals are present in the upstream operators from the contextWrite(...) call and the unmodified (downstream) Context is used when signals are delivered downstream, making the contextWrite(...) a logical boundary for the context propagation mechanism.
This mechanism automatically performs Flux.contextCapture() and Mono.contextCapture() in Flux.blockFirst(), Flux.blockLast(), Flux.toIterable(), and Mono.block() (and their overloads).
大致意思就是会自动的将设置在Context的key/value设置到ThreadLocals中去,使用它的前提是需要引入[这里是代码009]。当然,除了会自动将Context中的key/value设置到ThreadLocals,也会在调用链结束也就是发生订阅关系时自动将当前线程中的ThreadLocals中的key/value设置到Context。
有了Hooks.enableAutomaticContextPropagation()
就可以和Servlet一样使用MDC了,因为Reactor会自动将Context信息同步到ThreadLocal,同样也会将ThreadLocal信息同步到Context。
用法(后面还有其他实现方式)
有了理论基础就可以动手写代码实际验证了。笔者的实现是在servlet上使用Reactor,所以部分代码可能在Spring WebFlux不一样,理论上直接在Spring WebFlux使用会更容易。
Jar包引入
首先引入Jar包,建议使用最新的版本的包,将Spring Webflux中的Reactor-core移除掉。虽然这个特性在3.5.3就引入,但是我实际使用过程中发现使用3.5.3并没有达到理想的效果,可能是有一些瑕疵,但是最新版本的没有问题,所以推荐使用最新版本的。
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.7.2</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>context-propagation</artifactId>
<version>1.1.2</version>
</dependency>
过滤器设置TRACE_ID
如果是Spring WebFlux过滤器的写法需要修改,这里是Servlet的写法。
@Bean
public Filter correlationFilter() {
return (request, response, chain) -> {
String traceId = IdUtil.fastSimpleUUID();
try {
MDC.put(TRACE_ID, traceId);
chain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
};
}
开启自动传递
在项目启动时执行Hooks.enableAutomaticContextPropagation();
建议就放在main方法的第一行。
过滤器设置TRACE_ID
如果是Spring WebFlux过滤器的写法需要修改,这里是Servlet的写法。
@Bean
public Filter correlationFilter() {
return (request, response, chain) -> {
String traceId = IdUtil.fastSimpleUUID();
try {
MDC.put(TRACE_ID, traceId);
chain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
};
}
注册ThreadLocalAccessor
下面是自定义的参数传达。
ContextRegistry.getInstance().registerThreadLocalAccessor(
TRACE_ID,
() -> MDC.get(TRACE_ID),
traceId -> MDC.put(TRACE_ID, traceId),
() -> MDC.remove(TRACE_ID));
当然如果想偷懒的话可以直接使用官方提供的Slf4jThreadLocalAccessor
,它可以将MDC中所有的数据都设置到ThreadLocal,当然也可以指定部分key。
//全部key
ContextRegistry.getInstance().registerThreadLocalAccessor(new Slf4jThreadLocalAccessor());
//指定key
ContextRegistry.getInstance().registerThreadLocalAccessor(new Slf4jThreadLocalAccessor("指定key"));
日志文件配置
在日志输出格式中增加上面配置的TRACE_ID。
pattern="${spring:spring.application.name}--[%X{TRACE_ID}]-[%d][%t][%level][%logger:%L] - %msg%n"
charset="UTF-8" />
至此就实现了Spring WebFlux的链路跟踪的功能,上面提到的方式是以最小包引入的实现的,所以我们自己做了一些比如像手动在过滤器设置TRACE_ID
以及注册ThreadLocalAccessor额外的工作。其实在社区中有一种更简便的方式。
第二种用法(偷懒方式)
如果不介意项目中引入更多的包,可以使用这种方式。
jar包引入
在这个包中其实包含了上面的context-propagation
,同样也注意要移除低版本的reactor-core。
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.7.2</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
开启自动传递
在项目启动时执行Hooks.enableAutomaticContextPropagation();
建议就放在main方法的第一行。
最后确定日志文件配置了链路id就ok了。
最后
对比几年前,现在实现Spring WebFlux的方式更加简便更加优雅,但是有一点需要注意。因为它多了很多从Context同步信息到ThreadLocal的操作,在引入这种方式时一定要做好压测,确保性能没有问题。