微服务实战系列之SpringCloud Alibaba:
在微服务实战系列之SpringCloud Alibaba学习(一)中,学习了搭建三大微服务并完成交互开发与测试。
在微服务实战系列之SpringCloud Alibaba学习(二)中,针对微服务会遇到的一些问题,使用了一些微服务组件来解决,实现了用户微服务、商品微服务和订单微服务之间的远程调用,并且实现了服务调用的负载均衡。也基于阿里开源的Sentinel实现了服务的限流与容错,并详细介绍了Sentinel的核心技术与配置规则 。如下:
Nacos
来实现服务的注册与发现Ribbon
来实现服务调用的负载均衡Sentinel
实现接口限流在微服务实战系列之SpringCloud Alibaba学习(三)中,简单介绍了服务网关,并对SpringCloud Gateway的核心架构进行了简要说明,也在项目中整合了SpringCloud Gateway网关实现了通过网关访问后端微服务,另外,也基于SpringCloud Gateway整合Sentinel实现了网关的限流功能。
SpringCloud Gateway
网关实现了通过网关访问后端微服务以及限流功能在微服务实战系列之SpringCloud Alibaba学习(四)中,正式学习分布式链路追踪技术与解决方案。
随着互联网业务快速扩展,企业的业务系统变得越来越复杂,不少企业开始向分布式、微服务方向发展,将原本的单体应用拆分成分布式、微服务。这也使得当客户端请求系统的接口时,原本在同一个系统内部的请求逻辑变成了需要在多个微服务之间流转的请求。
单体架构中可以使用AOP在调用具体的业务逻辑前后分别打印一下时间即可计算出整体的调用时间,使用 AOP捕获异常也可知道是哪里的调用导致的异常。
但是在分布式微服务场景下,使用AOP技术是无法追踪到各个微服务的调用情况
的,也就无法知道系统中处理一次请求的整体调用链路。
另外,在分布式与微服务场景下,我们需要解决如下问题:
上述问题就是分布式链路追踪技术要解决的问题。所谓的分布式链路追踪,就是将对分布式系统的一次请求转化成一个完整的调用链路。这个完整的调用链路从请求进入分布式系统的入口开始,直到整个请求返回为止。并在请求调用微服务的过程中,记录相应的调用日志,监控系统调用的性能,并且可以按照某种方式显示请求调用的情况。
在分布式链路追踪中,可以统计调用每个微服务的耗时,请求会经过哪些微服务的流转,每个微服务的运行状况等信息。
假定三个微服务调用的链路如下图所示:Service 1 调用 Service 2,Service 2 调用 Service 3 和 Service 4。
那么链路追踪会在每个服务调用的时候加上 Trace ID 和 Span ID。如下图所示:
小伙伴注意上面的颜色,相同颜色的代表是同一个 Span ID,说明是链路追踪中的一个节点。
通过 Parent ID 就可找到父节点,整个链路即可以进行跟踪追溯了。
目前,行业内比较成熟的分布式链路追踪技术解决方案如下所示。
在微服务实战系列之SpringCloud Alibaba学习
中,我们会使用 Sleuth+ZipKin
的方案实现分布式链路追踪。
Sleuth是SpringCloud中提供的一个分布式链路追踪组件,在设计上大量参考并借用了Google Dapper的设计。
Span简介
Span在Sleuth中代表一组基本的工作单元,当请求到达各个微服务时,Sleuth会通过一个唯一的标识,也就是SpanId来标记开始通过这个微服务,在当前微服务中执行的具体过程和执行结束。
此时,通过SpanId标记的开始时间戳和结束时间戳,就能够统计到当前Span的调用时间,也就是当前微服务的执行时间。另外,也可以用过Span获取到事件的名称,请求的信息等数据。
总结:远程调用和Span是一对一的关系,是分布式链路追踪中最基本的工作单元,每次发送一个远程调用服务就会产生一个 Span。Span Id 是一个 64 位的唯一 ID,通过计算 Span 的开始和结束时间,就可以统计每个服务调用所耗费的时间。
Trace简介
Trace的粒度比Span的粒度大,Trace主要是由具有一组相同的Trace ID的Span组成的,从请求进入分布式系统入口经过调用各个微服务直到返回的整个过程,都是一个Trace。
也就是说,当请求到达分布式系统的入口时,Sleuth会为请求创建一个唯一标识,这个唯一标识就是Trace Id,不管这个请求在分布式系统中如何流转,也不管这个请求在分布式系统中调用了多少个微服务,这个Trace Id都是不变的,直到整个请求返回。
总结:一个 Trace 可以对应多个 Span,Trace和Span是一对多的关系。Trace Id是一个64 位的唯一ID。Trace Id可以将进入分布式系统入口经过调用各个微服务,再到返回的整个过程的请求串联起来,内部每通过一次微服务时,都会生成一个新的SpanId。Trace串联了整个请求链路,而Span在请求链路中区分了每个微服务。
Annotation简介
Annotation记录了一段时间内的事件,内部使用的重要注解如下所示。
总结:链路追踪系统内部定义了少量核心注解,用来定义一个请求的开始和结束,通过这些注解,我们可以计算出请求的每个阶段的时间。需要注解的是,这里说的请求,是在系统内部流转的请求,而不是从浏览器、APP、H5、小程序等发出的请求。
Sleuth提供了分布式链路追踪能力,如果需要使用Sleuth的链路追踪功能,需要在项目中集成Sleuth。
最简使用
(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
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
port: 7777
dashboard: 127.0.0.1:8888
web-context-unify: false
eager: true
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"
allowCredentials: true
allowedHeaders: "*"
discovery:
locator:
enabled: true
route-id-prefix: gateway-
(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,是否将结果输出到第三方平台]
具体如下所示。
[server-user,03fef3d312450828,76b298dba54ec579,true]
[server-product,03fef3d312450828,41ac8836d2df4eec,true]
[server-product,03fef3d312450828,6b7b3662d63372bf,true]
[server-order,03fef3d312450828,cbd935d57cae84f9,true]
[server-gateway,03fef3d312450828,03fef3d312450828,true]
可以看到,每个服务都打印出了链路追踪的日志信息,说明引入Sleuth的依赖后,就可以在命令行查看链路追踪情况。
Sleuth支持抽样采集数据。尤其是在高并发场景下,如果采集所有的数据,那么采集的数据量就太大了,非常耗费系统的性能。通常的做法是可以减少一部分数据量,特别是对于采用Http方式去发送采集数据,能够提升很大的性能。
Sleuth可以采用如下方式配置抽样采集数据。
spring:
sleuth:
sampler:
probability: 1.0
Sleuth支持对异步任务的链路追踪,在项目中使用@Async注解开启一个异步任务后,Sleuth会为异步任务重新生成一个Span。但是如果使用了自定义的异步任务线程池,则会导致Sleuth无法新创建一个Span,而是会重新生成Trace和Span。此时,需要使用Sleuth提供的LazyTraceExecutor类来包装下异步任务线程池,才能在异步任务调用链路中重新创建Span。
在服务中开启异步线程池任务,需要使用@EnableAsync。所以,在演示示例前,先在用户微服务shop-user的io.binghe.shop.UserStarter启动类上添加@EnableAsync注解,如下所示。
/**
* @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);
}
}
(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类,用来自定义异步任务线程池,代码如下所示。
/**
* @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类的代码如下所示。
/**
* @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实现链路追踪时,我们是通过查看日志的情况来了解系统调用的链路情况,这并不是一种很好的解决方案,如果系统所包含的微服务越来越多,通过查看日志的方式来分析系统的调用是非常复杂的,在实际项目中根本不可行。此时,我们可以将Sleuth和ZipKin进行整合,利用ZipKin将日志进行聚合,将链路日志进行可视化展示,并支持全文检索
。
Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper论文实现,可以收集微服务运行过程中的实时链路数据,并进行展示。
Zipkin是一种分布式链路跟踪系统,能够收集微服务运行过程中的实时调用链路信息,并能够将这些调用链路信息展示到Web界面上供开发人员分析,开发人员能够从ZipKin中分析出调用链路中的性能瓶颈,识别出存在问题的应用程序,进而定位问题和解决问题。
其中,ZipKin核心组件的功能如下所示。
Zipkin在总体上会分为两个端,一个是Zipkin服务端,一个是Zipkin客户端,客户端主要是配置在微服务应用中,收集微服务中的调用链路信息,将数据发送给ZipKin服务端。
Zipkin总体上分为服务端和客户端,我们需要下载并启动ZipKin服务端的Jar包,在微服务中集成ZipKin的客户端。
(1)下载ZipKin服务端Jar文件,可以直接在浏览器中输入如下链接进行下载。
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec
如果大家使用的是Linux操作系统,也可以在命令行输入如下命令进行下载。
wget https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec
这里,我通过浏览器下载的ZipKin服务端Jar文件为:zipkin-server-2.12.9-exec.jar。
(2)在命令行输入如下命令启动ZipKin服务端。
java -jar zipkin-server-2.12.9-exec.jar
(3)由于ZipKin服务端启动时,默认监听的端口号为9411,所以,在浏览器中输入http://localhost:9411链接就可以打开ZipKin的界面,如下所示。
在浏览器中输入http://localhost:9411链接能够打开上述页面就说明ZipKin服务端已经准备好啦。
(1)在每个微服务(用户微服务shop-user,商品微服务shop-product,订单微服务shop-order,网关服务shop-gateway)中添加ZipKin依赖,如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)在网关服务shop-gateway的application.yml文件中添加如下配置。
spring:
sleuth:
sampler:
probability: 1.0
zipkin:
base-url: http://127.0.0.1:9411
discovery-client-enabled: false
其中各配置的说明如下所示。
spring.sleuth.sampler.probability:表示Sleuth的采样百分比。
spring.zipkin.base-url:ZipKin服务端的地址。
spring.zipkin.discovery-client-enabled:配置成false,使Nacos将其当成一个URL,不要按服务名处理。
(3)分别启动用户微服务,商品微服务,订单微服务和服务网关,在浏览器中访问链接http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。
我们实现了在项目中集成ZipKin,但是此时我们集成ZipKin后,ZipKin中的数据是保存在系统内存中的,如果我们重启了ZipKin,则保存在系统内存中的数据就会丢失,那我如何避免数据丢失呢?ZipKin支持将数据进行持久化来防止数据丢失,可以将数据保存到ElasticSearch、Cassandra或者MySQL中。这里,我们重点介绍下如何将数据保存到MySQL和ElasticSearch中。
(1)将Zipkin数据持久化到MySQL,我们需要知道MySQL的数据表结构,好在ZipKin提供了MySQL脚本,小伙伴们可以在链接:https://github.com/openzipkin/zipkin/tree/master/zipkin-storage里面下载。
当然,我将下载后的MySQL脚本放到了网关服务shop-gateway的resources目录下的scripts目录下。
(2)在MySQL数据库中新建zipkin数据库,如下所示。
create database if not exists zipkin;
(3)在新建的数据库zipkin中运行mysql.sql脚本,运行脚本后的效果如下所示。
可以看到,在zipkin数据库中新建了zipkin_annotations、zipkin_dependencies和zipkin_spans三张数据表。
(4)启动ZipKin时指定MySQL数据源,如下所示。
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root
(5)启动ZipKin后,在浏览器中访问链接http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。
(6)查看zipkin数据库中的数据,发现zipkin_annotations数据表与zipkin_spans数据表已经存在系统的调用链路数据。
(1)到ElasticSearch官网下载ElasticSearch,链接为:
https://www.elastic.co/cn/downloads/elasticsearch。
这里下载的安装包是:elasticsearch-8.2.0-windows-x86_64.zip。
(2)解压elasticsearch-8.2.0-windows-x86_64.zip,在解压后的bin目录下找到elasticsearch.bat脚本,双击运行ElasticSearch。
(3)启动ZipKin服务端时,指定ElasticSearch,如下所示。
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=elasticsearch --ESHOST=localhost:9200
(4)启动ZipKin服务端后,在浏览器中访问链接http://localhost:10001/server-order/order/submit_order?userId=1001&productId=1001&count=1,如下所示。
ZipKin就会将请求的链路信息保存到ElasticSearch中进行持久化。
参考:
本文是参考冰河技术得SpringCloud Alibaba实战专栏内容整理的,只是方便学习使用(侵权删),具体想要了解或想要获取源码的,请参考下面冰河技术的链接,关注公众号获取源码。
SpringCloud Alibaba
冰河技术:《SpringCloud Alibaba实战》