追踪自定义线程池
Sleuth支持对异步任务的链路追踪,在项目中使用@Async注解开启一个异步任务后,Sleuth会为异步任务重新生成一个Span。但是如果使用了自定义的异步任务线程池,则会导致Sleuth无法新创建一个Span,而是会重新生成Trace和Span。此时,需要使用Sleuth提供的LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建Span。
在服务中开启异步线程池任务,需要使用@EnableAsync。所以,在演示示例前,先在用户微服务shop-user的io.binghe.shop.UserStarter
启动类上添加@EnableAsync注解,如下所示。
/** * @author binghe * @version 1.0.0 * @description 启动用户服的类 */ @SpringBootApplication @EnableTransactionManagement(proxyTargetClass = true) @MapperScan(value = { "io.binghe.shop.user.mapper" }) @EnableDiscoveryClient @EnableAsync public class UserStarter { public static void main(String[] args){ SpringApplication.run(UserStarter.class, args); } }
演示使用@Async注解开启任务
(1)在用户微服务shop-user的io.binghe.shop.user.service.UserService
接口中定义一个asyncMethod()方法,如下所示。
void asyncMethod();
(2)在用户微服务shop-user的io.binghe.shop.user.service.impl.UserServiceImpl
类中实现asyncMethod()方法,并在asyncMethod()方法上添加@Async注解,如下所示。
@Async @Override public void asyncMethod() { log.info("执行了异步任务..."); }
(3)在用户微服务shop-user的io.binghe.shop.user.controller.UserController
类中新增asyncApi()方法,如下所示。
@GetMapping(value = "/async/api") public String asyncApi() { log.info("执行异步任务开始..."); userService.asyncMethod(); log.info("异步任务执行结束..."); return "asyncApi"; }
(4)分别启动用户微服务和网关服务,在浏览器中输入链接http://localhost:10001/server-user/user/async/api
(5)查看用户微服务与网关服务的控制台日志,分别存在如下日志。
- 用户微服务
[server-user,499d6c7128399ed0,a81bd920de0b07de,true]执行异步任务开始... [server-user,499d6c7128399ed0,a81bd920de0b07de,true]异步任务执行结束... [server-user,499d6c7128399ed0,e2f297d512f40bb8,true]执行了异步任务...
- 网关服务
[server-gateway,499d6c7128399ed0,499d6c7128399ed0,true]
可以看到Sleuth为异步任务重新生成了Span。
演示自定义任务线程池
在演示使用@Async注解开启任务的基础上继续演示自定义任务线程池,验证Sleuth是否为自定义线程池新创建了Span。
(1)在用户微服务shop-user中新建io.binghe.shop.user.config
包,在包下创建ThreadPoolTaskExecutorConfig类,继承org.springframework.scheduling.annotation.AsyncConfigurerSupport
类,用来自定义异步任务线程池,代码如下所示。
/** * @author binghe * @version 1.0.0 * @description Sleuth异步线程池配置 */ @Configuration @EnableAutoConfiguration public class ThreadPoolTaskExecutorConfig extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(10); executor.setThreadNamePrefix("trace-thread-"); executor.initialize(); return executor; } }
(2)以debug的形式启动用户微服务和网关服务,并在io.binghe.shop.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法中打上断点,如下所示。
可以看到,项目启动后并没有进入io.binghe.shop.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法,说明项目启动时,并不会创建异步任务线程池。
(3)在浏览器中输入链接http://localhost:10001/server-user/user/async/api
,此时可以看到程序已经执行到io.binghe.shop.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法的断点位置。
说明异步任务线程池是在调用了异步任务的时候创建的。
接下来,按F8跳过断点继续运行程序,可以看到浏览器上的显示结果如下。
(4)查看用户微服务与网关服务的控制台日志,分别存在如下日志。
- 用户微服务
[server-user,f89f2355ec3f9df1,4d679555674e96a4,true]执行异步任务开始... [server-user,f89f2355ec3f9df1,4d679555674e96a4,true]异步任务执行结束... [server-user,0ee48d47e58e2a42,0ee48d47e58e2a42,true]执行了异步任务...
- 网关服务
[server-gateway,f89f2355ec3f9df1,f89f2355ec3f9df1,true]
可以看到,使用自定义异步任务线程池时,在用户微服务中在执行异步任务时,重新生成了Trace和Span。
注意对比用户微服务中输出的三条日志信息,最后一条日志信息的TraceID和SpanID与前两条日志都不同。
演示包装自定义线程池
在自定义任务线程池的基础上继续演示包装自定义线程池,验证Sleuth是否为包装后的自定义线程池新创建了Span。
(1)在用户微服务shop-user的io.binghe.shop.user.config.ThreadPoolTaskExecutorConfig
类中注入BeanFactory,并在getAsyncExecutor()方法中使用org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor()
来包装返回的异步任务线程池,修改后的io.binghe.shop.user.config.ThreadPoolTaskExecutorConfig
类的代码如下所示。
/** * @author binghe * @version 1.0.0 * @description Sleuth异步线程池配置 */ @Configuration @EnableAutoConfiguration public class ThreadPoolTaskExecutorConfig extends AsyncConfigurerSupport { @Autowired private BeanFactory beanFactory; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(10); executor.setThreadNamePrefix("trace-thread-"); executor.initialize(); return new LazyTraceExecutor(this.beanFactory, executor); } }
(2)分别启动用户微服务和网关服务,在浏览器中输入链接http://localhost:10001/server-user/user/async/api
(3)查看用户微服务与网关服务的控制台日志,分别存在如下日志。
- 用户微服务
[server-user,157891cb90fddb65,0a278842776b1f01,true]执行异步任务开始... [server-user,157891cb90fddb65,0a278842776b1f01,true]异步任务执行结束... [server-user,157891cb90fddb65,1ba55fd3432b77ae,true]执行了异步任务...
- 网关服务
[server-gateway,157891cb90fddb65,157891cb90fddb65,true]
可以看到Sleuth为异步任务重新生成了Span。
综上说明:Sleuth支持对异步任务的链路追踪,在项目中使用@Async注解开启一个异步任务后,Sleuth会为异步任务重新生成一个Span。但是如果使用了自定义的异步任务线程池,则会导致Sleuth无法新创建一个Span,而是会重新生成Trace和Span。此时,需要使用Sleuth提供的LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建Span。
自定义链路过滤器
在Sleuth中存在链路过滤器,并且还支持自定义链路过滤器。
自定义链路过滤器概述
TracingFilter是Sleuth中负责处理请求和响应的组件,可以通过注册自定义的TracingFilter实例来实现一些扩展性的需求。
演示自定义链路过滤器
本案例演示通过过滤器验证只有HTTP或者HTTPS请求才能访问接口,并且在访问的链接不是静态文件时,将traceId放入HttpRequest中在服务端获取,并在响应结果中添加自定义Header,名称为SLEUTH-HEADER,值为traceId。
(1)在用户微服务shop-user中新建io.binghe.shop.user.filter
包,并创建MyGenericFilter类,继承org.springframework.web.filter.GenericFilterBean
类,代码如下所示。
/** * @author binghe * @version 1.0.0 * @description 链路过滤器 */ @Component @Order( Ordered.HIGHEST_PRECEDENCE + 6) public class MyGenericFilter extends GenericFilterBean{ private Pattern skipPattern = Pattern.compile(SleuthWebProperties.DEFAULT_SKIP_PATTERN); private final Tracer tracer; public MyGenericFilter(Tracer tracer){ this.tracer = tracer; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)){ throw new ServletException("只支持HTTP访问"); } Span currentSpan = this.tracer.currentSpan(); if (currentSpan == null) { chain.doFilter(request, response); return; } HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = ((HttpServletResponse) response); boolean skipFlag = skipPattern.matcher(httpServletRequest.getRequestURI()).matches(); if (!skipFlag){ String traceId = currentSpan.context().traceIdString(); httpServletRequest.setAttribute("traceId", traceId); httpServletResponse.addHeader("SLEUTH-HEADER", traceId); } chain.doFilter(httpServletRequest, httpServletResponse); } }
(2)在用户微服务shop-user的io.binghe.shop.user.controller.UserController
类中新建sleuthFilter()方法,在sleuthFilter()方法中获取并打印traceId,如下所示。
@GetMapping(value = "/sleuth/filter/api") public String sleuthFilter(HttpServletRequest request) { Object traceIdObj = request.getAttribute("traceId"); String traceId = traceIdObj == null ? "" : traceIdObj.toString(); log.info("获取到的traceId为: " + traceId); return "sleuthFilter"; }
(3)分别启动用户微服务和网关服务,在浏览器中输入http://localhost:10001/server-user/user/sleuth/filter/api
,如下所示。
查看用户微服务的控制台会输出如下信息。
获取到的traceId为: f63ae7702f6f4bba
查看浏览器的控制台,看到在响应的结果信息中新增了一个名称为SLEUTH-HEADER,值为f63ae7702f6f4bba的Header,如下所示。
说明使用Sleuth的过滤器可以处理请求和响应信息,并且可以在Sleuth的过滤器中获取到TraceID。
好了,今天我们就到儿吧,限于篇幅,文中并未给出完整的案例源代码,想要完整源代码的小伙伴可加入【冰河技术】知识星球获取源码。也可以加我微信:hacker_binghe,一起交流技术。