概述
随着 Spring Cloud 微服务架构的流行,一次请求往往需要涉及到多个服务,因此服务性能监控和排查就变得更复杂。 通过 APM 帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。而 Spring Cloud 提供了 Spring Cloud Sleuth 可快速集成 Zipkin。
问题举例
打印 traceId 有何意义?
如何在日志中打印 Zipkin traceId?
如何在子线程或线程池中如何获取 Zipkin traceId 并打印?
问题解决
打印 traceId 意义
分布式环境下,微服务之间的调用错综复杂,如果突然爆出一个错误,虽然有日志记录,但到底是哪个服务出了问题呢?是前端传的参数错误,还是系统X或系统Y提供的接口导致?在这种情况下,错误排查起来就非常费劲。
为了追踪一个请求完整的流转过程,可以给每次请求分配一个唯一的 traceId,当请求调用其他服务时,通过传递这个 traceId。在输出日志时,将这个 traceId 打印到日志文件中,再使用日志分析工具(ELK)从日志文件中搜索,使用 traceId 就可以分析一个请求完整的调用过程,若更进一步,还可以做性能分析。
日志中打印 Zipkin traceId
使用 Spring Cloud 框架整合 Zipkin 特别方便,只需要在 maven pom 文件中配置 spring-cloud-sleuth-zipkin-stream
(还需依赖其他 pom,可自行百度),再到 logback-spring.xml
文件中配置日志格式模板,Zipkin 默认 traceId 名称为 X-B3-TraceId
。
<property name="log.console_log_pattern" value="[%date] %clr([%level]) [%thread] [traceId:%clr(%X{X-B3-TraceId}){blue}] %clr([%logger]:%L){cyan} >>> %msg %n"/>
子线程或线程池中获取 Zipkin traceId 并打印到日志中
经过阅读 Spring Cloud Sleuth 源码,发现 Zipkin 使用 ThreadLocal 来存储 traceId,只能在当前线程获取,无法子线程传递或线程池传递,获取需要改造 Zipkin 使用 TransmittableThreadLocal 存储 traceId。 通过看源码,发现存储 traceId 的代码逻辑在 SpanContextHolder
class SpanContextHolder {
private static final ThreadLocal<SpanContextHolder.SpanContext> CURRENT_SPAN = new NamedThreadLocal("Trace Context");
}
而 NamedThreadLocal
继承于 ThreadLocal
public class NamedThreadLocal<T> extends ThreadLocal<T> {
}
然后再看哪里调用了 SpanContextHolder
类,发现在 DefaultTracer
类中调用了 SpanContextHolder
,再看哪里初始化了 DefaultTracer
,再追踪到了 TraceAsyncConfiguration
类
@Configuration
@ConditionalOnProperty(
value = {"spring.sleuth.enabled"},
matchIfMissing = true
)
@EnableConfigurationProperties({TraceKeys.class, SleuthProperties.class})
public class TraceAutoConfiguration {
@Bean
@ConditionalOnMissingBean({Tracer.class})
public DefaultTracer sleuthTracer(Sampler sampler, Random random, SpanNamer spanNamer, SpanLogger spanLogger, SpanReporter spanReporter, TraceKeys traceKeys) {
return new DefaultTracer(sampler, random, spanNamer, spanLogger, spanReporter, this.properties.isTraceId128(), traceKeys);
}
}
看到这里,发现 DefaultTracer
的创建使用了 @ConditionalOnMissingBean({Tracer.class})
,那就说明了只要自定义一个 Tracer
,TraceAutoConfiguration
中的 DefaultTracer
就不再创建了。
解决步骤
第一步: 创建自己的 TraceAutoConfiguration
配置类
@Order
@Configuration
@ConditionalOnClass(TraceAsyncAspect.class)
@ConditionalOnProperty(value = {"spring.sleuth.async.enabled", "spring.sleuth.enabled"}, matchIfMissing = true)
@EnableConfigurationProperties({TraceKeys.class, SleuthProperties.class})
public class MyTraceAsyncConfiguration {
@Autowired
private SleuthProperties properties;
@Bean
public MyTracer sleuthTracer(Sampler sampler, Random random,
SpanNamer spanNamer, SpanLogger spanLogger,
SpanReporter spanReporter, TraceKeys traceKeys) {
return new MyTracer(sampler, random, spanNamer, spanLogger,
spanReporter, this.properties.isTraceId128(), traceKeys);
}
}
第二步: 该配置类里面创建的 Trace 类则是我们自定义类,把原有的 DefaultTracer
拷贝出来改名成我们自定义类名(如上面的 MyTracer
),把 MyTracer
类中使用了 SpanContextHolder
替换成自定义的 SpanContextHolder
。
第三步: 创建自定义的 SpanContextHolder
,拷贝 SpanContextHolder
进行改造,把里面使用的 NamedThreadLocal
替换成自定义的 NamedThreadLocal
。
class MySpanContextHolder {
private static final ThreadLocal<SpanContext> CURRENT_SPAN = new NamedTransmittableThreadLocal<>("Trace Context");
}
第四步: 把 NamedThreadLocal
拷贝进行改造,继承于 TransmittableThreadLocal
即可。
public class NamedTransmittableThreadLocal<T> extends TransmittableThreadLocal<T> {
}
扩展话题
通过上面的问题可以举一反三,只要是跟子线程或者线程池之间的数据传输问题,都可以通过 TransmittableThreadLocal 来处理,如果是在子线程或者线程池内的日志中打印 ThrealLocal
的数据,可以通过如下方式解决:
- Log4j2 MDC 集成 TTL
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>log4j2-ttl-thread-context-map</artifactId>
<version>1.2.0</version>
</dependency>
- Logback MDC 集成 TTL
<dependency>
<groupId>com.ofpay</groupId>
<artifactId>logback-mdc-ttl</artifactId>
<version>1.0.2</version>
</dependency>
总结
在上述问题中,使用 TransmittableThreadLocal 解决子线程或者线程池之间的数据传输问题,在我们平时开发过程中,也有很多类似的场景,比如我们使用 ThreadLocal 存储用户信息,但是需要在子线程或者线程池中获取用户数据,我们常用的 shiro
API SecurityUtils.getSubject()
也是通过 InheritableThreadLocal
存储 Subject
。