目录
随着互联网的不断发展,企业的业务系统变得越来越复杂,原本单一的单体应用系统已经无法满足企业业务发展的需要。于是,很多企业开始了对项目的分布式与微服务改造,新项目也在开始的时候就会采用分布式与微服务的架构模式。
1、分布式链路追踪
1.1、概述
随着互联网业务快速扩展,企业的业务系统变得越来越复杂,不少企业开始向分布式、微服务方向发
展,将原本的单体应用拆分成分布式、微服务。这也使得当客户端请求系统的接口时,原本在同一个系统内部的请求逻辑变成了需要在多个微服务之间流转的请求。
单体架构中可以使用 AOP 在调用具体的业务逻辑前后分别打印一下时间即可计算出整体的调用时间,使用 AOP 捕获异常也可知道是哪里的调用导致的异常。
但是在分布式微服务场景下,使用 AOP 技术是无法追踪到各个微服务的调用情况的,也就无法知道系统中处理一次请求的整体调用链路。
另外,在分布式与微服务场景下,我们需要解决如下问题:
- 如何快速发现并定位到分布式系统中的问题。
- 如何尽可能精确的判断故障对系统的影响范围与影响程度。
- 如何尽可能精确的梳理出服务之间的依赖关系,并判断出服务之间的依赖关系是否合理。
- 如何尽可能精确的分析整个系统调用链路的性能与瓶颈点。
- 如何尽可能精确的分析系统的存储瓶颈与容量规划。
- 如何实时观测系统的整体调用链路情况。
上述问题就是分布式链路追踪技术要解决的问题。所谓的分布式链路追踪,就是将对分布式系统的一次请求转化成一个完整的调用链路。这个完整的调用链路从请求进入分布式系统的入口开始,直到整个请求返回为止。并在请求调用微服务的过程中,记录相应的调用日志,监控系统调用的性能,并且可以按照某种方式显示请求调用的情况。
1.2、核心原理
假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。
那么链路追踪会在每个服务调用的时候加上 Trace ID 和 Span ID。如下图所示:
- 用户端调用 Service 1,生成一个 Request,Trace ID 和 Span ID 为空,那个时候请求还
没有到 Service 1。 - 请求到达 Service 1,记录了 Trace ID = X,Span ID 等于 A。
- Service 1 发送请求给 Service 2,Span ID 等于 B,被称作 Client Sent,即用户端发送一
个请求。 - 请求到达 Service 2,Span ID 等于 B,Trace ID 不会改变,被称作 Server Received,即
服务端取得请求并准备开始解决它。 - Service 2 开始解决这个请求,解决完之后,Trace ID 不变,Span ID = C。
- Service 2 开始发送这个请求给 Service 3,Trace ID 不变,Span ID = D,被称作 Client
Sent,即用户端发送一个请求。 - Service 3 接收到这个请求,Span ID = D,被称作 Server Received。
- Service 3 开始解决这个请求,解决完之后,Span ID = E。
- Service 3 开始发送响应给 Service 2,Span ID = D,被称作 Server Sent,即服务端发送
响应。 - Service 2 收到 Service 3 的响应,Span ID = D,被称作 Client Received,即用户端接收
响应。 - Service 2 开始返回 响应给 Service 1,Span ID = B,和第三步的 Span ID 相同,被称
作 Client Received,即用户端接收响应。 - Service 1 解决完响应,Span ID = A,和第二步的 Span ID 相同。
- Service 1 开始向用户端返回响应,Span ID = A、
- Service 3 向 Service 4 发送请求和 Service 3 相似,对应的 Span ID 是 F 和 G。可以参照上面前面的第六步到第十步
把以上的相同颜色的步骤简化为下面的链路追踪图:
- 第一个节点:Span ID = A,Parent ID = null,Service 1 接收到请求。
- 第二个节点:Span ID = B,Parent ID= A,Service 1 发送请求到 Service 2 返回响应给Service 1
的过程。 - 第三个节点:Span ID = C,Parent ID= B,Service 2 的 中间解决过程。
- 第四个节点:Span ID = D,Parent ID= C,Service 2 发送请求到 Service 3 返回响应给Service 2
的过程。 - 第五个节点:Span ID = E,Parent ID= D,Service 3 的中间解决过程。
- 第六个节点:Span ID = F,Parent ID= C,Service 3 发送请求到 Service 4 返回响应给 Service 3 的过程。
- 第七个节点:Span ID = G,Parent ID= F,Service 4 的中间解决过程
1.3、解决方案
目前,行业内比较成熟的分布式链路追踪技术解决方案如下所示:
技术 | 说明 |
---|---|
Cat | 由大众点评开源,基于 Java 开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高。风险较大 |
ZipKin | 由 Twitter 公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。结合 spring-cloud-sleuth 使用较为简单, 集成方便, 但是功能较简单 |
Pinpoint | Pinpoint 是一款开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI 功能强大,接入端无代码侵入 |
Skywalking | SkyWalking 是国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI功能较强,接入端无代码侵入 |
Sleuth | Sleuth 是 SpringCloud 中的一个组件,为 Spring Cloud 实现了分布式跟踪解决方案 |
2、集成 Sleuth 实现链路追踪
2.1、Sleuth 概述
Sleuth 是 SpringCloud 中提供的一个分布式链路追踪组件,在设计上大量参考并借用了 Google Dapper 的设计
2.1.1、Span 简介
Span 在 Sleuth 中代表一组基本的工作单元,当请求到达各个微服务时,Sleuth 会通过一个唯一的标识,也就是 SpanId 来标记开始通过这个微服务,在当前微服务中执行的具体过程和执行结束,此时,通过 SpanId 标记的开始时间戳和结束时间戳,就能够统计到当前 Span 的调用时间,也就是当前微服务的执行时间。另外,也可以用过 Span 获取到事件的名称,请求的信息等数据。
2.1.2、Trace 简介
Trace 的粒度比 Span 的粒度大,Trace 主要是由具有一组相同的 Trace ID 的 Span 组成的,从请求进入分布式系统入口经过调用各个微服务直到返回的整个过程,都是一个 Trace。也就是说,当请求到达分布式系统的入口时,Sleuth 会为请求创建一个唯一标识,这个唯一标识就是 Trace Id,不管这个请求在分布式系统中如何流转,也不管这个请求在分布式系统中调用了多少个微服务,这个 Trace Id 都是不变的,直到整个请求返回。
2.1.3、Annotation 简介
Annotation 记录了一段时间内的事件,内部使用的重要注解如下所示:
- cs(Client Send)客户端发出请求,标记整个请求的开始时间。
- sr(Server Received)服务端收到请求开始进行处理,通过 sr 与 cs 可以计算网络的延迟时间,例如:sr- cs = 网络延迟(服务调用的时间)。
- ss(Server Send)服务端处理完毕准备将结果返回给客户端, 通过 ss 与 sr 可以计算服务器上的请求处理时间,例如:ss - sr = 服务器上的请求处理时间。
- cr(Client Reveived)客户端收到服务端的响应,请求结束。通过 cr 与 cs 可以计算请求的总时间,例如:cr - cs = 请求的总时间
2.2、集成 Sleuth
Sleuth 提供了分布式链路追踪能力,如果需要使用 Sleuth 的链路追踪功能,需要在项目中集成 Sleuth。
2.2.1、最简使用
1、在每个微服务(用户微服务 shop-user
、商品微服务 shop-product
、订单微服务 shop-order
、网关服务 shop-gateway
)下的 pom.xml
文件中添加如下 Sleuth
的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2、将项目的 application.yml
文件备份成 application-pre-filter.yml
,并将 application.yml
文件的内容
替换为 application-sentinel.yml
文件的内容,这一步是为了让整个项目集成 Sentinel
、SpringCloud Gateway
和 Nacos
。application.yml
替换后的文件内容如下所示:
server:
port: 10002
spring:
application:
name: server-gateway
main:
allow-bean-definition-overriding: true # 解决重复注册的问题
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 7777
dashboard: 127.0.0.1:8888
web-context-unify: false # 开启 sentinel 的 web 监控
eager: true # 启动时加载所有规则到 Sentinel
gateway:
globalcors: # 配置跨域
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowCredentials: true
allowedHeaders: "*"
discovery:
locator:
enabled: true # 开启服务发现
route-id-prefix: gateway- # 路由 id 前缀
3、分别启动 Nacos
、Sentinel
、用户微服务 shop-user
,商品微服务 shop-product
,订单微服务
shop-order
和网关服务 shop-gateway
,在浏览器中输入链接 localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1
,如下所示:
4、分别查看用户微服务 shop-user
,商品微服务 shop-product
,订单微服务 shop-order
和网关服务shop-gateway
的控制台输出【调用到的接口需要日志打印】,每个服务的控制台都输出了如下格式所示的信息:
[微服务名称,TraceID,SpanID,是否将结果输出到第三方平台]
具体如下所示:
- 用户微服务
shop-user
[server-user,878079143b6dc50d,ff6532c549a74886,true]
- 商品微服务
shop-product
[server-product,878079143b6dc50d,55f0ab3d9060c096,true]
[server-product,878079143b6dc50d,fc2b203d909deed5,true]
- 订单微服务
shop-order
[server-order,878079143b6dc50d,24b24b94f287e162,true]
- 网关服务
shop-gateway
[server-gateway,878079143b6dc50d,878079143b6dc50d,true]
每个服务都打印出了链路追踪的日志信息,说明引入 Sleuth 的依赖后,就可以在命令行查看链路追踪情况
2.2.2、抽样采集数据
Sleuth 支持抽样采集数据。尤其是在高并发场景下,如果采集所有的数据,那么采集的数据量就太大
了,非常耗费系统的性能。通常的做法是可以减少一部分数据量,特别是对于采用 HTTP 方式去发送采集数据,能够提升很大的性能。
Sleuth 可以采用如下方式配置抽样采集数据:
spring:
sleuth:
sampler:
probability: 1.0
2.2.3、追踪自定义线程池
Sleuth 支持对异步任务的链路追踪,在项目中使用 @Async
注解开启一个异步任务后,Sleuth 会为异步任务重新生成一个 Span。但是如果使用了自定义的异步任务线程池,则会导致 Sleuth 无法新创建一个 Span,而是会重新生成 Trace 和 Span。此时,需要使用 Sleuth 提供的 LazyTraceExecutor
类来包装下异步任务线程池,才能在异步任务调用链路中重新创建 Span。
在服务中开启异步线程池任务,需要使用 @EnableAsync
。所以,先在用户微服务 shop-user
的启动类上添加 @EnableAsync
注解,如下所示:
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = {"com.zzc.user.mapper"})
@EnableDiscoveryClient
@EnableAsync // 添加异步
public class ShopUserApplication {
public static void main(String[] args) {
SpringApplication.run(ShopUserApplication.class, args);
}
}
2.2.3.1、演示使用 @Async 注解开启任务
1、在用户微服务 shop-user
的 com.zzc.user.service.UserService
接口中定义一个asyncMethod()
方法,如下所示:
void asyncMethod();
2、在用户微服务 shop-user
的 com.zzc.user.service.impl.UserServiceImpl
类中实现asyncMethod()
方法,并在 asyncMethod()
方法上添加 @Async
注解,如下所示:
@Async
@Override
public void asyncMethod() {
log.info("执行了异步任务...");
}
3、在用户微服务 shop-user
的com.zzc.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、查看用户微服务与网关服务的控制台日志,分别存在如下日志:
- 用户微服务
INFO [server-user,7665700fe4705c45,d690abaa42ae39a2,true] 9344 --- [nio-8060-exec-2] UserController : 执行异步任务开始...
INFO [server-user,7665700fe4705c45,d690abaa42ae39a2,true] 9344 --- [nio-8060-exec-2] UserController : 异步任务执行结束...
INFO [server-user,7665700fe4705c45,711fd801ff1b9be4,true] 9344 --- [ task-2] UserServiceImpl : 执行了异步任务...
- 网关服务
INFO [server-gateway,7665700fe4705c45,7665700fe4705c45,true] 11072 --- [ctor-http-nio-4] GlobalGatewayLogFilter : 访问接口时长: 7ms
可以看到 Sleuth 为异步任务重新生成了 Span
2.2.3.2、演示自定义任务线程池
在演示使用 @Async
注解开启任务的基础上继续演示自定义任务线程池,验证 Sleuth 是否为自定义线程池新创建了 Span。
1、在用户微服务 shop-user
中新建 com.zzc.user.config
包,在包下创建 ThreadPoolTaskExecutorConfig
类,继承 org.springframework.scheduling.annotation.AsyncConfigurerSupport
类,用来自定义异步任务线程池,代码如下所示:
@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 的形式启动用户微服务和网关服务,并在 com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法中打上断点,如下所示:
可以看到,项目启动后并没有进入 com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法,说明项目启动时,并不会创建异步任务线程池
3、在浏览器中输入链接
http://localhost:10001/server-user/user/async/api
,此时可以看到程序已经执行到com.zzc.user.config.ThreadPoolTaskExecutorConfig#getAsyncExecutor()
方法的断点位置:
说明异步任务线程池是在调用了异步任务的时候创建的
4、放过断点,查看用户微服务与网关服务的控制台日志,分别存在如下日志:
- 用户微服务
[server-user,660a8e5e012f2ce2,c24ac93bd34daf5f,true]: 执行异步任务开始...
[server-user,660a8e5e012f2ce2,c24ac93bd34daf5f,true]: 异步任务执行结束...
[server-user,411ad130ab8b4166,411ad130ab8b4166,true]: 执行了异步任务...
- 网关服务
[server-gateway,660a8e5e012f2ce2,660a8e5e012f2ce2,true]
可以看到,使用自定义异步任务线程池时,在用户微服务中在执行异步任务时,重新生成了 Trace 和 Span
2.2.3.3、演示包装自定义线程池
在自定义任务线程池的基础上继续演示包装自定义线程池,验证 Sleuth 是否为包装后的自定义线程池新创建了 Span
1、在用户微服务 shop-user
的 com.zzc.user.config.ThreadPoolTaskExecutorConfig
类中注入 BeanFactory
,并在 getAsyncExecutor()
方法中使用 org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor()
来包装返回的异
步任务线程池,修改后的 com.zzc.user.config.ThreadPoolTaskExecutorConfig
类的代码如下所示:
@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,2d2955917de0be06,d0e72709a0017f7f,true]: 执行异步任务开始...
[server-user,2d2955917de0be06,d0e72709a0017f7f,true]: 异步任务执行结束...
[server-user,2d2955917de0be06,3a7c998785cfe446,true]: 执行了异步任务...
- 网关服务
[server-gateway,2d2955917de0be06,2d2955917de0be06,true]
可以看到 Sleuth 为异步任务重新生成了 Span
2.2.3.4、总结
Sleuth 支持对异步任务的链路追踪,在项目中使用 @Async
注解开启一个异步任务后,Sleuth 会为异步任务重新生成一个 Span。但是如果使用了自定义的异步任务线程池,则会导致 Sleuth 无法新创建一个 Span,而是会重新生成 Trace 和 Span。此时,需要使用 Sleuth 提供的LazyTraceExecutor
类来包装下异步任务线程池,才能在异步任务调用链路中重新创建 Span
2.2.4、自定义链路过滤器
在 Sleuth 中存在链路过滤器,并且还支持自定义链路过滤器
2.2.4.1、自定义链路过滤器概述
TracingFilter
是 Sleuth 中负责处理请求和响应的组件,可以通过注册自定义的 TracingFilter
实例来实现一些扩展性的需求
2.2.4.2、演示自定义链路过滤器
1、在用户微服务 shop-user
中新建 com.zzc.user.filter
包,并创建 MyGenericFilter
类,继承 org.springframework.web.filter.GenericFilterBean
类,代码如下所示:
@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
的 com.zzc.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
,查看用户微服务的控制台会输出如下信息:
[server-user,37018cdeb6cc7b5b,f7f441bfe422e2ee,true]: 获取到的traceId为: 37018cdeb6cc7b5b
查看浏览器的控制台,看到在响应的结果信息中新增了一个名称为 SLEUTH-HEADER
,如下所示
说明使用 Sleuth 的过滤器可以处理请求和响应信息,并且可以在 Sleuth 的过滤器中获取到 TraceID