MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Spring Cloud Sleuth 实现分布式链路追踪

2021-04-234.6k 阅读

微服务架构下的链路追踪挑战

在微服务架构中,一个看似简单的业务请求往往需要经过多个微服务的协同处理。以一个常见的电商场景为例,当用户发起一个下单请求时,该请求可能首先到达订单服务,订单服务需要调用库存服务检查商品库存,调用支付服务处理支付流程,还可能调用物流服务规划配送。在这个过程中,一个请求会在多个服务之间穿梭,每个服务又可能包含多个组件和数据库交互。

这种复杂的交互模式带来了链路追踪的挑战。当系统出现问题,比如响应时间过长或者出现错误时,开发人员很难快速定位问题出在哪一个微服务、哪一个组件甚至哪一次数据库操作上。传统的单体架构中,日志和监控相对集中,问题定位较为容易。但在微服务架构下,各个服务独立部署、独立运行,日志分散在不同的服务器上,监控数据也分布在各个服务的监控系统中,使得问题排查变得异常困难。

例如,假设用户反馈下单操作响应时间过长。开发人员可能首先会查看订单服务的日志,但发现订单服务处理请求的时间并不长。然后去查看库存服务,发现库存服务在查询库存时花费了大量时间。但进一步深入,可能发现是库存服务依赖的数据库出现了性能问题。这个过程中,如果没有有效的链路追踪手段,开发人员需要逐个排查相关服务及其依赖,效率极低。

Spring Cloud Sleuth 简介

Spring Cloud Sleuth 是 Spring Cloud 生态系统中的一个重要组件,专门用于解决微服务架构下的分布式链路追踪问题。它为基于 Spring Boot 的微服务提供了分布式追踪的能力,能够轻松地将追踪信息传播到整个分布式系统中。

Spring Cloud Sleuth 基于 Google 的 Dapper 论文理念实现,它的核心概念包括 Trace(追踪)、Span(跨度)和 Annotation(注解)。

Trace(追踪)

Trace 代表了一个完整的业务请求链路。从用户发起请求开始,到最终响应返回,整个过程就是一个 Trace。在上述电商下单的例子中,从用户发起下单请求到订单确认响应,这一系列微服务调用的全过程就是一个 Trace。每个 Trace 都有一个唯一的 Trace ID,通过这个 ID 可以将整个请求链路中的所有 Span 关联起来。

Span(跨度)

Span 表示在 Trace 中的一个独立的工作单元。每个微服务的一次调用就是一个 Span。比如订单服务调用库存服务,这就是一个 Span。每个 Span 都有自己的唯一 ID,同时它还包含了 Trace ID,用于表明它属于哪个 Trace。Span 还记录了该工作单元的开始时间和结束时间,通过计算两者的差值,可以得到该 Span 的执行时间。

Annotation(注解)

Annotation 用于在 Span 中标记关键事件,常见的 Annotation 有以下几种:

  • CS(Client Send):客户端发起请求,标志着 Span 的开始。
  • SR(Server Receive):服务端接收到请求,意味着客户端的 CS 与服务端的 SR 之间存在网络传输时间。
  • SS(Server Send):服务端处理完请求并准备发送响应,其与 SR 之间的时间差代表了服务端的处理时间。
  • CR(Client Receive):客户端接收到服务端的响应,标志着 Span 的结束,其与 CS 之间的时间差就是整个请求的处理时间。

Spring Cloud Sleuth 的工作原理

Spring Cloud Sleuth 在微服务之间传播追踪信息主要依赖于 HTTP 头信息。当一个微服务发起一个新的请求到另一个微服务时,它会将当前的 Trace ID 和 Span ID 等追踪信息添加到 HTTP 头中。目标微服务接收到请求后,从 HTTP 头中提取这些追踪信息,并以此为基础创建新的 Span。

例如,假设微服务 A 发起一个请求到微服务 B。微服务 A 在发送请求时,会在 HTTP 头中添加 X - B3 - TraceId(Trace ID)、X - B3 - SpanId(Span ID)等信息。微服务 B 接收到请求后,从 HTTP 头中提取这些信息,并创建一个新的 Span,该 Span 的 Trace ID 与接收到的 X - B3 - TraceId 一致,同时生成一个新的 Span ID。

在服务内部,Spring Cloud Sleuth 通过 AOP(面向切面编程)技术,在方法调用前后插入逻辑来记录 Span 的开始和结束时间,以及添加 Annotation。比如,当一个服务方法被调用时,Sleuth 会创建一个新的 Span 并记录开始时间,当方法执行结束时,记录结束时间并添加相应的 Annotation。

在项目中集成 Spring Cloud Sleuth

引入依赖

要在 Spring Boot 项目中使用 Spring Cloud Sleuth,首先需要在 pom.xml 文件中引入相关依赖。假设项目使用的是 Maven 构建工具,添加如下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

如果项目使用 Gradle 构建工具,在 build.gradle 文件中添加:

implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'

配置

Spring Cloud Sleuth 通常不需要太多复杂的配置即可开始工作。默认情况下,它会自动配置并在应用程序中启用链路追踪功能。不过,你也可以根据项目需求进行一些定制化配置。

例如,如果你想修改 Trace ID 和 Span ID 的生成策略,可以在 application.yml 文件中添加如下配置:

spring:
  sleuth:
    sampler:
      probability: 1.0 # 设置采样率为100%,即所有请求都进行追踪

上述配置将采样率设置为 100%,意味着所有的请求都会生成 Trace 和 Span 信息。在生产环境中,为了减少性能开销,通常会设置一个较低的采样率,比如 0.1,表示 10% 的请求会被追踪。

代码示例

为了更直观地理解 Spring Cloud Sleuth 的工作原理,下面以一个简单的微服务调用示例来展示。假设我们有两个微服务:service - aservice - bservice - a 会调用 service - b

定义服务接口

service - a 中定义一个接口用于调用 service - b

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "service - b")
public interface ServiceBClient {
    @GetMapping("/message")
    String getMessage();
}

service - b 中定义提供消息的接口:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ServiceBController {
    @GetMapping("/message")
    public String getMessage() {
        return "Hello from Service B";
    }
}

调用服务

service - a 的控制器中调用 service - b

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ServiceAController {
    @Autowired
    private ServiceBClient serviceBClient;

    @GetMapping("/invoke - b")
    public String invokeB() {
        return serviceBClient.getMessage();
    }
}

观察链路追踪信息

当启动这两个微服务并访问 service - a/invoke - b 接口时,Spring Cloud Sleuth 会自动生成 Trace 和 Span 信息。可以通过查看日志来观察这些信息。默认情况下,日志中会输出类似如下的信息:

[service - a,{traceId},{spanId},true] INFO com.example.servicea.ServiceAController - Invoking Service B
[service - b,{traceId},{spanId},true] INFO com.example.serviceb.ServiceBController - Received request from Service A

其中 {traceId}{spanId} 是实际生成的 Trace ID 和 Span ID。通过这些信息,可以清晰地看到请求在 service - aservice - b 之间的流转情况。

与其他工具集成

与 Zipkin 集成

Zipkin 是一个分布式追踪系统,Spring Cloud Sleuth 可以与 Zipkin 无缝集成,将追踪数据发送到 Zipkin 进行可视化展示。

引入 Zipkin 依赖

pom.xml 文件中添加 Zipkin 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

配置 Zipkin 服务器地址

application.yml 文件中添加如下配置:

spring:
  sleuth:
    sampler:
      probability: 1.0
  zipkin:
    base-url: http://localhost:9411 # Zipkin 服务器地址

启动 Zipkin 服务器

可以通过 Docker 快速启动一个 Zipkin 服务器:

docker run -d -p 9411:9411 openzipkin/zipkin

查看 Zipkin 界面

当微服务发送追踪数据到 Zipkin 后,可以通过浏览器访问 http://localhost:9411 打开 Zipkin 界面。在界面上,可以根据 Trace ID 查看详细的链路信息,包括每个 Span 的执行时间、调用关系等。

与 Elasticsearch 集成

为了更好地存储和查询追踪数据,Spring Cloud Sleuth 还可以与 Elasticsearch 集成。将追踪数据存储到 Elasticsearch 中,可以利用 Elasticsearch 的强大搜索和分析功能。

引入 Elasticsearch 依赖

pom.xml 文件中添加 Elasticsearch 相关依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置 Elasticsearch

application.yml 文件中配置 Elasticsearch 地址:

spring:
  elasticsearch:
    rest:
      uris: http://localhost:9200

自定义数据存储逻辑

需要编写自定义的存储逻辑将 Sleuth 生成的追踪数据存储到 Elasticsearch 中。这通常涉及到创建自定义的 SpanExporter 等组件。以下是一个简单的示例:

import brave.Span;
import brave.Tracing;
import brave.export.SpanExporter;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

@Component
public class ElasticsearchSpanExporter implements SpanExporter {

    @Autowired
    private RestHighLevelClient client;

    @Override
    public void export(Span span) {
        try {
            IndexRequest request = new IndexRequest("sleuth_traces")
                  .id(span.context().spanIdString())
                  .source("traceId", span.context().traceIdString(),
                           "spanId", span.context().spanIdString(),
                           "parentId", span.context().parentId(),
                           "name", span.name(),
                           "startTime", new Date(span.startTimestamp()),
                           "duration", span.duration(),
                           "tags", span.tags())
                  .contentType(XContentType.JSON);

            IndexResponse response = client.index(request, RequestOptions.DEFAULT);
            if (!response.getResult().name().equals("CREATED")) {
                // 处理存储失败情况
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述代码定义了一个 ElasticsearchSpanExporter,它实现了 SpanExporter 接口,将每个 Span 的信息存储到 Elasticsearch 的 sleuth_traces 索引中。

性能优化与注意事项

性能优化

虽然 Spring Cloud Sleuth 对微服务性能的影响相对较小,但在高并发场景下,仍然需要关注性能优化。

  • 合理设置采样率:如前文所述,在生产环境中不应将采样率设置为 100%,过高的采样率会增加系统开销。根据业务需求和性能测试结果,合理设置采样率,既能满足问题排查需求,又能减少性能损耗。
  • 异步处理:对于一些非关键的链路追踪操作,比如将追踪数据发送到 Zipkin 或 Elasticsearch,可以采用异步方式处理,避免影响主业务流程的性能。

注意事项

  • 版本兼容性:Spring Cloud Sleuth 与其他 Spring Cloud 组件以及相关集成工具(如 Zipkin、Elasticsearch)之间存在版本兼容性问题。在升级或引入新组件时,务必查阅官方文档,确保版本匹配,以免出现兼容性错误。
  • 日志管理:由于链路追踪会生成大量日志信息,合理管理日志非常重要。可以采用日志分级、定期清理等策略,避免日志文件过大影响系统性能。同时,在日志中记录敏感信息时要谨慎,确保系统安全。

通过深入理解和应用 Spring Cloud Sleuth,开发人员能够在微服务架构中更高效地进行链路追踪,快速定位和解决问题,提升系统的可靠性和可维护性。无论是小型项目还是大型企业级应用,Spring Cloud Sleuth 都为微服务的监控和调试提供了强大的支持。