解決使用 Spring Cloud Sleuth 在線程池日志中打印 traceId

概述

隨著 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 。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容