Skip to content

分布式链路追踪

分布式链路追踪是微服务架构中的关键能力,它允许开发人员和运维人员追踪请求在整个分布式系统中的流转路径,帮助快速定位性能瓶颈和故障原因。Spring Cloud 提供了 Spring Cloud Sleuth 和 Zipkin 集成,用于实现分布式链路追踪功能。

为什么需要分布式链路追踪?

在单体应用中,所有模块都在同一个进程中运行,可以通过日志、调试器等工具轻松跟踪请求的处理过程。但在微服务架构中,一个请求可能涉及多个服务,传统的监控和调试方法难以应对如下挑战:

  1. 服务调用链复杂:一个请求可能涉及多个微服务的协作
  2. 调用关系不可见:难以确定请求经过了哪些服务,以及各服务的处理顺序
  3. 性能瓶颈难定位:无法确定请求处理延迟发生在哪个服务
  4. 故障原因难分析:当系统出现问题时,难以快速定位根本原因

分布式链路追踪系统解决了这些问题,它通过收集分布式系统中各个服务的调用数据,构建完整的调用链,并提供可视化界面,帮助开发人员和运维人员理解系统行为和定位问题。

分布式链路追踪核心概念

1. Trace(追踪)

Trace 是一个请求从开始到结束的完整调用链,由一系列 Span 组成。一个 Trace 有一个全局唯一的 Trace ID,用于标识整个调用链。

2. Span(跨度)

Span 是调用链中的基本工作单元,表示一次逻辑操作,如一次 RPC 调用、数据库查询等。每个 Span 都有一个 Span ID,以及指向父 Span 的引用(如果有的话)。

每个 Span 包含以下信息:

  • 操作名称:描述 Span 执行的操作
  • 起止时间:Span 开始和结束的时间戳
  • 标签:附加在 Span 上的键值对,用于提供额外的上下文信息
  • 日志:Span 执行过程中记录的结构化日志
  • SpanContext:包含 Trace ID、Span ID 和其他追踪信息,用于跨进程传递

3. Annotation(注解)

Annotation 是在 Span 中特定时间点记录的事件,Sleuth 提供了以下核心 Annotation:

  • cs (Client Sent):客户端发送请求的时间,表示 Span 的开始
  • sr (Server Received):服务端接收请求的时间
  • ss (Server Sent):服务端发送响应的时间
  • cr (Client Received):客户端接收响应的时间,表示 Span 的结束

4. Sampling(采样)

由于追踪信息的收集会对系统性能产生一定影响,链路追踪系统通常采用采样策略,只追踪部分请求。采样决策在请求入口处做出,并在整个调用链中保持一致。

Spring Cloud Sleuth

Spring Cloud Sleuth 是 Spring Cloud 提供的分布式追踪解决方案,它可以无缝集成到 Spring Cloud 应用中,自动在常见的入站和出站请求中添加追踪信息。

Sleuth 的主要特性

  1. 自动检测:自动检测常见的通信通道,如 HTTP 请求、Feign 客户端、RestTemplate、消息中间件等
  2. 传递上下文:自动在服务间传递追踪上下文
  3. 集成日志:将追踪信息集成到应用日志中
  4. 与 Zipkin 集成:将追踪数据发送到 Zipkin 进行存储和可视化
  5. 支持采样策略:提供多种采样策略,如概率采样、速率限制采样等

集成 Sleuth

在 Spring Boot 应用中集成 Sleuth 非常简单:

  1. 添加依赖:
xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  1. 配置采样率(可选):
yaml
spring:
  sleuth:
    sampler:
      probability: 1.0  # 采样率,1.0 表示 100% 采样

集成 Sleuth 后,日志输出会自动包含追踪信息:

2023-04-20 14:35:26.879  INFO [service-a,5fe3d8b8b51c393f,5fe3d8b8b51c393f] 82851 --- [nio-8080-exec-1] com.example.ServiceAController          : Handling request

其中:

  • service-a 是应用名称
  • 5fe3d8b8b51c393f 是 Trace ID
  • 5fe3d8b8b51c393f 是 Span ID(在这个例子中,Trace ID 和 Span ID 相同,表示这是调用链的第一个 Span)

Sleuth 与 OpenTracing 集成

OpenTracing 是分布式追踪的标准 API,Sleuth 可以与 OpenTracing 集成,使用 OpenTracing API 进行手动检测:

  1. 添加依赖:
xml
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-opentracing</artifactId>
</dependency>
  1. 使用 OpenTracing API 创建自定义 Span:
java
@Service
public class OrderService {
    
    private final Tracer tracer;
    
    public OrderService(Tracer tracer) {
        this.tracer = tracer;
    }
    
    public Order processOrder(Order order) {
        Span span = tracer.buildSpan("processOrder").start();
        try (Scope scope = tracer.scopeManager().activate(span)) {
            // 处理订单逻辑
            span.setTag("orderId", order.getId());
            
            // 创建子 Span
            Span childSpan = tracer.buildSpan("validateOrder").start();
            try (Scope childScope = tracer.scopeManager().activate(childSpan)) {
                validateOrder(order);
                childSpan.setTag("valid", true);
            } catch (Exception e) {
                childSpan.setTag("error", true);
                childSpan.log(Map.of("error.message", e.getMessage()));
                throw e;
            } finally {
                childSpan.finish();
            }
            
            // 继续处理订单
            updateInventory(order);
            return order;
        } finally {
            span.finish();
        }
    }
    
    private void validateOrder(Order order) {
        // 验证订单逻辑
    }
    
    private void updateInventory(Order order) {
        // 更新库存逻辑
    }
}

Zipkin

Zipkin 是一个开源的分布式追踪系统,用于收集、存储和查询分布式系统中的追踪数据。它提供了 REST API 和 Web UI,用于查询和可视化追踪数据。

Zipkin 架构

Zipkin 系统由以下组件组成:

  1. Collector:接收追踪数据,验证和处理追踪数据,并将其存储
  2. Storage:存储追踪数据,支持多种存储后端,如内存、MySQL、Elasticsearch、Cassandra 等
  3. Query Service:提供 API 用于查询追踪数据
  4. Web UI:基于 Query Service API 的用户界面,用于查看和分析追踪数据

部署 Zipkin 服务器

有多种方式部署 Zipkin 服务器:

  1. 使用 Docker
bash
docker run -d -p 9411:9411 openzipkin/zipkin
  1. 使用 Java
bash
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
  1. 使用 Spring Boot

创建一个 Spring Boot 应用,添加以下依赖和配置:

xml
<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-server</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.java</groupId>
    <artifactId>zipkin-autoconfigure-ui</artifactId>
</dependency>
java
@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZipkinServerApplication.class, args);
    }
}

将 Sleuth 与 Zipkin 集成

要将 Sleuth 收集的追踪数据发送到 Zipkin,需要添加 Zipkin 客户端依赖:

  1. 添加依赖:
xml
<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>
  1. 配置 Zipkin 服务器地址:
yaml
spring:
  zipkin:
    base-url: http://localhost:9411
    sender:
      type: web  # 使用 HTTP 发送追踪数据
  sleuth:
    sampler:
      probability: 1.0  # 采样率

使用消息中间件发送追踪数据

在高流量系统中,直接通过 HTTP 发送追踪数据可能会影响系统性能。可以使用消息中间件(如 RabbitMQ 或 Kafka)异步发送追踪数据:

  1. 添加相应的依赖:

对于 RabbitMQ:

xml
<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>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
</dependency>

对于 Kafka:

xml
<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>
<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
  1. 配置消息中间件:

对于 RabbitMQ:

yaml
spring:
  zipkin:
    sender:
      type: rabbit  # 使用 RabbitMQ 发送追踪数据
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

对于 Kafka:

yaml
spring:
  zipkin:
    sender:
      type: kafka  # 使用 Kafka 发送追踪数据
  kafka:
    bootstrap-servers: localhost:9092

使用 Sleuth 和 Zipkin 实现分布式链路追踪

下面通过一个电商微服务系统的例子,展示如何使用 Sleuth 和 Zipkin 实现分布式链路追踪。

系统架构

系统包括以下服务:

  • API 网关:所有外部请求的入口
  • 订单服务:处理订单相关业务
  • 用户服务:处理用户相关业务
  • 商品服务:处理商品相关业务
  • 支付服务:处理支付相关业务

配置每个服务

对于每个服务,添加 Sleuth 和 Zipkin 依赖,并配置 Zipkin 服务器地址:

xml
<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>
yaml
spring:
  application:
    name: order-service  # 根据服务更改名称
  zipkin:
    base-url: http://zipkin-server:9411
  sleuth:
    sampler:
      probability: 1.0

跟踪 HTTP 请求

Sleuth 会自动跟踪 RestTemplate、Feign 客户端等方式的 HTTP 请求:

java
@Service
public class OrderService {
    
    private final RestTemplate restTemplate;
    
    public OrderService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    public Order createOrder(OrderRequest request) {
        // 调用用户服务验证用户
        User user = restTemplate.getForObject("http://user-service/users/{id}", User.class, request.getUserId());
        
        // 调用商品服务获取商品信息
        Product product = restTemplate.getForObject("http://product-service/products/{id}", Product.class, request.getProductId());
        
        // 创建订单
        Order order = new Order();
        order.setUserId(user.getId());
        order.setProductId(product.getId());
        order.setPrice(product.getPrice());
        order.setStatus(OrderStatus.CREATED);
        
        // 保存订单
        return orderRepository.save(order);
    }
}

Sleuth 会自动为每个 HTTP 请求创建一个新的 Span,并将 Trace ID 和父 Span ID 传递到下游服务。

跟踪异步操作

对于异步操作,Sleuth 提供了多种方式传递追踪上下文:

  1. @Async 方法
java
@Service
public class OrderNotificationService {
    
    private final Logger log = LoggerFactory.getLogger(OrderNotificationService.class);
    
    @Async
    public void sendOrderConfirmation(Order order) {
        log.info("Sending order confirmation for order {}", order.getId());
        // 发送订单确认通知
    }
}
  1. 线程池
java
@Configuration
public class ThreadPoolConfig {
    
    @Bean
    public Executor asyncExecutor(BeanFactory beanFactory) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        
        return new LazyTraceExecutor(beanFactory, executor);
    }
}
  1. CompletableFuture
java
@Service
public class ProductService {
    
    private final RestTemplate restTemplate;
    private final Tracer tracer;
    
    public ProductService(RestTemplate restTemplate, Tracer tracer) {
        this.restTemplate = restTemplate;
        this.tracer = tracer;
    }
    
    public CompletableFuture<Product> getProductAsync(String productId) {
        return CompletableFuture.supplyAsync(() -> {
            Span span = tracer.currentSpan();
            try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
                return restTemplate.getForObject("http://product-service/products/{id}", Product.class, productId);
            }
        });
    }
}

跟踪消息队列

Sleuth 可以跟踪通过消息队列(如 RabbitMQ、Kafka)传递的消息:

  1. 发送消息
java
@Service
public class OrderEventPublisher {
    
    private final RabbitTemplate rabbitTemplate;
    
    public OrderEventPublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }
    
    public void publishOrderCreatedEvent(Order order) {
        OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), order.getUserId(), order.getProductId());
        rabbitTemplate.convertAndSend("order-events", "order.created", event);
    }
}
  1. 接收消息
java
@Component
public class OrderEventListener {
    
    private final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
    
    @RabbitListener(queues = "order-created-queue")
    public void handleOrderCreatedEvent(OrderCreatedEvent event) {
        log.info("Received order created event for order {}", event.getOrderId());
        // 处理订单创建事件
    }
}

Sleuth 会自动在消息头中添加追踪信息,并在消费者端恢复追踪上下文。

自定义 Span

对于一些特殊操作,可以创建自定义 Span 来提供更详细的追踪信息:

java
@Service
public class InventoryService {
    
    private final Logger log = LoggerFactory.getLogger(InventoryService.class);
    private final Tracer tracer;
    
    public InventoryService(Tracer tracer) {
        this.tracer = tracer;
    }
    
    public void updateInventory(String productId, int quantity) {
        Span span = tracer.nextSpan().name("updateInventory");
        try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
            log.info("Updating inventory for product {}", productId);
            
            span.tag("productId", productId);
            span.tag("quantity", String.valueOf(quantity));
            
            // 检查库存
            boolean available = checkAvailability(productId, quantity);
            span.tag("available", String.valueOf(available));
            
            if (!available) {
                log.warn("Insufficient inventory for product {}", productId);
                throw new InsufficientInventoryException(productId);
            }
            
            // 更新库存
            deductInventory(productId, quantity);
            log.info("Inventory updated for product {}", productId);
        } catch (Exception e) {
            span.tag("error", "true");
            span.tag("error.message", e.getMessage());
            throw e;
        } finally {
            span.end();
        }
    }
    
    private boolean checkAvailability(String productId, int quantity) {
        // 检查库存逻辑
        return true;
    }
    
    private void deductInventory(String productId, int quantity) {
        // 扣减库存逻辑
    }
}

在 Zipkin 中查看追踪数据

一旦系统开始运行并生成追踪数据,可以访问 Zipkin Web UI(http://zipkin-server:9411)查看和分析追踪数据:

  1. 搜索追踪:可以按服务名称、操作名称、标签等条件筛选追踪
  2. 查看调用拓扑:可以查看服务间的调用关系和依赖
  3. 分析调用链:可以查看完整的调用链,包括每个 Span 的详细信息和时间线
  4. 识别性能瓶颈:可以通过时间线分析识别系统的性能瓶颈

高级配置

自定义采样策略

Sleuth 支持多种采样策略,可以根据需求进行配置:

yaml
spring:
  sleuth:
    sampler:
      probability: 0.1  # 概率采样,0.1 表示 10% 的请求会被采样

对于更复杂的需求,可以自定义采样器:

java
@Configuration
public class SamplingConfig {
    
    @Bean
    public Sampler customSampler() {
        return new Sampler() {
            @Override
            public boolean isSampled(String traceId) {
                // 自定义采样逻辑
                return true;
            }
        };
    }
}

过滤敏感信息

在某些情况下,可能需要过滤掉一些敏感信息,防止它们被记录到追踪数据中:

java
@Configuration
public class SleuthConfig {
    
    @Bean
    public SpanAdjuster spanAdjuster() {
        return span -> {
            // 移除包含敏感信息的标签
            span.tags().remove("authorization");
            span.tags().remove("cookie");
            return span;
        };
    }
}

自定义 Zipkin 报告器

如果需要自定义 Zipkin 报告器的行为,可以配置自定义的 Zipkin Reporter Bean:

java
@Configuration
public class ZipkinReporterConfig {
    
    @Bean
    public Reporter<Span> customReporter() {
        return new Reporter<Span>() {
            @Override
            public void report(Span span) {
                // 自定义报告逻辑
            }
        };
    }
}

与 Spring Cloud Sleuth 3.0+ 和 Micrometer Tracing 的迁移

从 Spring Cloud 2020.0.0 版本开始,Spring Cloud Sleuth 3.0+ 开始逐步迁移到 Micrometer Tracing。在 Spring Boot 3.0 中,推荐使用 Micrometer Tracing 替代 Spring Cloud Sleuth:

  1. 添加依赖:
xml
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>
  1. 配置:
yaml
management:
  tracing:
    sampling:
      probability: 1.0
    propagation:
      type: b3
  zipkin:
    tracing:
      endpoint: http://zipkin-server:9411/api/v2/spans
  1. 使用 Micrometer Tracing API:
java
@Service
public class OrderService {
    
    private final Logger log = LoggerFactory.getLogger(OrderService.class);
    private final Tracer tracer;
    
    public OrderService(Tracer tracer) {
        this.tracer = tracer;
    }
    
    public Order processOrder(Order order) {
        Span span = tracer.nextSpan().name("processOrder").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            log.info("Processing order {}", order.getId());
            // 处理订单
            return order;
        } finally {
            span.end();
        }
    }
}

最佳实践

有效使用标签

标签是键值对,可以附加在 Span 上提供额外的上下文信息:

java
span.tag("orderId", order.getId());
span.tag("userId", order.getUserId());
span.tag("amount", String.valueOf(order.getAmount()));

标签的有效使用可以帮助更快地定位问题:

  • 使用有意义的键名
  • 对于常见属性使用统一的命名约定
  • 避免在标签中包含大量数据或敏感信息

记录关键事件

除了标签外,还可以在 Span 中记录关键事件:

java
span.event("orderValidated");
span.event("paymentProcessed");

对于更详细的事件信息,可以使用 log 方法:

java
span.log(Map.of(
    "event", "orderShipped",
    "shippingCompany", "DHL",
    "trackingNumber", "123456789"
));

控制 Span 的粒度

合理的 Span 粒度对于有效的追踪至关重要:

  • 粒度太粗:无法定位具体问题
  • 粒度太细:产生过多数据,增加系统负担

一般原则:

  • 为每个重要的逻辑操作创建一个 Span
  • 为可能失败或耗时的操作创建单独的 Span
  • 避免为非常简单或快速的操作创建 Span

处理错误

当操作失败时,应在 Span 中记录错误信息:

java
try {
    // 业务逻辑
} catch (Exception e) {
    span.tag("error", "true");
    span.tag("error.message", e.getMessage());
    span.tag("error.type", e.getClass().getName());
    throw e;
}

性能考虑

分布式追踪会对系统性能产生一定影响,应考虑以下几点:

  • 使用适当的采样率,根据系统负载和需求调整
  • 对于高流量系统,使用消息中间件异步发送追踪数据
  • 避免在热点代码路径中创建过多的 Span
  • 定期监控追踪系统的性能和资源使用情况

总结

分布式链路追踪是微服务架构中不可或缺的组件,它提供了透明的跨服务可视化能力,帮助开发人员和运维人员理解系统行为、定位性能瓶颈和排查故障。Spring Cloud Sleuth 和 Zipkin 提供了一套完整的分布式追踪解决方案,易于集成到 Spring Cloud 应用中。

通过合理配置和使用 Sleuth 和 Zipkin,可以实现对微服务系统的全方位监控,提高系统的可观测性,降低运维成本,为构建可靠的微服务系统提供支持。随着微服务规模的扩大和复杂度的提高,分布式链路追踪将变得越来越重要,成为微服务架构的标准组件。