Sleuth 入门教程 - 基础篇

大纲

前言

在传统的 SOA 架构体系中,系统调用层级不多,调用关系也不复杂,一旦出现问题,根据异常信息可以很快定位到问题模块并进行排查。但是在微服务的世界里,实例数目成百上千,实例之间的调用关系几乎是网状结构,靠人力去监控和排查问题已经不太可能。这种情况下,一个完善的调用链路监控框架对于运维和开发来说,都是不可或缺的。

分布式链路追踪介绍

分布式链路追踪的概述

微服务架构下,服务按照不同的维度进行拆分,一次请求可能会涉及多个服务,并且有可能是由不同的团队开发,可能使用不同的编程语言来实现,有可能布在了几千台服务器上,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。这些工具就是 APM (Application Performance Management),其中最出名的是谷歌公开的论文提到的 Dapper。

  • Dapper 论文中对实现一个分布式链路追踪系统提出了如下需求:
    • 性能低损耗:分布式链路追踪系统对服务的性能损耗应尽可能做到可以忽略不计,尤其是对性能敏感的应用不能产生损耗。
    • 对应用透明:即要求尽可能用非侵入的方式来实现跟踪,尽可能做到业务代码的低侵入,对业务开发人员应该做到透明化。
    • 可伸缩性:是指不能随着微服务和集群规模的扩大而使分布式链路追踪系统瘫痪。
    • 跟踪数据可视化和迅速反馈:即要有可视化的监控界面,从跟踪数据收集、处理到结果的展现尽量做到快速,这样就可以对系统的异常状况。
    • 持续监控:即要求分布式链路追踪系统必须是 7X24 小时工作的,否则将难以定位到系统偶尔抖动的行为。

上图是谷歌论文中的一个基础案例,这里解释一下调用流程:A~E 分别表示五个服务,用户发起一次请求到 A,然后 A 分别发送 RPC 请求到 B 和 C,B 处理请求后返回,C 还要发起两个 RPC 请求到 D 和 E。

分布式链路追踪的作用

  • 故障定位

    • 当系统发生故障时,分布式链路追踪可以帮助快速确定故障发生的具体服务或组件。通过查看请求在不同服务中的延迟情况,可以找出导致性能瓶颈的具体服务。
  • 性能分析

    • 分布式链路追踪可以帮助识别请求在系统中的响应时间,开发者可以根据每个服务的延迟情况优化系统的性能。
  • 请求上下文跟踪

    • 在分布式系统中,服务之间的调用是异步和分散的,链路追踪可以帮助开发者全面了解每个请求的完整执行流程,甚至可以跟踪某些异步任务或消息队列中的任务执行情况。
  • 提升可观测性

    • 分布式链路追踪与日志、度量(Metrics)一起,构成了完整的系统可观测性工具链。这三者结合使用,可以为开发者提供从系统健康状况到具体请求细节的全面视图。

分布式链路追踪的挑战

尽管分布式链路追踪带来了许多好处,但它也面临一些挑战:

  • 性能开销

    • 追踪每个请求的所有跨服务操作需要记录大量的数据,可能会带来额外的性能开销,尤其是在高并发场景下。
  • 数据存储和分析

    • 由于分布式系统中存在大量的请求和调用,追踪系统需要有效地存储和分析这些数据,才能为开发者提供有用的信息。
  • 异构系统的支持

    • 分布式系统可能使用不同的技术栈和通信协议,链路追踪工具需要具备对这些异构系统的广泛支持。

分布式链路追踪的技术选型

技术选型总结

  • 如果是中小型项目,且已使用 Spring Cloud 周边生态的技术,建议采用 Sleuth + Zipkin 方案。
  • 如果是大型分布式系统,尤其是要求在高性能、高并发的场景下表现优异,建议采用 Pinpoint、SkyWalking 或者 OpenTelemetry。
  • 更多关于分布式链路追踪的技术选型介绍,可以参考 这里

Brave 的概述

Brave 是一个用于捕捉分布式系统之间调用信息的工具库,然后将这些信息以 Span 的形式发送给 Zipkin。

特别注意

  • 从 Sleuth 的 2.0.0 版本开始,Sleuth 不再自己存储上下文信息,而是使用 Brave 作为调用链工具库,并且遵循 Brave 的命名和标记惯例。
  • 如果想要沿用旧版本的使用方式,可以将 spring.sleuth.http.legacy.enabled 属性设置为 true

Zipkin 的概述

Zipkin 是一个基于 Google Dapper 论文设计的分布式链路追踪系统,由 Twitter 开发并贡献给开源社区,它可以收集系统的延时数据并提供展示界面,以便用户排查问题。Zipkin 的部署很简单,去官网下载所需的版本,然后直接 java -jar zipkin.jar 即可运行。Zipkin 启动后,默认监听的端口为 9411,在浏览器地址栏输入 http://localhost:9411 即访问 Zipkin 的控制台页面,如下图所示:

提示

  • Zipkin 还支持从源码启动和 Docker 镜像启动。
  • Zipkin 支持多种数据采集方式,包括从 Kafka、RabbitMQ、ActiveMQ 采集追踪数据。
  • Zipkin 支持各种数据持久化方式,包括 Cassandra、Elasticsearch、MySQL,默认是将数据存储在内存中。
  • 更多关于 Zipkin 安装与运行的介绍可以参考 这里

Sleuth 的概述

Sleuth 的简介

Spring Cloud Sleuth 是 Spring Cloud 的分布式链路追踪解决方案,它从 Dapper、Zipkin、HTrace 中借鉴了很多思路。Sleuth 对于大部分用户来说都是透明的,系统间的交互信息都能被自动采集。用户可以通过日志文件获取链路数据,也可以将追踪数据发送给远程服务进行统一收集展示。更详细的介绍可以参考:Spring Cloud Sleuth 官方文档Spring Cloud Sleuth GitHub 项目

  • Trace:由一系列 Span 组成的树状结构。简而言之,就是一次请求调用。
  • Span:基本工作单元。比如,发送一次 RPC 请求就是一个新的 Span。Span 通过一个 64 位的 ID 标识,还包含有描述、事件时间戳、标签、调用它的 Span(父 Span)的 ID、处理器 ID(一般为 IP 地址)。注意:第一个 Span 是 root span, 它的 ID 值和 Trace 的 ID 值一样。
  • Annotation:标注,用来描述事件的实时状态。事件有如下:
    • cs:Client Sent。客户端发起请求的时间,它表示一个 Span 的开始。
    • sr:Server Received。服务方接收到请求并开始处理的时间,它减去 cs 的时间就是网络延迟时间。
    • ss:Server Sent。它表示请求处理完成的时间,将响应数据返回给客户端。它减去 sr 的时间就是服务方处理业务的时间。
    • cr: Client Received。它表示客户端接收到服务方的返回值的时间,是当前 Span 结束的信号。它减去 cs 的时间就是一次请求的完整处理时间。

Sleuth 的原理

Sleuth 通过 Trace 定义一次业务调用链,根据它的信息,就能知道有多少个系统参与了该业务处理。而系统间的调用顺序和时间戳信息,则是通过 Span 来记录的。Trace 和 Span 的信息经过整合,就能知道该业务的完整调用链。在一次业务处理中,Trace 和 Span 的详细流转情况如下图所示:

各个 Span 之间的父子关系图如下

Sleuth 入门案例

1. 版本说明

在本案例中,使用各组件的版本如下所示:

组件版本
Spring Boot2.0.3
Spring CloudFinchley.RELEASE

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

3. 创建 Eureka Server 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-server

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

public static void main(String[] args){
SpringApplication.run(EurekaServerApplication.class, args);
}
}

添加 Eureka Server 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8090

eureka:
instance:
hostname: 127.0.0.1
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

4. 创建 Provider 工程

为了测试 Feign 的 Web 服务客户端的功能,必须要有一个服务提供者。创建 Provider 的 Maven 工程后,由于需要将服务注册到 Eureka Server,工程下的 pom.xml 文件需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Eureka Server:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args){
SpringApplication.run(ProviderApplication.class, args);
}
}

application.yml 文件中指定服务名称(provider-service、注册中心地址与端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9090

spring:
application:
name: provider-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

创建用于测试的 Controller 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/provider")
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;
}

}

5. 创建 Consumer 工程

创建 Consumer 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-openfeignspring-cloud-starter-sleuth。由于需要从 Eureka Server 获取服务列表,即作为 Eureka 客户端,还需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>

创建启动主类,添加注解 @EnableFeignClients@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

public static void main(String[] args){
SpringApplication.run(FeignApplication.class, args);
}
}

创建配置类,定义 RestTemplate 和 ExecutorService(线程池)的 Bean。特别注意,这里封装的是 Sleuth 提供的 TraceableExecutorService 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class ConsumerConfiguration {

@Autowired
private BeanFactory beanFactory;

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}

@Bean
public ExecutorService executorService() {
// 为了简单起见,注册固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
return new TraceableExecutorService(this.beanFactory, executorService);
}

}

创建服务接口类,用于通过 Feign 调用 Provider 服务:

1
2
3
4
5
6
7
@FeignClient(name = "provider-service")
public interface ProviderFeignService {

@RequestMapping("/provider/sayHello")
String sayHello(@RequestParam("name") String name);

}

创建用于测试的 Controller 类,使用多种方式调用 Provider 服务的那个自定义 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

private static final Logger log = LoggerFactory.getLogger(ConsumerController.class);

@Autowired
private RestTemplate restTemplate;

@Autowired
private ExecutorService executorService;

@Autowired
private ProviderFeignService providerFeignService;

/**
* 使用 Feign 来调用
*/
@GetMapping("/helloByFeign")
public String helloByFeign(String name) {
log.info("client sent. Feign 方式, 参数: {}", name);

String result = providerFeignService.sayHello(name);

log.info("client received. Feign 方式, 结果: {}", result);
return result;
}

/**
* 使用 RestTemplate 来调用
*/
@GetMapping("/helloByRestTemplate")
public String helloByRestTemplate(String name) {
log.info("client sent. RestTemplate 方式, 参数: {}", name);

String url = "http://provider-service/provider/sayHello?name=" + name;
String result = restTemplate.getForObject(url, String.class);

log.info("client received. RestTemplate 方式, 结果: {}", result);
return result;
}

/**
* 创建新线程 + Feign 来调用
*/
@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 = providerFeignService.sayHello(name);
return result;
});

String result = (String) future.get();
log.info("client received. 返回主线程, 结果: {}", result);
return result;
}

}

application.yml 文件中配置端口号、注册中心地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8080

spring:
application:
name: consumer-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

6. 测试案例代码

  • (1) 分别启动 Eureka Server、Provider Service 和 Consumer Service 三个应用。

  • (2) 在浏览器地址栏输入 http://localhost:8080/consumer/helloByFeign?name=rust,输出的日志信息如下:

1
2
INFO [consumer-service,f3502a77e0a4616f,f3502a77e0a4616f,false] 27662 --- [io-8080-exec-10] c.s.study.controller.ConsumerController  : client sent. Feign 方式, 参数: rust
INFO [consumer-service,f3502a77e0a4616f,f3502a77e0a4616f,false] 27662 --- [io-8080-exec-10] c.s.study.controller.ConsumerController : client received. Feign 方式, 结果: hello, rust
  • (3) 在浏览器地址栏输入 http://localhost:8080/consumer/helloByRestTemplate?name=rust,输出的日志信息如下:
1
2
INFO [consumer-service,923fef095d3c898d,923fef095d3c898d,false] 27662 --- [nio-8080-exec-1] c.s.study.controller.ConsumerController  : client sent. RestTemplate 方式, 参数: rust
INFO [consumer-service,923fef095d3c898d,923fef095d3c898d,false] 27662 --- [nio-8080-exec-1] c.s.study.controller.ConsumerController : client received. RestTemplate 方式, 结果: hello, rust
  • (4) 在浏览器地址栏输入 http://localhost:8080/consumer/helloByNewThread?name=rust,输出的日志信息如下:
1
2
3
INFO [consumer-service,19afd8053e4bd650,19afd8053e4bd650,false] 27662 --- [nio-8080-exec-4] c.s.study.controller.ConsumerController  : client sent. 子线程方式, 参数: rust
INFO [consumer-service,19afd8053e4bd650,b822a24370712e8b,false] 27662 --- [pool-1-thread-1] c.s.study.controller.ConsumerController : client sent. 进入子线程, 参数: rust
INFO [consumer-service,19afd8053e4bd650,19afd8053e4bd650,false] 27662 --- [nio-8080-exec-4] c.s.study.controller.ConsumerController : client received. 返回主线程, 结果: hello, rust

由上面的案例可以看出,引入了 spring-cloud-sleuth 之后,首先日志组件可以自动打印 Span 信息,然后 Span 信息不仅可以随着 Feign、RestTemplate 往服务端传递,还可以在父子线程之间传递(线程上下文共享传递)。值得一提的是,如果不引入 Sleuth,那么日志信息里面将会是 [passjava-question,,,],后面跟着三个空字符串,如下图所示:

7. 下载案例代码

  • 完整的案例代码可以从 这里 下载得到。

Sleuth 底层原理

Sleuth 对 Feign 的支持

Feign 提供了 feign.Client 接口以便开发者自定义远程调用功能。Sleuth 则使用了 TracingFeignClient 来实现 Feign 接口,然后在执行 HTTP 调用前,在 Header 中添加 Span 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
final class TracingFeignClient implements Client {

// 省略非核心代码

@Override public Response execute(Request request, Request.Options options) throws IOException {
Map<String, Collection<String>> headers = new HashMap<>(request.headers());
Span span = handleSend(headers, request, null);
if (log.isDebugEnabled()) {
log.debug("Handled send of " + span);
}
Response response = null;
Throwable error = null;
try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) {
return response = this.delegate.execute(modifiedRequest(request, headers), options);
}
catch (IOException | RuntimeException | Error e) {
error = e;
throw e;
}
finally {
handleReceive(span, response, error);
if (log.isDebugEnabled()) {
log.debug("Handled receive of " + span);
}
}
}

}

Sleuth 对 RestTemplate 的支持

在上面的入门例子中,演示了使用 RestTmeplate 跟服务端进行交互时,Span 信息也能从客户端传递给服务端。不过使用的 RestTemplate 并不是临时初始化的,而是在启动时注册成一个 Bean。那么临时初始化的 RestTemplate 行不行呢?答案是否定的。在官方文档中,有以下这么一段说明:

官方文档的翻译

你必须将 RestTemplate 注册成为一个 Bean,这样我们定义的拦截器(Interceptors)才能注入进去。如果你使用 new 的方式创建一个 RestTemplate 实例,我们的拦截器就失效了。

从官方文档中不难看出,Sleuth 是使用拦截器的方式对 RestTemplate 做了定制。这个拦截器是 brave.spring.web.TracingClientHttpRequestInterceptor,Sleuth 对它做了一层封装,为 LazyTracingClientHttpRequestInterceptor。RestTemplate 在执行 execute() 方法的时候,Request 会经过拦截器的处理,添加上 Span 信息。这就解释了为什么 new RestTemplate() 不行,把它注册成 Bean 就立即可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class TracingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

// 省略非核心代码

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
Span span = handler.handleSend(injector, request.getHeaders(), request);
ClientHttpResponse response = null;
Throwable error = null;
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
return response = execution.execute(request, body);
} catch (IOException | RuntimeException | Error e) {
error = e;
throw e;
} finally {
handler.handleReceive(response, error, span);
}
}

}

Sleuth 对多线程编程的支持

Sleuth 提供了 LazyTraceExecutor、TraceableExecutorService 和 TraceableScheduledExecutorService 三种多线程实现,它们都可以在创建新的任务时新建一个 Span。也就是说,如果想在多线程环境下使用 Sleuth,必须使用它提供的多线程实现,而不是自己去初始化线程池。

Sleuth 深入用法

TraceFilter

对于提供 HTTP 接口的服务方来说,它接收客户端 Span 信息的方式是使用 Filter(过滤器)。Sleuth 就是通过 Brave 的 TracingFilter 达到获取 Span 信息的目的。如果想对 Span 信息有一些自定义的修改,比如增加 tag 或者响应头信息,那么只需要注册一个自定义的 Filter(过滤器)就可以实现了。

特别注意

自定义 Filter 的优先级要比 TracingFilter 的优先级要低,不然你无法拿到 TracingFilter 处理之后的信息。

Baggage

Baggage 是存储在 Span 的上下文中的一组 Key/Value 键值对,跟 traceIdspanId 不同,它不是必选项。”Baggage” 翻译成中文是 “行李”,意思是可以将一些信息像行李一样挂在 Sleuth 中,由 Sleuth 帮我们沿着调用链路一路往下传递。毫无疑问,Baggage 是一个非常有用的功能,它相当于 Sleuth 暴露的一个功能接口,通过它就可以让自定义的数据跟着 Sleuth 一起往后接连传递。Baggage 的一个典型应用场景就是登录信息传递。

深入使用案例

本节将演示 TraceFilter 与 Baggage 的结合使用。在以下案例中,有 Consumer 和 Provider 两个核心模块,其中 Consumer 使用自定义的 Filter 来获取前端传来的 sessionId,并放入 Baggage 中,然后通过 Feign 调用的方式将 sessionId 传递给 Provider。

1. 版本说明

在本案例中,使用各组件的版本如下所示:

组件版本
Spring Boot2.0.3
Spring CloudFinchley.RELEASE

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

3. 创建 Eureka Server 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-server

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

public static void main(String[] args){
SpringApplication.run(EurekaServerApplication.class, args);
}
}

添加 Eureka Server 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8090

eureka:
instance:
hostname: 127.0.0.1
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

4. 创建 Provider 工程

创建 Provider 的 Maven 工程,配置工程里面的 pom.xml 文件,需要引入 spring-cloud-starter-openfeignspring-cloud-starter-sleuth。由于需要将服务注册到 Eureka Server,工程下的 pom.xml 文件需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Eureka Server:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args){
SpringApplication.run(ProviderApplication.class, args);
}
}

application.yml 文件中指定服务名称(provider-service、注册中心地址、端口号、Baggage 的 Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9090

spring:
application:
name: provider-service
sleuth:
baggage-keys: # 注意,Sleuth 2.0.0 之后,Baggage 的 key 必须在这里配置才能生效
- SessionId

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

提示

通过 spring.sleuth.baggage-keys 配置,可以定义要在分布式链路中传递的自定义键。这里配置了 SessionId,意味着这个 SessionId 键的值会作为 "行李"(Baggage)在整个分布式系统中传播。这使得每个服务都能访问并使用这个 SessionId 信息。

创建用于测试的 Controller 类,从 Baggage 里面获取下游服务传递过来的 SessionId

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/provider")
public class ProviderController {

@GetMapping("/sayHello")
public String sayHello(String name) {
System.out.println("==> SessionId: " + ExtraFieldPropagation.get("SessionId"));
return "hello, " + name + ", SessionId is " + ExtraFieldPropagation.get("SessionId");
}

}

5. 创建 Consumer 工程

创建 Consumer 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-openfeignspring-cloud-starter-sleuth。由于需要从 Eureka Server 获取服务列表,即作为 Eureka 客户端,还需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>

创建启动主类,添加注解 @EnableFeignClients@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

public static void main(String[] args){
SpringApplication.run(FeignApplication.class, args);
}
}

创建自定义的过滤器 SessionFilter,将自定义信息(如 SessionId)放入 Baggage 里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Order(TraceWebServletAutoConfiguration.TRACING_FILTER_ORDER + 1)
public class SessionFilter extends GenericFilterBean {

private final Pattern skipPattern = Pattern.compile(SleuthWebProperties.DEFAULT_SKIP_PATTERN);

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
throw new ServletException("Filter just supports HTTP requests");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
boolean skip = skipPattern.matcher(httpRequest.getRequestURI()).matches();
if (!skip) {
// 将 SessionId 放到 Baggage 中
System.out.println("==> SessionId: " + httpRequest.getSession().getId());
ExtraFieldPropagation.set("SessionId", httpRequest.getSession().getId());
}
filterChain.doFilter(request, response);
}

}

创建服务接口类,用于通过 Feign 调用 Provider 服务:

1
2
3
4
5
6
7
@FeignClient(name = "provider-service")
public interface ProviderFeignService {

@RequestMapping("/provider/sayHello")
String sayHello(@RequestParam("name") String name);

}

创建用于测试的 Controller 类,使用多种方式调用 Provider 服务的那个自定义 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

@Autowired
private ProviderFeignService providerFeignService;

@GetMapping("/hello")
public String hello(String name) {
return providerFeignService.sayHello(name);
}

}

application.yml 文件中配置端口号、注册中心地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8080

spring:
application:
name: consumer-service
sleuth:
baggage-keys: # 注意,Sleuth 2.0.0 之后,Baggage 的 key 必须在这里配置才能生效
- SessionId

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

提示

通过 spring.sleuth.baggage-keys 配置,可以定义要在分布式链路中传递的自定义键。这里配置了 SessionId,意味着这个 SessionId 键的值会作为 "行李"(Baggage)在整个分布式系统中传播。这使得每个服务都能访问并使用这个 SessionId 信息。

6. 测试案例代码

  • (1) 分别启动 Eureka Server、Provider Service 和 Consumer Service 三个应用。
  • (2) 在浏览器地址栏输入 http://localhost:8080/consumer/hello?name=rust
  • (3) 页面显示 “hello, rust, SessionId is xxxx” 即为请求成功,说明自定义的数据可以在整个分布式系统中传播。

7. 下载案例代码

  • 完整的案例代码可以从 这里 下载得到。