spring cloud-sleuth原理浅析

清疚 2022-11-20 03:56 602阅读 0赞

本文基于sleuth 2.2.5版本

sleuth是一个链路追踪工具,通过它在日志中打印的信息可以分析出一个服务的调用链条,也可以得出链条中每个服务的耗时,这为我们在实际生产中,分析超时服务,分析服务调用关系,做服务治理提供帮助。
第一次使用sleuth,虽说跟着网上的教程也可以运行出正确的结果,但是对于原理、更进一步的使用还是一头蒙。我就尝试着分析一下源代码,其代码量并不大,但是代码还真是难懂,看了一段时间源码,并从网上找了资料,只是对原理、部分类的作用有了一些了解,我通过本文做一下介绍。

文章目录

  • 一、概念介绍
  • 二、场景描述
  • 三、原理解析
    • 1、spring.factories文件
    • 2、处理日志打印
    • 3、TracingFilter过滤器
    • 4、拦截RestTemplate
  • 四、链路信息抽样
  • 五、总结

一、概念介绍

先说几个概念。
span:span是sleuth中最基本的工作单元,一个微服务收到请求后会创建一个span同时产生一个span id,span id是一个64位的随机数,sleuth将其转化为16进制的字符串,打印在日志里面。其对应的实现类是RealSpan。
trace id:在一个调用链条中,trace id是始终不变的,每经过一个微服务span id生成一个新的,所以通过trace id可以找出调用链上所有经过的微服务。trace id默认是64位,可以通过spring.sleuth.traceId128=true设置trace id为128位。调用链的第一个服务,其span id和trace id是同一个值。

sleuth目前并不是对所有调用访问都可以做链路追踪,它目前支持的有:rxjava、feign、quartz、RestTemplate、zuul、hystrix、grpc、kafka、Opentracing、redis、Reator、circuitbreaker、spring的Scheduled。国内用的比较多的dubbo,sleuth无法对其提供支持。

二、场景描述

本文以http访问介绍一下sleuth原理。本文介绍的场景是从浏览器发起start请求,然后在服务中通过RestTemplate访问另一个服务end。代码如下:

  1. @RestController
  2. public class TestController {
  3. private static Logger log = LoggerFactory.getLogger(TestController.class);
  4. @Autowired
  5. private RestTemplate restTemplate;
  6. @RequestMapping("start")
  7. public String start(){
  8. log.info("start收到请求");
  9. restTemplate.getForObject("http://localhost:8081/end",String.class);
  10. log.info("start请求处理结束");
  11. return "1";
  12. }
  13. @RequestMapping("end")
  14. public String end(){
  15. log.info("end收到请求");
  16. log.info("end请求处理结束");
  17. return "2";
  18. }
  19. @Bean
  20. public RestTemplate getRestTemplate(){
  21. return new RestTemplate();
  22. }
  23. }

下面按照该场景介绍一下sleuth如何执行的。

三、原理解析

1、spring.factories文件

spring boot启动时,需要执行自动配置类。自动配置类都在sleuth-core.jar包的spring.factories文件中。

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2. # 下面三个自动配置类在任何场景下都需要执行,它们是基础类
  3. org.springframework.cloud.sleuth.annotation.SleuthAnnotationAutoConfiguration,\
  4. org.springframework.cloud.sleuth.autoconfig.TraceAutoConfiguration,\
  5. org.springframework.cloud.sleuth.propagation.SleuthTagPropagationAutoConfiguration,\
  6. #下面每个自动配置类是应用于具体框架或者中间件的,比如
  7. #TraceWebClientAutoConfiguration:对RestTemplate、WebClient等创建拦截器,当发出请求时可以对其拦截在请求的header中添加链路信息
  8. org.springframework.cloud.sleuth.instrument.web.TraceHttpAutoConfiguration,\
  9. org.springframework.cloud.sleuth.instrument.web.TraceWebServletAutoConfiguration,\
  10. org.springframework.cloud.sleuth.instrument.web.client.TraceWebClientAutoConfiguration,\
  11. org.springframework.cloud.sleuth.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\
  12. org.springframework.cloud.sleuth.instrument.async.AsyncAutoConfiguration,\
  13. org.springframework.cloud.sleuth.instrument.async.AsyncCustomAutoConfiguration,\
  14. org.springframework.cloud.sleuth.instrument.async.AsyncDefaultAutoConfiguration,\
  15. org.springframework.cloud.sleuth.instrument.scheduling.TraceSchedulingAutoConfiguration,\
  16. org.springframework.cloud.sleuth.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\
  17. org.springframework.cloud.sleuth.instrument.hystrix.SleuthHystrixAutoConfiguration,\
  18. org.springframework.cloud.sleuth.instrument.circuitbreaker.SleuthCircuitBreakerAutoConfiguration,\
  19. org.springframework.cloud.sleuth.instrument.rxjava.RxJavaAutoConfiguration,\
  20. org.springframework.cloud.sleuth.instrument.reactor.TraceReactorAutoConfiguration,\
  21. org.springframework.cloud.sleuth.instrument.web.TraceWebFluxAutoConfiguration,\
  22. org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration,\
  23. org.springframework.cloud.sleuth.instrument.rpc.TraceRpcAutoConfiguration,\
  24. org.springframework.cloud.sleuth.instrument.grpc.TraceGrpcAutoConfiguration,\
  25. org.springframework.cloud.sleuth.instrument.messaging.SleuthKafkaStreamsConfiguration,\
  26. org.springframework.cloud.sleuth.instrument.messaging.TraceMessagingAutoConfiguration,\
  27. org.springframework.cloud.sleuth.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\
  28. org.springframework.cloud.sleuth.instrument.messaging.TraceSpringMessagingAutoConfiguration,\
  29. org.springframework.cloud.sleuth.instrument.messaging.websocket.TraceWebSocketAutoConfiguration,\
  30. org.springframework.cloud.sleuth.instrument.opentracing.OpentracingAutoConfiguration,\
  31. org.springframework.cloud.sleuth.instrument.redis.TraceRedisAutoConfiguration,\
  32. org.springframework.cloud.sleuth.instrument.quartz.TraceQuartzAutoConfiguration
  33. # TraceEnvironmentPostProcessor后处理器与日志打印相关
  34. org.springframework.boot.env.EnvironmentPostProcessor=\
  35. org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor

2、处理日志打印

先来看一下后处理器TraceEnvironmentPostProcessor,TraceEnvironmentPostProcessor用于处理日志打印,如果应用程序不设置日志打印格式,那么该类会设置默认的打印格式,该类比较简单。

  1. public void postProcessEnvironment(ConfigurableEnvironment environment,
  2. SpringApplication application) {
  3. Map<String, Object> map = new HashMap<String, Object>();
  4. // This doesn't work with all logging systems but it's a useful default so you see
  5. // traces in logs without having to configure it.
  6. //将打印的日志格式存入map中
  7. //日志打印一共四个内容:应用名、trace id、span id、是否发送到zipkin
  8. if (Boolean
  9. .parseBoolean(environment.getProperty("spring.sleuth.enabled", "true"))) {
  10. map.put("logging.pattern.level", "%5p [${spring.zipkin.service.name:"
  11. + "${spring.application.name:}},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]");
  12. }
  13. addOrReplace(environment.getPropertySources(), map);
  14. }
  15. //下面这个方法用于将日志打印的格式设置到默认配置中
  16. //如果应用没有设置打印格式,则使用默认配置
  17. private void addOrReplace(MutablePropertySources propertySources,
  18. Map<String, Object> map) {
  19. MapPropertySource target = null;
  20. if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
  21. PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME);
  22. if (source instanceof MapPropertySource) {
  23. target = (MapPropertySource) source;
  24. for (String key : map.keySet()) {
  25. if (!target.containsProperty(key)) {
  26. target.getSource().put(key, map.get(key));
  27. }
  28. }
  29. }
  30. }
  31. if (target == null) {
  32. target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
  33. }
  34. if (!propertySources.contains(PROPERTY_SOURCE_NAME)) {
  35. propertySources.addLast(target);
  36. }
  37. }

日志打印的设置完毕后,在来看SleuthLogAutoConfiguration,该类也是与日志打印相关的。该类的注释是:

  1. * {@link Configuration} that adds a {@link Slf4jScopeDecorator} that prints tracing information in the logs.

意思是SleuthLogAutoConfiguration将Slf4jScopeDecorator添加到配置中,可以在日志中打印追踪信息。从注释中可以看出SleuthLogAutoConfiguration只与Slf4j对接。下面来看一下代码:

  1. @Configuration(proxyBeanMethods = false)
  2. @ConditionalOnClass(MDC.class)
  3. @EnableConfigurationProperties(SleuthSlf4jProperties.class)
  4. public static class Slf4jConfiguration {
  5. @Bean
  6. @ConditionalOnProperty(value = "spring.sleuth.log.slf4j.enabled",
  7. matchIfMissing = true)
  8. static CurrentTraceContext.ScopeDecorator slf4jSpanDecorator(
  9. SleuthProperties sleuthProperties,
  10. SleuthSlf4jProperties sleuthSlf4jProperties) {
  11. return new Slf4jScopeDecorator(sleuthProperties, sleuthSlf4jProperties);
  12. }
  13. }

slf4jSpanDecorator创建Slf4jScopeDecorator对象并放入spring容器中。
下面是Slf4jScopeDecorator的构造方法。

  1. Slf4jScopeDecorator(SleuthProperties sleuthProperties,
  2. SleuthSlf4jProperties sleuthSlf4jProperties) {
  3. //下面四个add方法的入参便是可以在日志中打印的属性
  4. CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder().clear()
  5. .add(SingleCorrelationField.create(BaggageFields.TRACE_ID))
  6. .add(SingleCorrelationField.create(BaggageFields.PARENT_ID))
  7. .add(SingleCorrelationField.create(BaggageFields.SPAN_ID))
  8. .add(SingleCorrelationField.newBuilder(BaggageFields.SAMPLED)
  9. .name("spanExportable").build());
  10. Set<String> whitelist = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
  11. whitelist.addAll(sleuthSlf4jProperties.getWhitelistedMdcKeys());
  12. //除了sleuth指定的四个属性外,应用程序还可以自定义一些参数打印或者传输到zipkin
  13. Set<String> retained = new LinkedHashSet<>();
  14. retained.addAll(sleuthProperties.getBaggageKeys());
  15. retained.addAll(sleuthProperties.getLocalKeys());
  16. retained.addAll(sleuthProperties.getPropagationKeys());
  17. retained.retainAll(whitelist);
  18. for (String name : retained) {
  19. builder.add(SingleCorrelationField.newBuilder(BaggageField.create(name))
  20. .dirty().build());
  21. }
  22. this.delegate = builder.build();
  23. }

程序运行的时候还会调用该类的decorateScope方法将要打印的内容设置到MDC里面。本文后面介绍decorateScope方法。

3、TracingFilter过滤器

如果在web环境下运行时,spring boot会自动创建TracingFilter,该类创建是在TraceWebServletAutoConfiguration中完成的:

  1. @Bean
  2. @ConditionalOnMissingBean
  3. public TracingFilter tracingFilter(HttpTracing tracing) {
  4. return (TracingFilter) TracingFilter.create(tracing);
  5. }

TracingFilter实现了javax.servlet.Filter接口,spring将TracingFilter作为web过滤器设置到web容器中,这样TracingFilter会对所有的网络请求拦截。下面看一下doFilter方法(代码有删减):

  1. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  2. throws IOException, ServletException {
  3. //代码删减
  4. //下面这行代码用于创建Span对象,如果是调用链的第一个服务,
  5. //则会生产trace id和span id,如果不是第一个则会读取请求报文的header信息,
  6. //将header的trace id和span id设置到Span对象中。
  7. //Span对象的实现类是RealSpan
  8. Span span = handler.handleReceive(new HttpServletRequestWrapper(req));
  9. // Add attributes for explicit access to customization or span context
  10. request.setAttribute(SpanCustomizer.class.getName(), span.customizer());
  11. request.setAttribute(TraceContext.class.getName(), span.context());
  12. SendHandled sendHandled = new SendHandled();
  13. request.setAttribute(SendHandled.class.getName(), sendHandled);
  14. Throwable error = null;
  15. //newScope方法用于将Span对象的trace id、span id等设置到sl4j的MDC中
  16. Scope scope = currentTraceContext.newScope(span.context());
  17. try {
  18. // any downstream code can see Tracer.currentSpan() or use Tracer.currentSpanCustomizer()
  19. chain.doFilter(req, res);
  20. } catch (Throwable e) {
  21. error = e;
  22. throw e;
  23. } finally {
  24. // When async, even if we caught an exception, we don't have the final response: defer
  25. if (servlet.isAsync(req)) {
  26. servlet.handleAsync(handler, req, res, span);
  27. } else if (sendHandled.compareAndSet(false, true)){
  28. // we have a synchronous response or error: finish the span
  29. HttpServerResponse responseWrapper = HttpServletResponseWrapper.create(req, res, error);
  30. handler.handleSend(responseWrapper, span);
  31. }
  32. scope.close();
  33. }
  34. }

在doFilter里面,创建Span对象时,如果是调用链的第一个服务,那么span id是一个long型的随机数,然后设置trace id=span id。如果不是第一个服务,则将header里面的trace id、span id、parent span id直接设置到新建的Span对象。
当将trace id、span id设置到MDC时,就要调用之前提到的Slf4jScopeDecorator.decorateScope方法。

  1. public Scope decorateScope(TraceContext context, Scope scope) {
  2. return LEGACY_IDS.decorateScope(context, delegate.decorateScope(context, scope));
  3. }

delegate.decorateScope方法根据Slf4jScopeDecorator的构造方法的add方法添加的属性从入参context里面读取属性值,然后调用MDC.put设置到MDC中。这样打印日志的时候就可以将这些信息打印出来。
在MDC中存放的属性有:X-B3-TraceId、X-B3-SpanId、X-Span-Export、X-B3-ParentSpanId、traceId、spanId、spanExportable、parentId。
到这里为止,span的创建和日志的打印都准备完成了,进入应用程序后,我们打印的日志就都可以展示span id、trace id等信息了。

4、拦截RestTemplate

下面再来看一下sleuth如何拦截RestTemplate,将span id等信息加入到http请求的header里面。
spring boot启动时执行TraceWebClientAutoConfiguration自动配置,该类中有一个内部类TraceRestTemplateBeanPostProcessor :

  1. class TraceRestTemplateBeanPostProcessor implements BeanPostProcessor {
  2. //spring容器
  3. private final BeanFactory beanFactory;
  4. TraceRestTemplateBeanPostProcessor(BeanFactory beanFactory) {
  5. this.beanFactory = beanFactory;
  6. }
  7. @Override
  8. public Object postProcessBeforeInitialization(Object bean, String beanName)
  9. throws BeansException {
  10. return bean;
  11. }
  12. @Override
  13. //bean对象初始化后要执行该后处理器
  14. public Object postProcessAfterInitialization(Object bean, String beanName)
  15. throws BeansException {
  16. if (bean instanceof RestTemplate) {
  17. //如果spring容器中有RestTemplate对象,则对其进一步加工
  18. //inject方法见下面
  19. RestTemplate rt = (RestTemplate) bean;
  20. new RestTemplateInterceptorInjector(interceptor()).inject(rt);
  21. }
  22. return bean;
  23. }
  24. //该方法返回拦截器,该拦截器会被添加到RestTemplate中,用于对http请求拦截
  25. private LazyTracingClientHttpRequestInterceptor interceptor() {
  26. return new LazyTracingClientHttpRequestInterceptor(this.beanFactory);
  27. }
  28. }

下面是RestTemplateInterceptorInjector的inject方法:

  1. void inject(RestTemplate restTemplate) {
  2. if (hasTraceInterceptor(restTemplate)) {
  3. return;
  4. }
  5. List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>(
  6. restTemplate.getInterceptors());
  7. interceptors.add(0, this.interceptor);
  8. //将拦截器设置的restTemplate对象中,添加的拦截器就是LazyTracingClientHttpRequestInterceptor
  9. //而且该拦截器还是第一个被调用的
  10. restTemplate.setInterceptors(interceptors);
  11. }

设置好拦截器后,当每次RestTemplate发起http请求时,都会被该拦截器拦截。
下面来看一下该拦截器如何运作的。

  1. class LazyTracingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
  2. private final BeanFactory beanFactory;
  3. private TracingClientHttpRequestInterceptor interceptor;
  4. LazyTracingClientHttpRequestInterceptor(BeanFactory beanFactory) {
  5. this.beanFactory = beanFactory;
  6. }
  7. @Override
  8. //当发起请求时,首先被方法拦截。
  9. public ClientHttpResponse intercept(HttpRequest request, byte[] body,
  10. ClientHttpRequestExecution execution) throws IOException {
  11. //下面代码调用TracingClientHttpRequestInterceptor的intercept方法
  12. return interceptor().intercept(request, body, execution);
  13. }
  14. private TracingClientHttpRequestInterceptor interceptor() {
  15. if (this.interceptor == null) {
  16. this.interceptor = this.beanFactory
  17. .getBean(TracingClientHttpRequestInterceptor.class);
  18. }
  19. return this.interceptor;
  20. }
  21. }

下面是TracingClientHttpRequestInterceptor的intercept方法:

  1. @Override
  2. public ClientHttpResponse intercept(HttpRequest req, byte[] body,
  3. ClientHttpRequestExecution execution) throws IOException {
  4. HttpRequestWrapper request = new HttpRequestWrapper(req);
  5. //创建一个Span对象,其中span id重新生成,trace id不变
  6. //还要将span id、trace id等信息添加到http的header中
  7. Span span = handler.handleSend(request);
  8. ClientHttpResponse response = null;
  9. Throwable error = null;
  10. //下面的newScope方法会更新MDC数据,
  11. //也就是执行完currentTraceContext.newScop方法后,
  12. //MDC的span id会改变,不过在ws中记录变化前和变化后的数据,当远程服务返回后,
  13. //sleuth执行Multiple的close方法,将变化前的值再次设置到MDC中
  14. try (Scope ws = currentTraceContext.newScope(span.context())) {
  15. //调用远程服务
  16. return response = execution.execute(req, body);
  17. } catch (Throwable e) {
  18. error = e;
  19. throw e;
  20. } finally {
  21. handler.handleReceive(new ClientHttpResponseWrapper(request, response, error), span);
  22. }
  23. }

RestTemplate每次发起请求时,拦截器会在http请求header中放入如下信息:

  1. x-b3-traceid = 7416922facfd03af #表示当前调用链的trace id
  2. x-b3-spanid = 73c6727b0a44195b #表示span id
  3. x-b3-parentspanid = 78bd09f345e0d7aa #调用链中前一个服务的span id
  4. x-b3-sampled = 1 #表示是否取样,1表示要将调用信息发送到zipkin

当服务访问完毕后,sleuth会将之前添加到MDC的数据再清理掉。

四、链路信息抽样

我们一般将sleuth与zipkin结合使用,sleuth默认会将收集的所有信息发送到zipkin,这样是非常耗性能的,所以sleuth提供了两个参数,可以使sleuth按照一定的比例将信息发送到zipkin。

  1. #probability表示抽样概率,如果设置为1,表示信息全部发送到zipkin,如果设置0.5,表示50%会发送
  2. spring.sleuth.sampler.probability
  3. #rate表示每秒收集信息的速率,对于访问量不大的请求,可以设置该参数
  4. #比如设置rate=50,表示无论访问量大小,每秒最多发送50个信息到zipkin
  5. spring.sleuth.sampler.rate

上述两个参数是由SamplerAutoConfiguration处理的。根据配置参数的不同,创建不同的对象:ProbabilityBasedSampler,RateLimitingSampler。

五、总结

上面分析了web环境下的sleuth执行原理。
首先sleuth创建TraceFilter,对所有的网络请求进行拦截,如果请求的header中没有span信息,则创建Span对象,生成span id、trace id等,如果header中有,则直接使用header中的数据创建Span对象,之后将span id、trace id设置到sl4j的MDC中。
当使用RestTemplate发送请求时,RestTemplateInterceptorInjector拦截器对请求拦截,将新生成的span id、trace id等信息设置到请求的header中。这样服务端收到请求后就可以从header中解析出Span信息。
其他场景的执行原理都是类似的。本文不再介绍。
我们通过日志看到的信息其实只是sleuth收集信息的一小部分,在运行过程中,sleuth还会收集服务调用时间、接收到请求的时间、发起http请求的方法、http请求的路径,包括请求的IP端口等信息,这些信息都会存入Span对象,然后发送到zipkin中。

发博词

是Spring Cloud Sleuth的原理不是zipkin的原理。

追踪原理

Spring Cloud Sleuth可以追踪10种类型的组件,async、Hystrix,messaging,websocket,rxjava,scheduling,web(Spring MVC Controller,Servlet),webclient(Spring RestTemplate)、Feign、Zuul。下面是常用的八种类型。

Scheduled

原理是AOP处理Scheduled注解
TraceSchedulingAspect可以带出,只要是在IOC容器中的Bean带有@Scheduled注解的方法的调用都会被sleuth处理。

Messaging

原理是基于spring messaging的ChannelInterceptor。
TraceChannelInterceptor/IntegrationTraceChannelInterceptor
MessagingSpanTextMapExtractor和MessagingSpanTextMapInjector

Hystrix

原理是使用HystrixPlugins添加trace相关的plugin,自定义了一个HystrixConcurrencyStrategy的实现SleuthHystrixConcurrencyStrategy
具体参考TraceCommand和SleuthHystrixConcurrencyStrategy

Feign

原理是实现了两个Feign Client实例,一个不带Ribbon TraceFeignClient、一个带Ribbon,TraceLoadBalancerFeignClient
TraceFeignAspect AOP里面的逻辑是,有地方想获取Client实例,就拦截返回自己封装的Client。

Async

@Async注解和ThreadPoolTaskExecutor下面的类
具体参看TraceAsyncAspect

RestTempate

原理是spring client的Interceptor机制。具体参看TraceRestTemplateInterceptor。

Zuul

原理是zuul的Filter机制,ZuulFilter
实现了三个TracePreZuulFilter、TracePostZuulFilter两个Filter。

示例代码

示例代码提供了上述八种组件的追踪示例,项目结构如下:

  1. zipkin stream server
  2. eureka server
  3. Segment1[定时消息->消息中间件->监听消息中间件->feign+hystrix->feign+hystrix]
    ->Segment2[controller+async+webclient,controller2(让zuul调用)]->Segment3[zuul]

具体请查看示例代码:
github spring-cloud-sleuth-samples

注意:
zipkin stream server 的${spring.sleuth.stream.group}配置需要放到外部指定,不然不管用。
spring.kafka.consumer.group-id=xxx,内外配置都不管用
spring.cloud.stream.bindings.seluth.group=xxx ,内外配置都不管用
spring.sleuth.stream.group=xxx,在内配置不管用,在外配置管用
具体原因参看:StreamEnvironmentPostProcessor

https://blog.csdn.net/xichenguan/article/details/77448288

https://blog.csdn.net/weixin_38308374/article/details/108897599

发表评论

表情:
评论列表 (有 0 条评论,602人围观)

还没有评论,来说两句吧...

相关阅读

    相关 CAS原理浅析

    CAS原理浅析 1.介绍 > CAS 比较并交换 compareAndSwap。 > CAS是一种乐观锁机制,也被称为无锁机制。全称: > Compare-

    相关 spring cloud-sleuth原理浅析

    > 本文基于sleuth 2.2.5版本 sleuth是一个链路追踪工具,通过它在日志中打印的信息可以分析出一个服务的调用链条,也可以得出链条中每个服务的耗时,这为我们在实际

    相关 Kafka原理浅析

    1.Kafka简介             Kafka 是一个消息系统,原本开发自 LinkedIn,用作 LinkedIn 的活动流(Activity Stream)和运营