概述
隨著 Spring Cloud 微服務架構的流行,一次請求往往需要涉及到多個服務,因此服務性能監(jiān)控和排查就變得更復雜。 通過 APM 幫助理解系統(tǒng)行為、用于分析性能問題的工具,以便發(fā)生故障的時候,能夠快速定位和解決問題。而 Spring Cloud 提供了 Spring Cloud Sleuth 可快速集成 Zipkin。
問題舉例
打印 traceId 有何意義?
如何在日志中打印 Zipkin traceId?
如何在子線程或線程池中如何獲取 Zipkin traceId 并打?。?/p>
問題解決
打印 traceId 意義
分布式環(huán)境下,微服務之間的調用錯綜復雜,如果突然爆出一個錯誤,雖然有日志記錄,但到底是哪個服務出了問題呢?是前端傳的參數(shù)錯誤,還是系統(tǒng)X或系統(tǒng)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 源碼,發(fā)現(xiàn) Zipkin 使用 ThreadLocal 來存儲 traceId,只能在當前線程獲取,無法子線程傳遞或線程池傳遞,獲取需要改造 Zipkin 使用 TransmittableThreadLocal 存儲 traceId。 通過看源碼,發(fā)現(xiàn)存儲 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 類,發(fā)現(xiàn)在 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);
}
}
看到這里,發(fā)現(xiàn) DefaultTracer 的創(chuàng)建使用了 @ConditionalOnMissingBean({Tracer.class}) ,那就說明了只要自定義一個 Tracer,TraceAutoConfiguration 中的 DefaultTracer 就不再創(chuàng)建了。
解決步驟
第一步: 創(chuàng)建自己的 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);
}
}
第二步: 該配置類里面創(chuàng)建的 Trace 類則是我們自定義類,把原有的 DefaultTracer 拷貝出來改名成我們自定義類名(如上面的 MyTracer),把 MyTracer 類中使用了 SpanContextHolder 替換成自定義的 SpanContextHolder。
第三步: 創(chuàng)建自定義的 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> {
}
擴展話題
通過上面的問題可以舉一反三,只要是跟子線程或者線程池之間的數(shù)據(jù)傳輸問題,都可以通過 TransmittableThreadLocal 來處理,如果是在子線程或者線程池內的日志中打印 ThrealLocal 的數(shù)據(jù),可以通過如下方式解決:
- 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 解決子線程或者線程池之間的數(shù)據(jù)傳輸問題,在我們平時開發(fā)過程中,也有很多類似的場景,比如我們使用 ThreadLocal 存儲用戶信息,但是需要在子線程或者線程池中獲取用戶數(shù)據(jù),我們常用的 shiroAPI SecurityUtils.getSubject()也是通過 InheritableThreadLocal存儲 Subject 。