- Spring Cloud Sleuth介紹
1.1 是什么
Spring Cloud Sleuth提供了Spring Cloud分布式追蹤解決方案的API,集成了OpenZipKin Brave(推特的開源框架)
,可以跟蹤服務(wù)的請(qǐng)求和消息;Spring Cloud Sleuth的分布式跟蹤支持SpringBoot自動(dòng)配置。
1.2 特點(diǎn)
(1)搭配Slf4j MDC 和 logback,添加跟蹤數(shù)據(jù)(traceId和spanId),可以在日志文件中從給定的跟蹤或者跨度提取所有的日志,其中跨系統(tǒng)用traceId串起來,內(nèi)部線程之間是spanId。
(2)支持常見的Spring應(yīng)用程序,例如servlet filter,restTemplate,feignClient,scheduled actions和message channels等
(3)可以搭配組件ZipKin,使用 spring-cloud-sleuth-zipkin 組件,ZipKin可以將日志收集,進(jìn)行可視化展示和全文搜索,并根據(jù)日志信息數(shù)據(jù)進(jìn)行性能分析、數(shù)據(jù)分析和鏈路優(yōu)化等功能。
(4)Sleuth作為分布式追蹤解決方案,支持多線程下日志鏈路追蹤。
(5)Sleuth默認(rèn)支持大部分服務(wù)訪問做鏈路追蹤,目前支持的有:rxjava、feign、quartz、RestTemplate、grpc、kafka、redis等。
1.3 常見名詞
(1)Span:基本的工作單位,每個(gè)線程或者某次請(qǐng)求就是一個(gè)span;它是由一個(gè)64位的spanId和標(biāo)記所屬的Trace的TraceId組成,同時(shí)也會(huì)包含些額外的信息,比如說:進(jìn)程ID,服務(wù)名稱和tags標(biāo)簽信息等。
(2)Trace:形成樹形結(jié)構(gòu)的一組跨度,一條完整的鏈路追蹤就是Trace;內(nèi)部包含多個(gè)Span。
(3)TraceContext:傳播上下文,它包含鏈路數(shù)據(jù)信息;主要有鏈路標(biāo)識(shí)符(traceId、spanId和parentId等)、采樣數(shù)據(jù)和采樣狀態(tài)等。
(4)traceId:整個(gè)鏈路跟蹤的標(biāo)識(shí)符。
(5)spanId:某個(gè)線程或者某次請(qǐng)求即span的標(biāo)識(shí)符
(6)parentId:當(dāng)前span父節(jié)點(diǎn)的spanId,例如A->B,此時(shí)A的spanId就是B的parentId。
各名詞之間關(guān)系圖


通過以上流程圖可知,Sleuth底層邏輯調(diào)用鏈追蹤有兩個(gè)任務(wù),一是標(biāo)記出一次調(diào)用請(qǐng)求種所有的日志,即通過TraceId將所有服務(wù)日志串聯(lián)起來,形成一個(gè)完整的調(diào)用鏈;二是梳理日志前后關(guān)系,使用的SpanId和ParentSpanId來標(biāo)記,每個(gè)服務(wù)的調(diào)用都有一個(gè)唯一的SpanId,ParentSpanId代表上個(gè)服務(wù)即調(diào)用方的SpanId。其中traceId、spanId和parentId等組成TraceContext傳播上下文。
1.4 實(shí)操
1.4.1 引入組件Sleuth
本次使用的Spring Cloud Sleuth版本是2.2.8.RELEASE,對(duì)應(yīng)的Spring Boot、Spring Cloud和Spring Cloud Alibaba的版本是
Spring Cloud Alibaba Version
Spring Cloud Version
Spring Boot Version
2.2.7.RELEASE
Spring Cloud Hoxton.SR12
2.3.12.RELEASE
在Springboot項(xiàng)目中加入sleuth組件依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
加入依賴后,在logback-spring.xml的pattern處加上配置[trace=%X{X-B3-TraceId:-},span=%X{X-B3-SpanId:-}],示例如下
<pattern>%d %-5p [%F:%L] [trace=%X{X-B3-TraceId:-},span=%X{X-B3-SpanId:-}] %markMsg%n</pattern>
啟動(dòng)服務(wù),日志打印如下:
2024-02-22 17:23:56,695 INFO [DynamicServerListLoadBalancer.java:222] [trace=57a46058a9ca15fc,span=57a46058a9ca15fc] Using serverListUpdater PollingServerListUpdater
2024-02-22 17:23:56,721 INFO [DynamicServerListLoadBalancer.java:150] [trace=57a46058a9ca15fc,span=57a46058a9ca15fc] DynamicServerListLoadBalancer for client paygateway initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=paygateway,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@61a8c9e7
1.4.2 擴(kuò)展應(yīng)用
在跟蹤鏈路中添加requestId
在實(shí)際使用中,通常我們想將服務(wù)端某條日志記錄和客戶端的請(qǐng)求關(guān)聯(lián)起來,那么我們可以將客戶端的requestId增加到數(shù)據(jù)鏈路中,并在日志中打印出來。
要實(shí)現(xiàn)這個(gè)功能,就要使用到Sleuth中往傳播上下文添加額外信息的功能點(diǎn),傳播上下文除了traceId和spanId等這些必要字段,其他都稱為額外字段,這些額外字段的簡(jiǎn)單名稱是“Baggage”。
a.定義額外數(shù)據(jù)包裝類
import brave.baggage.BaggageField;
import brave.baggage.CorrelationScopeConfig;
import brave.context.slf4j.MDCScopeDecorator;
import brave.propagation.CurrentTraceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TracerBaggageConfig {
@Bean
public BaggageField baggageField(){
return BaggageField.create("requestId");
}
@Bean
public CurrentTraceContext.ScopeDecorator mdcScopeDecorator(){
return MDCScopeDecorator.newBuilder()
.clear()
.add(CorrelationScopeConfig.SingleCorrelationField.newBuilder(baggageField())
//實(shí)時(shí)刷新
.flushOnUpdate()
.build())
.build();
}
}
b.在過濾器中將requestId的值加到傳播上下文TraceContext
import brave.Span;
import brave.Tracer;
import brave.baggage.BaggageField;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.sleuth.instrument.web.TraceWebServletAutoConfiguration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class CustomSpanFilter implements GlobalFilter, Ordered {
@Autowired
private Tracer tracer;
@Autowired
private BaggageField baggageField;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Span currentSpan = this.tracer.currentSpan();
if(currentSpan == null){
return chain.filter(exchange);
}
//目前是從header中獲取requestId,在實(shí)際使用中請(qǐng)自定義
HttpHeaders headers = exchange.getRequest().getHeaders();
String requestId = headers.getFirst("requestId");
if(requestId == null){
return chain.filter(exchange);
}
//設(shè)置requestId的值
baggageField.updateValue(currentSpan.context(),requestId);
return chain.filter(exchange);
}
/**
* 在TraceContext中的跟蹤數(shù)據(jù)添加完后執(zhí)行
* @return
*/
@Override
public int getOrder() {
return TraceWebServletAutoConfiguration.TRACING_FILTER_ORDER + 1;
}
}
除了代碼還需要配置兩個(gè)配置項(xiàng)
spring:
sleuth:
baggage:
# 要添加到MDC的上下文字段列表
correlation-fields:
- requestId
# 設(shè)置接收并傳播到遠(yuǎn)程服務(wù)的字段列表
remoteFields:
- requestId
logback-spring.xml中加上requestId配置
<pattern>%d %-5p [%F:%L] [trace=%X{X-B3-TraceId:-},span=%X{X-B3-SpanId:-},requestId=%X{requestId:-}] %markMsg%n</pattern>
運(yùn)行后日志
2024-02-23 11:56:29,578 INFO [ServiceInfoHolder.java:184] [trace=5c40370d8161278a,span=5c40370d8161278a,requestId=12345] init new ips(0) service: DEFAULT_GROUP@@transaction -> []
2024-02-23 11:56:29,579 INFO [ServiceInfoHolder.java:169] [trace=5c40370d8161278a,span=5c40370d8161278a,requestId=12345] current ips:(0) service: DEFAULT_GROUP@@transaction -> []
2024-02-23 11:56:29,581 INFO [DynamicServerListLoadBalancer.java:150] [trace=5c40370d8161278a,span=5c40370d8161278a,requestId=12345] DynamicServerListLoadBalancer for client transaction initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=transaction,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@4a4da60b
注意:向MDC中添加額外字段,會(huì)影響性能,可以加但不要加多。
跨線程日志跟蹤功能
sleuth數(shù)據(jù)跟蹤兼容線程,需要將Runnable/Callable包裝在sleuth的包裝類中,代碼塊示例如下
a.Runnable
/**
* 定義線程
*/
Runnable runnable = new Runnable(){
@Override
public void run(){
//do same work
}
}
// Tracing tracing 該對(duì)象注入進(jìn)來
//方法一 手動(dòng)創(chuàng)建帶有顯式"calculateTax" Span名稱的"TraceRunnable"
Runnable traceRunnable = new TraceRunnable(tracing,spanName,runnable,"calculateTax")
//方法二 用"Tracing"包裝"Runnable",此方法要保證當(dāng)前span可用
Runnable traceRunnableFromTracer = traceing.currentTraceContext().wrap(runnable);
b.Callable
/**
* 定義線程
*/
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
return someLogic();
}
}
};
// Tracing tracing 該對(duì)象注入進(jìn)來
// 方法一 手動(dòng)創(chuàng)建帶有顯式"calculateTax" Span名稱的"TraceRunnable"
Callable<String> traceCallable = new TraceCallable<>(tracing, spanNamer,
callable, "calculateTax");
//方法二 用"Tracing"包裝"Callable",此方法要保證當(dāng)前span可用
Callable<String> traceCallableFromTracer = tracing.currentTraceContext()
.wrap(callable);
sleuth的包裝方式有兩種,一種是使用TraceCallable顯示包裝,一種是使用Tracing,在當(dāng)前span中設(shè)置去
多線程鏈路追蹤
Sleuth支持對(duì)異步任務(wù)的鏈路追蹤,在項(xiàng)目中使用@Async 注解開啟一個(gè)異步任務(wù)后,Sleuth會(huì)為異步任務(wù)重新生成一個(gè)Span,但是如果使用了自定義的異步任務(wù)線程池,則會(huì)導(dǎo)致Sleuth無法創(chuàng)建一個(gè)Span,而是會(huì)重新生成Trace和Span。此時(shí)需要使用Sleuth退供的Executor類來包裝異步任務(wù)線程池,才能在異步任務(wù)調(diào)用鏈路中重新創(chuàng)建Span。
使用@Async注解開啟異步任務(wù)
(1)先在服務(wù)的ServiceImpl接口中定義一個(gè)asyncMethod()方法
@Async
@Override
public void asyncMethod(){
log.info("執(zhí)行異步任務(wù)。。")
}
(2)在Controller或者service中調(diào)用
@GetMapping(value = "/async/api")
public String asyncApi() {
log.info("執(zhí)行異步任務(wù)開始...");
testService.asyncMethod();
log.info("異步任務(wù)執(zhí)行結(jié)束...");
return "asyncApi";
}
自定義任務(wù)線程池
使用Sleuth提供的Executor類來定義異步任務(wù)線程池,Sleuth包含所有的Executor類,主要有LazyTraceExecutor、LazyTraceExecutor、TraceableExecutorService、LazyTraceThreadPoolTaskScheduler和LazyTraceThreadPoolTaskExecutor等,詳情請(qǐng)參考源碼

LazyTraceThreadPoolTaskExecutor是繼承ThreadPoolTaskExecutor,用法相似。其他Sleuth的Executor類實(shí)現(xiàn)原理相同。
用LazyTraceThreadPoolTaskExecutor實(shí)現(xiàn)線程池,示例如下:
a.在config包下創(chuàng)建ThreadPoolTaskExecutorConfig類,用來自定義異步任務(wù)線程池
@Configuration
@EnableAsync
public class AsyncThreadPoolConfig{
@Autowired
private BeanFactory beanFactory;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor getScorePoolTaskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPollTaskExecutor();
//核心線程數(shù)
taskExecutor.setCorePoolSize(2);
//線程池維護(hù)線程的最大數(shù)量
taskExecutor.setMaxPoolSize(10);
//緩存隊(duì)列大小
taskExecutor.setQueueCapacity(10);
//允許的空閑時(shí)間,當(dāng)超過了核心線程數(shù)之外的線程在空閑時(shí)間到達(dá)之后會(huì)被銷毀
taskExecutor.setKeepAliveSeconds(20);
//異步方法內(nèi)部線程名稱
taskExecutor.setThreadNamePrefix("trace-thread-");
return new LazyTraceThreadPoolTaskExecutor(beanFactory,taskExecutor);
}
}
線程池定義好后,在服務(wù)中使用線程池來實(shí)現(xiàn)方法的異步調(diào)用。在實(shí)際使用過程中,可以把線程池中的配置數(shù)據(jù)改成可配的。
消息組件的鏈路追蹤
Sleuth支持對(duì)RPC設(shè)置日志跟蹤上下文,可以通過接收RPC請(qǐng)求中的跟蹤信息,將跟蹤信息設(shè)置到跟蹤上下文中,從而實(shí)現(xiàn)對(duì)RPC日志數(shù)據(jù)跟蹤的支持。以下拿RabbitMQ來舉例,如何實(shí)現(xiàn)將外部的traceId設(shè)置當(dāng)前的傳播上下文中
(1)生產(chǎn)者側(cè)
生產(chǎn)者發(fā)送消息時(shí),需要把當(dāng)前traceId設(shè)置到message中,這樣做的目的是將上層的traceId發(fā)送到下層,代碼示例如下
Message message = new Message(msgTopic, Msgtag, msg.getBytes());
String traceId = MDC.get("X-B3-TraceId");
message.putUserProperties("traceId",traceId);
message.setKey(key);
producer.sendOneway(message);
(2)消費(fèi)者側(cè)
消費(fèi)者消費(fèi)消息時(shí),需要將消息中的traceId設(shè)置到當(dāng)前傳播上下文中
設(shè)置信息提取器
public class TraceRemoteGetter implements Propagation.RemoteGetter<Map<String,String>> {
@Override
public Span.Kind spanKind() {
return Span.Kind.SERVER;
}
@Override
public String get(Map<String, String> request, String fieldName) {
return request.get(fieldName);
}
}
將上層的traceId設(shè)置當(dāng)前處理中
// Tracing tracing 該對(duì)象注入進(jìn)來
//配置從請(qǐng)求中提取跟蹤上下文的函數(shù)
TraceContext.Extractor<Map<String,String>> extractor = tracing.propagation()
.extractor(new TraceRemoteGetter());
Map<String,String> map = new HashMap<>();
//traceId是從消費(fèi)的消息中獲取到的上層traceId
//這個(gè)traceId不能隨便定義,需要通過采用系統(tǒng)規(guī)則定義,因?yàn)樵O(shè)置之后系統(tǒng)會(huì)轉(zhuǎn)換成數(shù)據(jù),
//如果不合規(guī)則會(huì)重新生成
map.put("X-B3-TraceId",traceId);
map.put("X-B3-ParentSpanId",traceId);
map.put("X-B3-Sampled","0");
//將外部traceId設(shè)置到上下文
TraceContextOrSamplingFlags extracted = extractor.extract(map);
Span span = tracing.tracer().nextSpan(extracted);
//當(dāng)新的TraceContext設(shè)置為當(dāng)前上下文
CurrentTraceContext.Scope scope = tracing.currentTraceContext().nextSpan(span.context());
log.info("重新定義日志跟蹤信息");
//此方法一定要執(zhí)行 不然會(huì)報(bào)錯(cuò)
scope.close();
注意:從上層傳過來的traceId的值不能隨便定義,需要通過采用系統(tǒng)規(guī)則定義,因?yàn)樵O(shè)置之后系統(tǒng)會(huì)轉(zhuǎn)換為數(shù)字,如果不合規(guī)則就會(huì)重新生成,那么traceId無效
還更多功能待開挖,感興趣請(qǐng)看:https://docs.spring.io/spring-cloud-sleuth/docs/2.2.8.RELEASE/reference/html/