0. 背景
传统的SOA架构体系中,系统调用层级不多,调用关系也不是很复杂,一旦出现问题,根据异常信息可以很快定位到问题模块并进行排查。
但是在微服务的世界里,实例数目成百上千,实例之间的调用关系呈网状结构,这个时候再靠人力去监控和排查问题几乎已经不太可能。
在这种情况下,一个完善的调用链路监控框架对于开发和运维来说,都是不可或缺的。
1. Spring Cloud Sleuth简介和原理
Spring Cloud Sleuth是Spring Cloud的分布式调用链解决方案,它从Google Dapper,Zipkin,HTrace中借鉴了很多思路。
Spring Cloud Sleuth常用术语:
-
Span(跨度)
基本工作单元。
比如发送一次RPC请求就是一个新的Span。
Span通过一个64位的ID标识,还包含有描述,事件时间戳,标签,调用它的Span的ID,处理器ID(一般为IP地址)。 -
Trace(跟踪)
一系列Span组成的树状结构。
简而言之,就是一次业务调用。 -
Annotation(标注)
用来描述事件的实时状态。
事件有如下几种状态:
cs: Client Sent 客户端发起请求,它表示一个Span的开始。
sr: Server Received 服务方接收到请求并开始处理,sr - cs的时间就是网络延迟。
ss: Server Sent 它表示服务方请求处理完成,并将响应数据返回给客户端。 ss - sr的时间就是服务方处理时间。
cr: Client Received 它表示客户端收到服务方的返回值,是当前Span结束的信号。cr - cs的时间就是一次请求的完整处理时间。
Sleuth原理介绍:
Sleuth通过Trace定义一次业务调用链。通过它的信息,我们能知道有多少个系统参与了该业务处理。
而系统间的调用顺序和时间戳信息,则是通过Span来记录的。
Trace和Span的信息经过整合,就能知道该业务的完整调用链。
2. 实战
下面我们通过一个简单的案例来演示Sleuth是如何使用的。
我们先新建一个pom项目simple-sleuth-demo
给它添加如下maven配置:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>14</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- 引入spring cloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>
然后我们先创建一个子模块sleuth-consumer,为jar类型
再创建一个子模块sleuth-provider,为jar类型
项目结构如下:
项目结构图
接下去便构建consumer相关的代码
application.properties
server.port=8082
spring.application.name=sleuth-consumer
SleuthConsumerApplication.java
@SpringBootApplication
@EnableFeignClients
public class SleuthConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthConsumerApplication.class, args);
}
}
由于我们一会儿要用到Feign,因此添加了@EnableFeignClients注解。
接着,我们新建一个config包,在包下面新建一个配置文件类
ConsumerConfiguration.java
@Configuration
public class ConsumerConfiguration {
@Autowired
private BeanFactory beanFactory;
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public ExecutorService executorService() {
// 简单起见, 我们注册固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
return new TraceableExecutorService(this.beanFactory, executorService);
}
}
上面代码可以看到,我们私用了BeanFactory和一个固定大小的线程池。
接着,我们新建一个service包,并在该包下面定义Feign接口
@FeignClient(name = "sleuth-provider", url = "localhost:8083")
public interface HelloService {
@RequestMapping("/sayHello")
String sayHello(@RequestParam("name") String name);
}
然后,我们再新建一个controller包,并在该包下添加controller
ConsumerController.java
@RestController
public class ConsumerController {
private static final Logger log = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private HelloService helloService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private ExecutorService executorService;
@GetMapping("/helloByFeign")
public String helloByFeign(String name) {
log.info("client sent. Feign 方式, 参数: {}", name);
String result = helloService.sayHello(name);
log.info("client received. Feign 方式, 结果: {}", result);
return result;
}
@GetMapping("/helloByRestTemplate")
public String helloByRestTemplate(String name) {
log.info("client sent. RestTemplate方式, 参数: {}", name);
String url = "http://localhost:8083/sayHello?name=" + name;
String result = restTemplate.getForObject(url, String.class);
log.info("client received. RestTemplate方式, 结果: {}", result);
return result;
}
@GetMapping("/helloByNewThread")
public String hello(String name) throws ExecutionException, InterruptedException {
log.info("client sent. 子线程方式, 参数: {}", name);
Future future = executorService.submit(() -> {
log.info("client sent. 进入子线程, 参数: {}", name);
String result = helloService.sayHello(name);
return result;
});
String result = (String) future.get();
log.info("client received. 返回主线程, 结果: {}", result);
return result;
}
}
#########################################################
sleuth-provider项目代码
application.properties
server.port=8083
spring.application.name=sleuth-provider
SleuthProviderApplication.java
@SpringBootApplication
public class SleuthProviderApplication {
public static void main(String[] args) {
SpringApplication.run(SleuthProviderApplication.class, args);
}
}
ProviderController.java
@RestController
public class ProviderController {
private static final Logger log = LoggerFactory.getLogger(ProviderController.class);
@GetMapping("/sayHello")
public String hello(String name) {
log.info("server received. 参数: {}", name);
String result = "hello, " + name;
log.info("server sent. 结果: {}", result);
return result;
}
}
#########################################################
- 启动sleuth-provider
- 启动sleuth-consumer
- 浏览器输入: http://localhost:8082/helloByFeign?name=kg
consumer端显示:
consumer log
provider端显示:
provider log
可以看到,引入了spring-cloud-sleuth之后,我们的日志组件可以自动打印Span信息。
好了,一个基本Spring Cloud Sleuth的实例就完成了。
网友评论