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

Spring Cloud Sleuth 分布式链路追踪详解

2024-06-215.8k 阅读

什么是分布式链路追踪

在单体应用架构中,系统的功能模块相对集中,调用关系较为简单。当出现问题时,通过简单的日志记录和调试手段,我们可以较容易地定位问题所在。然而,随着业务的不断发展和系统规模的逐渐扩大,单体应用架构面临着诸如可维护性差、可扩展性不足等诸多挑战。因此,微服务架构应运而生。

在微服务架构下,一个完整的业务系统被拆分成多个独立的微服务,每个微服务专注于完成特定的业务功能。这些微服务之间通过网络相互调用,协同工作以满足业务需求。这种架构虽然提升了系统的可维护性和可扩展性,但也引入了新的复杂性。例如,一次用户请求可能会涉及多个微服务之间的多次调用,形成一条复杂的调用链路。当请求出现异常或性能问题时,由于涉及多个微服务,很难快速准确地定位问题出在哪一个微服务以及具体的哪一次调用上。

分布式链路追踪(Distributed Tracing)就是为了解决上述问题而诞生的技术。它通过在整个分布式系统中为每个请求分配一个唯一的标识符(称为Trace ID),并在请求经过的每个微服务中记录相关的调用信息(如调用时间、处理时间等),从而可以完整地记录下一次请求在分布式系统中的调用路径和处理过程。这样,当出现问题时,我们可以根据Trace ID快速定位到问题所在的微服务和具体调用,大大提高了问题排查和性能优化的效率。

Spring Cloud Sleuth 简介

Spring Cloud Sleuth 是 Spring Cloud 生态系统中的一个重要组件,专门用于实现分布式链路追踪功能。它基于业界广泛使用的分布式追踪标准,如 OpenTracing 等,为基于 Spring Boot 的微服务应用提供了简单易用的分布式链路追踪解决方案。

Spring Cloud Sleuth 的设计理念是尽可能地对业务代码透明,开发者只需在微服务项目中引入相关依赖,进行少量的配置,就可以自动为微服务之间的调用添加链路追踪功能。它会自动生成 Trace ID 和 Span ID,记录调用的开始时间、结束时间等关键信息,并将这些信息通过日志或其他方式进行记录,方便开发者后续查看和分析。

Spring Cloud Sleuth 的核心概念

  1. Trace(追踪)
    • Trace 代表一次完整的用户请求在分布式系统中的调用路径。它是一个由多个 Span 组成的树状结构,从请求的入口开始,到请求最终处理完成结束。例如,一个电商系统中,用户下单请求可能会涉及订单服务、库存服务、支付服务等多个微服务的调用,整个这一系列调用构成一个 Trace。
    • Trace 有一个全局唯一的 Trace ID,用于标识这个特定的请求路径。通过 Trace ID,我们可以在分布式系统的海量日志中快速筛选出与该请求相关的所有调用信息,从而全面了解请求的处理过程。
  2. Span(跨度)
    • Span 是 Trace 中的基本工作单元,它代表了一次微服务内部或微服务之间的具体调用。每个 Span 都有一个唯一的 Span ID,同时它还包含一些关键信息,如开始时间、结束时间、操作名称等。例如,在订单服务调用库存服务查询库存的过程,就可以看作是一个 Span。
    • Span 之间存在父子关系,形成树状结构。根 Span 通常是请求进入系统的入口点,后续的子 Span 则是在处理请求过程中微服务之间的具体调用。通过这种父子关系,我们可以清晰地了解请求在各个微服务之间的流转顺序和调用层次。
  3. Annotation(注解)
    • Annotation 用于在 Span 中记录一些关键事件,如请求的发送(cs - Client Send)、请求的接收(sr - Server Receive)、响应的发送(ss - Server Send)、响应的接收(cr - Client Receive)等。这些注解时间戳信息对于分析请求的性能和调用过程非常重要。
    • 例如,通过记录 cs 和 cr 的时间差,可以了解客户端发起请求到接收到响应的总时间;通过记录 sr 和 ss 的时间差,可以了解服务端处理请求的时间。这些时间信息对于定位性能瓶颈和优化系统性能至关重要。

Spring Cloud Sleuth 的工作原理

  1. 生成 Trace ID 和 Span ID
    • 当一个请求进入基于 Spring Cloud Sleuth 的微服务系统时,Spring Cloud Sleuth 会为该请求生成一个全局唯一的 Trace ID。同时,在请求进入的第一个微服务(根 Span)中,会生成一个唯一的 Span ID。
    • 当该微服务调用其他微服务时,会为每个子调用生成新的 Span ID,并将父 Span ID 作为新 Span 的引用。这样,通过 Trace ID 可以串联起整个请求的调用链路,而通过 Span ID 及其父子关系可以清晰地看到每个具体调用在链路中的位置和层次。
  2. 传递 Trace 上下文
    • 在微服务之间进行调用时,Spring Cloud Sleuth 需要将 Trace 上下文(包括 Trace ID、Span ID 等关键信息)传递到下一个微服务中,以便继续记录调用链路。这通常通过在 HTTP 头或消息队列消息中添加特定的追踪相关头信息来实现。
    • 例如,在基于 HTTP 的微服务调用中,Spring Cloud Sleuth 会在 HTTP 请求头中添加 X - B3 - TraceId(Trace ID)、X - B3 - SpanId(Span ID)等头信息。当目标微服务接收到请求时,会从请求头中提取这些信息,从而将新的调用纳入到同一个 Trace 中。
  3. 记录 Annotation 信息
    • 在每个 Span 的生命周期中,Spring Cloud Sleuth 会自动记录关键的 Annotation 信息,如请求的发送和接收时间、响应的发送和接收时间等。这些信息会随着 Span 的结束被记录到日志或发送到外部的追踪存储系统中。
    • 例如,当一个微服务接收到请求(sr 事件)时,Spring Cloud Sleuth 会记录当前时间作为该事件的时间戳;当该微服务处理完请求并发送响应(ss 事件)时,又会记录此时的时间戳。通过这些时间戳信息,我们可以计算出微服务处理请求的耗时等性能指标。

在 Spring Boot 项目中集成 Spring Cloud Sleuth

  1. 添加依赖
    • 首先,在 Spring Boot 项目的 pom.xml 文件中添加 Spring Cloud Sleuth 依赖。假设使用的是 Maven 构建工具,示例如下:
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - starter - sleuth</artifactId>
    </dependency>
    
    • 如果项目使用 Gradle 构建,依赖添加如下:
    implementation 'org.springframework.cloud:spring - cloud - starter - sleuth'
    
  2. 配置基本参数
    • Spring Cloud Sleuth 通常不需要大量复杂的配置即可开始工作。但在一些场景下,可能需要配置一些基本参数。例如,可以通过 application.propertiesapplication.yml 文件配置是否启用 Sleuth。
    • application.yml 中的配置示例:
    spring:
      sleuth:
        enabled: true
    
    • 还可以配置一些与采样率相关的参数。采样率决定了有多少请求会被进行链路追踪。默认情况下,Spring Cloud Sleuth 的采样率为 1.0,即所有请求都会被追踪。如果系统请求量非常大,为了减少性能开销,可以适当降低采样率。例如,将采样率设置为 0.1,表示只有 10% 的请求会被追踪。配置如下:
    spring:
      sleuth:
        sampler:
          probability: 0.1
    
  3. 查看追踪日志
    • 集成 Spring Cloud Sleuth 后,当项目运行并处理请求时,会在日志中记录相关的追踪信息。默认情况下,Spring Cloud Sleuth 会将追踪信息以日志的形式输出,日志格式中会包含 Trace ID、Span ID 以及 Annotation 等关键信息。
    • 例如,在日志中可能会看到类似如下的记录:
    [traceId = 1234567890abcdef, spanId = 0123456789abcdef, parentSpanId = null, exported = true] INFO com.example.demo.controller.DemoController - Handling request
    
    • 这里 traceId 就是 Trace ID,spanId 是当前 Span 的 ID,parentSpanId 在根 Span 中为 nullexported 表示该 Span 是否被导出到外部追踪系统(如果配置了外部追踪系统)。通过这些日志信息,我们可以初步了解请求在微服务中的调用情况。

Spring Cloud Sleuth 与 Zipkin 集成

虽然 Spring Cloud Sleuth 可以通过日志记录追踪信息,但在实际生产环境中,仅仅通过日志来分析分布式链路追踪数据存在一些局限性,比如查看不方便、难以进行可视化分析等。Zipkin 是一个分布式追踪系统,它可以接收、存储和展示 Spring Cloud Sleuth 生成的追踪数据,为我们提供更直观、更强大的分析工具。

  1. 搭建 Zipkin Server
    • 可以通过 Docker 快速搭建一个 Zipkin Server。执行以下命令:
    docker run -d -p 9411:9411 openzipkin/zipkin
    
    • 这会在本地启动一个 Zipkin Server,监听在 9411 端口。Zipkin Server 启动后,可以通过浏览器访问 http://localhost:9411 打开 Zipkin 的 Web 界面。
  2. 配置微服务连接 Zipkin
    • 在 Spring Boot 微服务项目中,需要配置连接到 Zipkin Server。在 pom.xml 文件中添加 Zipkin 客户端依赖:
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - sleuth - zipkin</artifactId>
    </dependency>
    
    • application.yml 文件中配置 Zipkin Server 的地址:
    spring:
      sleuth:
        sampler:
          probability: 1.0
      zipkin:
        base - url: http://localhost:9411
    
    • 这里将采样率设置为 1.0,确保所有请求的追踪数据都发送到 Zipkin Server。spring.zipkin.base - url 配置了 Zipkin Server 的地址。
  3. 在 Zipkin 中查看追踪数据
    • 当微服务应用与 Zipkin Server 配置好并运行后,微服务生成的追踪数据会发送到 Zipkin Server。在 Zipkin 的 Web 界面中,可以通过 Trace ID、服务名称等条件进行查询。
    • Zipkin 会以可视化的方式展示调用链路,包括每个微服务的调用顺序、调用耗时等信息。例如,可以看到一个请求在各个微服务之间的流转路径,以及每个微服务处理请求的时间。通过这种可视化展示,能够更直观地发现性能瓶颈和调用异常等问题。

Spring Cloud Sleuth 高级特性与优化

  1. 自定义 Span 和 Annotation
    • 在某些情况下,Spring Cloud Sleuth 自动生成的 Span 和 Annotation 可能无法满足业务需求,这时可以进行自定义。
    • 例如,假设在一个电商微服务中,有一个复杂的订单处理逻辑,其中包含多个子步骤,希望对每个子步骤进行单独的追踪。可以通过编程方式创建自定义 Span。示例代码如下:
    import brave.Span;
    import brave.Tracer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class CustomSpanController {
    
        @Autowired
        private Tracer tracer;
    
        @GetMapping("/custom - span")
        public String customSpan() {
            Span span = tracer.nextSpan().name("custom - order - processing").start();
            try {
                // 模拟订单处理子步骤
                // 子步骤 1
                Span subSpan1 = tracer.nextSpan(span.context()).name("check - inventory").start();
                try {
                    // 检查库存逻辑
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    subSpan1.finish();
                }
                // 子步骤 2
                Span subSpan2 = tracer.nextSpan(span.context()).name("calculate - price").start();
                try {
                    // 计算价格逻辑
                    Thread.sleep(150);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    subSpan2.finish();
                }
                return "Custom span processed";
            } finally {
                span.finish();
            }
        }
    }
    
    • 在这段代码中,首先创建了一个名为 custom - order - processing 的主 Span,然后在主 Span 内创建了两个子 Span,分别代表检查库存和计算价格的步骤。通过这种方式,可以更细粒度地追踪业务逻辑的执行过程。
  2. 性能优化
    • 调整采样率:如前文所述,采样率对系统性能有较大影响。在高并发系统中,如果所有请求都进行追踪,会产生大量的追踪数据,对系统性能造成一定压力。通过合理调整采样率,可以在保证一定问题排查能力的同时,降低性能开销。例如,对于一些关键业务流程,可以适当提高采样率;对于普通业务流程,可降低采样率。
    • 异步发送追踪数据:Spring Cloud Sleuth 默认是同步发送追踪数据到 Zipkin 等外部系统。在高并发场景下,同步发送可能会影响微服务的响应性能。可以配置异步发送方式,通过引入消息队列等中间件,将追踪数据先发送到消息队列,然后由异步任务从消息队列中消费并发送到外部追踪系统。这样可以减少微服务处理请求的等待时间,提高系统整体性能。
    • 优化日志输出:如果主要依赖日志进行追踪分析,优化日志输出格式和频率也很重要。可以采用更简洁但又包含关键信息的日志格式,减少日志输出的冗余。同时,合理设置日志级别,避免在生产环境输出过多的调试信息,从而降低日志输出对系统性能的影响。

Spring Cloud Sleuth 与其他技术的集成

  1. 与 Spring Cloud Gateway 集成
    • Spring Cloud Gateway 是 Spring Cloud 生态中的 API 网关,它可以对请求进行路由、过滤等操作。将 Spring Cloud Sleuth 与 Spring Cloud Gateway 集成,可以在网关层面就开始记录请求的追踪信息,并将追踪上下文传递到后续的微服务中。
    • 首先,在 Spring Cloud Gateway 项目中添加 Spring Cloud Sleuth 依赖:
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - starter - sleuth</artifactId>
    </dependency>
    
    • 然后,Spring Cloud Gateway 会自动将追踪相关的头信息(如 X - B3 - TraceIdX - B3 - SpanId 等)传递到下游微服务,确保整个请求链路的追踪信息完整。在网关的日志中,也会记录与请求相关的追踪信息,方便从入口处开始分析请求的处理过程。
  2. 与 Feign 集成
    • Feign 是 Spring Cloud 中用于声明式 HTTP 客户端的组件,常用于微服务之间的 HTTP 调用。Spring Cloud Sleuth 与 Feign 集成非常方便,它会自动为 Feign 客户端的调用添加链路追踪功能。
    • 当在微服务项目中引入 Feign 和 Spring Cloud Sleuth 依赖后,Feign 客户端在发起 HTTP 请求时,会自动将当前的 Trace 上下文添加到请求头中,传递给目标微服务。同时,Feign 客户端调用的开始和结束时间等信息也会被记录为 Span 信息,纳入到整个分布式链路追踪体系中。这使得我们可以清晰地了解微服务之间通过 Feign 进行调用的具体情况,包括调用耗时、是否出现异常等。

实践案例分析

假设我们有一个简单的电商微服务系统,包含用户服务、订单服务和库存服务。用户下单时,订单服务需要调用用户服务获取用户信息,调用库存服务检查库存并扣减库存。

  1. 项目结构与依赖配置
    • 每个微服务都是一个独立的 Spring Boot 项目。在每个项目的 pom.xml 文件中添加 Spring Cloud Sleuth 和 Zipkin 相关依赖:
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - starter - sleuth</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring - cloud - sleuth - zipkin</artifactId>
    </dependency>
    
    • application.yml 文件中配置 Zipkin Server 地址和采样率:
    spring:
      sleuth:
        sampler:
          probability: 1.0
      zipkin:
        base - url: http://localhost:9411
    
  2. 微服务间调用实现
    • 订单服务调用用户服务:订单服务使用 Feign 客户端调用用户服务获取用户信息。示例代码如下:
    @FeignClient(name = "user - service")
    public interface UserFeignClient {
        @GetMapping("/users/{userId}")
        User getUserById(@PathVariable("userId") Long userId);
    }
    
    @Service
    public class OrderService {
    
        @Autowired
        private UserFeignClient userFeignClient;
    
        public void placeOrder(Order order) {
            Long userId = order.getUserId();
            User user = userFeignClient.getUserById(userId);
            // 后续订单处理逻辑
        }
    }
    
    • 订单服务调用库存服务:同样使用 Feign 客户端调用库存服务检查库存和扣减库存。
    @FeignClient(name = "inventory - service")
    public interface InventoryFeignClient {
        @PostMapping("/inventory/check")
        boolean checkInventory(@RequestBody InventoryCheckRequest request);
    
        @PostMapping("/inventory/deduct")
        boolean deductInventory(@RequestBody InventoryDeductRequest request);
    }
    
    @Service
    public class OrderService {
    
        @Autowired
        private InventoryFeignClient inventoryFeignClient;
    
        public void placeOrder(Order order) {
            // 检查库存
            InventoryCheckRequest checkRequest = new InventoryCheckRequest();
            checkRequest.setProductId(order.getProductId());
            checkRequest.setQuantity(order.getQuantity());
            boolean hasInventory = inventoryFeignClient.checkInventory(checkRequest);
            if (hasInventory) {
                // 扣减库存
                InventoryDeductRequest deductRequest = new InventoryDeductRequest();
                deductRequest.setProductId(order.getProductId());
                deductRequest.setQuantity(order.getQuantity());
                inventoryFeignClient.deductInventory(deductRequest);
            } else {
                // 库存不足处理
            }
            // 后续订单处理逻辑
        }
    }
    
  3. 问题排查与分析
    • 假设在某个订单处理过程中出现异常,通过在 Zipkin 界面中根据 Trace ID 查找该订单请求的调用链路。可以看到订单服务调用用户服务和库存服务的具体情况,包括调用时间、响应时间等。
    • 如果发现库存服务调用耗时过长,进一步查看库存服务的相关日志和 Span 信息,可能发现是数据库查询库存的操作导致性能问题。通过这种方式,利用 Spring Cloud Sleuth 和 Zipkin 可以快速定位和解决分布式系统中的问题。

通过以上对 Spring Cloud Sleuth 的详细介绍,从核心概念、工作原理到实际应用中的集成、优化以及与其他技术的结合,我们可以看到它在分布式链路追踪方面为微服务架构提供了强大而实用的功能,帮助开发者更好地理解和管理复杂的分布式系统。