跳到主要内容

分布式链路追踪

在微服务架构中,一个请求可能经过多个服务的处理,每个服务可能还调用其他服务。当出现性能问题或错误时,如何快速定位是哪个服务出了问题?分布式链路追踪(Distributed Tracing)就是解决这个问题的核心技术。

为什么需要链路追踪?

微服务架构的挑战

假设一个电商系统的下单流程:

┌─────────────────────────────────────────────────────────────────────┐
│ 下单请求调用链 │
│ │
│ 客户端 │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ API网关 │ 100ms │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 订单服务 │ 200ms ───→ 库存服务 (150ms) │
│ └──────┬───────┘ └──→ 商品服务 (80ms) │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 支付服务 │ 300ms ───→ 账户服务 (200ms) │
│ └──────────────┘ └──→ 银行网关 (500ms) │
│ │
│ 总耗时:用户等待了多久?哪个服务最慢? │
│ 出错时:错误发生在哪个服务? │
└─────────────────────────────────────────────────────────────────────┘

核心问题

  • 一个请求经过了哪些服务?
  • 每个服务处理耗时多少?
  • 哪个服务是性能瓶颈?
  • 错误发生在哪个服务、哪行代码?

链路追踪的作用

链路追踪可以帮我们:

  1. 可视化调用链:清晰展示请求在服务间的流转路径
  2. 性能分析:识别慢服务、慢接口,进行针对性优化
  3. 故障定位:快速定位错误发生的服务和代码位置
  4. 依赖分析:了解服务间的依赖关系
  5. 容量规划:基于调用数据进行容量评估

核心概念

Trace 和 Span

链路追踪的核心概念是 Trace(链路)Span(跨度)

┌─────────────────────────────────────────────────────────────────────┐
│ Trace 结构示意 │
│ │
│ Trace ID: abc123 (一次请求对应一个 Trace) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Span: API网关 (100ms) │ │
│ │ Span ID: span-1 │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Span: 订单服务 (200ms) │ │ │
│ │ │ Span ID: span-2, Parent: span-1 │ │ │
│ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │
│ │ │ │ Span: 库存服务 │ │ Span: 商品服务 │ │ │ │
│ │ │ │ (150ms) │ │ (80ms) │ │ │ │
│ │ │ │ Parent: span-2 │ │ Parent: span-2 │ │ │ │
│ │ │ └───────────────────┘ └───────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Span: 支付服务 (300ms) │ │ │
│ │ │ Parent: span-1 │ │ │
│ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │
│ │ │ │ Span: 账户服务 │ │ Span: 银行网关 │ │ │ │
│ │ │ │ (200ms) │ │ (500ms) │ │ │ │
│ │ │ └───────────────────┘ └───────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Trace(链路)

  • 代表一次完整的请求链路
  • 包含一个或多个 Span
  • 由全局唯一的 Trace ID 标识

Span(跨度)

  • 代表一个独立的工作单元(如一次 RPC 调用、一次数据库查询)
  • 包含以下信息:
    • Span ID:当前 Span 的唯一标识
    • Parent Span ID:父 Span 的 ID(用于构建调用树)
    • 开始时间结束时间:用于计算耗时
    • 操作名称:描述这个 Span 做了什么
    • Tags:键值对形式的元数据(如 HTTP 状态码、数据库语句)
    • Logs:事件日志
    • Status:执行状态(成功/失败)

上下文传播

链路追踪需要在服务间传播上下文信息,最常见的是 W3C Trace Context 标准:

┌─────────────────────────────────────────────────────────────────────┐
│ 上下文传播过程 │
│ │
│ 服务 A │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 生成 Trace ID: abc123 │ │
│ │ 2. 生成 Span ID: span-1 │ │
│ │ 3. 创建 HTTP Header: │ │
│ │ traceparent: 00-abc123-span-1-01 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ │ HTTP Request │
│ │ Headers: │
│ │ traceparent: 00-abc123-span-1-01 │
│ ▼ │
│ 服务 B │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 解析 Header 获取 Trace ID: abc123 │ │
│ │ 2. 创建新 Span ID: span-2 │ │
│ │ 3. 记录 Parent Span ID: span-1 │ │
│ │ 4. 继续处理请求... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

traceparent 格式(W3C 标准):

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ │ │ └── trace-flags
│ │ └── parent-span-id
│ └── trace-id
└── version

Spring Boot 集成链路追踪

从 Spring Boot 3.0 开始,Spring Cloud Sleuth 被移除,改用 Micrometer Tracing 作为链路追踪的门面,支持多种 Tracer 实现。

支持的 Tracer

Tracer说明
OpenTelemetry + ZipkinOpenTelemetry 是可观测性的行业标准,Zipkin 作为后端存储
OpenTelemetry + OTLP使用 OpenTelemetry Protocol 发送到支持 OTLP 的后端
Brave + ZipkinOpenZipkin 的 Brave 库,Spring Cloud Sleuth 旧版使用的方案

快速入门:OpenTelemetry + Zipkin

1. 添加依赖

<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Micrometer Tracing 与 OpenTelemetry 的桥接 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>

<!-- OpenTelemetry Exporter - Zipkin -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>

<!-- Web MVC 自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. 配置文件

# application.yml

spring:
application:
name: order-service # 服务名称,显示在追踪数据中

management:
tracing:
sampling:
probability: 1.0 # 采样率,1.0 表示 100% 采样(开发环境)
enabled: true
# Baggage 配置(跨服务传递自定义数据)
baggage:
remote-fields: userId,tenantId # 传播到下游的 baggage
correlation:
fields: userId,tenantId # 添加到 MDC 的 baggage

# Zipkin 配置
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans

endpoints:
web:
exposure:
include: health,info,metrics,prometheus

# 日志配置,显示 Trace ID
logging:
pattern:
correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "

3. 启动 Zipkin

使用 Docker 快速启动 Zipkin:

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

或者下载 Zipkin Jar 包:

curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar

访问 http://localhost:9411 查看 Zipkin UI。

4. 编写示例代码

@SpringBootApplication
@RestController
@Slf4j
public class OrderApplication {

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

@Autowired
private RestTemplate restTemplate;

@Autowired
private ObservationRegistry observationRegistry;

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}

@GetMapping("/order")
public String createOrder() {
log.info("创建订单开始");

// 调用库存服务
String inventoryResult = restTemplate.getForObject(
"http://localhost:8081/inventory", String.class);
log.info("库存服务返回: {}", inventoryResult);

// 调用支付服务
String paymentResult = restTemplate.getForObject(
"http://localhost:8082/payment", String.class);
log.info("支付服务返回: {}", paymentResult);

log.info("创建订单完成");
return "订单创建成功";
}

/**
* 使用 Observation API 创建自定义 Span
*/
@GetMapping("/custom-span")
public String customSpan() {
return Observation.createNotStarted("custom-operation", observationRegistry)
.lowCardinalityKeyValue("operation.type", "manual")
.highCardinalityKeyValue("user.id", "user-123")
.observe(() -> {
// 业务逻辑
log.info("执行自定义操作");
simulateSlowOperation();
return "自定义 Span 创建成功";
});
}

private void simulateSlowOperation() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

5. 测试

# 调用接口
curl http://localhost:8080/order

# 查看日志,可以看到 Trace ID 和 Span ID
# [order-service,abc123,span-1] 创建订单开始
# [order-service,abc123,span-1] 库存服务返回: OK

# 打开 Zipkin UI (http://localhost:9411) 查看追踪数据

日志关联 ID

默认情况下,Spring Boot 会在日志中包含 [traceId-spanId] 格式的关联 ID:

2024-01-15 10:30:00.123 INFO [order-service,65f8a4b3c2d1e5f0,65f8a4b3c2d1e5f0] 12345 --- [nio-8080-exec-1] c.e.OrderApplication : 创建订单开始

可以自定义日志格式:

logging:
pattern:
correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] "
include-application-name: false

服务间调用追踪

OpenFeign 集成

OpenFeign 自动支持链路追踪,只需使用自动配置的 Feign.Builder

@Configuration
public class FeignConfig {

// 无需额外配置,Spring Boot 自动注入支持追踪的 Builder
// Feign 会自动传播 Trace Context
}

@FeignClient(name = "inventory-service", url = "${inventory.service.url}")
public interface InventoryClient {

@GetMapping("/inventory/deduct")
Result deduct(@RequestParam("productId") String productId,
@RequestParam("count") Integer count);
}

重要:不要手动创建 Feign.builder(),否则追踪不会生效。

WebClient 集成

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient(WebClient.Builder builder) {
// 使用自动配置的 Builder,追踪自动生效
return builder
.baseUrl("http://inventory-service")
.build();
}
}

@Service
public class InventoryService {

@Autowired
private WebClient webClient;

public Mono<String> deduct(String productId, int count) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/inventory/deduct")
.queryParam("productId", productId)
.queryParam("count", count)
.build())
.retrieve()
.bodyToMono(String.class);
}
}

RestTemplate 集成

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// 使用自动配置的 Builder
return builder
.rootUri("http://inventory-service")
.build();
}
}

自定义 Span

使用 Observation API

@Service
@Slf4j
public class OrderService {

private final ObservationRegistry observationRegistry;

public OrderService(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}

public Order createOrder(OrderRequest request) {
return Observation.createNotStarted("order.create", observationRegistry)
.lowCardinalityKeyValue("order.type", request.getType()) // 低基数标签
.highCardinalityKeyValue("user.id", request.getUserId()) // 高基数标签
.observe(() -> {
log.info("创建订单: {}", request);

// 业务逻辑
Order order = new Order();
order.setOrderId(UUID.randomUUID().toString());
order.setUserId(request.getUserId());
order.setStatus("CREATED");

// 保存到数据库(自动创建数据库 Span)
orderRepository.save(order);

return order;
});
}
}

低基数 vs 高基数标签

类型说明示例
低基数(Low Cardinality)值的种类有限,可用于聚合http.methodhttp.status_codeorder.type
高基数(High Cardinality)值的种类很多,不适合聚合user.idorder.idtrace.id

使用 Tracer API(更底层)

@Service
public class PaymentService {

private final Tracer tracer;

public PaymentService(Tracer tracer) {
this.tracer = tracer;
}

public void processPayment(String orderId, BigDecimal amount) {
// 创建新的 Span
Span span = tracer.nextSpan().name("payment.process");

try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
// 添加标签
span.tag("order.id", orderId);
span.tag("payment.amount", amount.toString());

// 业务逻辑
doPayment(orderId, amount);

// 记录事件
span.event("payment.completed");

} catch (Exception e) {
span.error(e); // 记录错误
throw e;
} finally {
span.end(); // 结束 Span
}
}
}

Baggage 传播

Baggage 是在整条调用链中传播的自定义数据,常用于传递用户 ID、租户 ID 等上下文信息。

配置 Baggage

management:
tracing:
baggage:
# 需要传播到下游服务的字段
remote-fields: userId,tenantId,requestId
# 需要添加到 MDC 的字段(可在日志中使用)
correlation:
fields: userId,tenantId

使用 Baggage

@Service
public class UserService {

private final Tracer tracer;

public UserService(Tracer tracer) {
this.tracer = tracer;
}

public void processWithBaggage(String userId) {
// 创建 Baggage
try (BaggageInScope baggage = tracer.createBaggageInScope("userId", userId)) {
// Baggage 会自动传播到下游服务
// 同时也添加到了 MDC,可在日志中使用 %X{userId}

callDownstreamService();
}
}
}

在日志中使用 Baggage

<!-- logback-spring.xml -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId:-},%X{spanId:-},%X{userId:-}] [%thread] %-5level %logger{36} - %msg%n</pattern>

数据库追踪

自动追踪 JDBC

添加依赖后,JDBC 操作会自动创建 Span:

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

追踪特定数据库操作

@Repository
public class OrderRepository {

private final JdbcTemplate jdbcTemplate;
private final ObservationRegistry observationRegistry;

public Order findById(String orderId) {
return Observation.createNotStarted("db.order.find", observationRegistry)
.lowCardinalityKeyValue("db.table", "orders")
.highCardinalityKeyValue("order.id", orderId)
.observe(() -> {
String sql = "SELECT * FROM orders WHERE order_id = ?";
return jdbcTemplate.queryForObject(sql, orderRowMapper(), orderId);
});
}
}

与 Jaeger 集成

Jaeger 是另一个流行的分布式追踪系统,由 Uber 开源。

使用 OpenTelemetry + OTLP

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
management:
opentelemetry:
tracing:
export:
otlp:
endpoint: http://localhost:4318/v1/traces

启动 Jaeger:

docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14250:14250 \
-p 14268:14268 \
-p 14269:14269 \
-p 9411:9411 \
jaegertracing/all-in-one:latest

访问 http://localhost:16686 查看 Jaeger UI。

完整示例:微服务追踪

项目结构

tracing-demo/
├── api-gateway/ # API 网关(8080)
├── order-service/ # 订单服务(8081)
├── inventory-service/ # 库存服务(8082)
├── payment-service/ # 支付服务(8083)
└── docker-compose.yml

docker-compose.yml

version: '3.8'

services:
zipkin:
image: openzipkin/zipkin
ports:
- "9411:9411"
networks:
- tracing-network

order-service:
build: ./order-service
ports:
- "8081:8081"
environment:
- SPRING_PROFILES_ACTIVE=docker
- MANAGEMENT_ZIPKIN_TRACING_ENDPOINT=http://zipkin:9411/api/v2/spans
depends_on:
- zipkin
networks:
- tracing-network

inventory-service:
build: ./inventory-service
ports:
- "8082:8082"
environment:
- SPRING_PROFILES_ACTIVE=docker
- MANAGEMENT_ZIPKIN_TRACING_ENDPOINT=http://zipkin:9411/api/v2/spans
depends_on:
- zipkin
networks:
- tracing-network

payment-service:
build: ./payment-service
ports:
- "8083:8083"
environment:
- SPRING_PROFILES_ACTIVE=docker
- MANAGEMENT_ZIPKIN_TRACING_ENDPOINT=http://zipkin:9411/api/v2/spans
depends_on:
- zipkin
networks:
- tracing-network

networks:
tracing-network:

公共配置

每个服务都需要添加依赖:

<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Micrometer Tracing + OpenTelemetry -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>

<!-- Zipkin Exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
</dependencies>

测试追踪效果

# 启动所有服务
docker-compose up -d

# 调用订单接口
curl http://localhost:8081/order/create?userId=user1\&productId=prod1\&amount=100

# 打开 Zipkin UI
# http://localhost:9411
# 点击 "Run Query" 查看追踪数据

最佳实践

采样策略

生产环境不需要 100% 采样,可以根据场景设置:

management:
tracing:
sampling:
probability: 0.1 # 10% 采样率

建议

  • 开发/测试环境:100% 采样
  • 生产环境:根据流量调整,一般 1%-10%

标签设计

低基数标签(适合聚合分析):

  • http.method
  • http.status_code
  • http.route
  • service.name
  • db.operation

高基数标签(不适合聚合):

  • user.id
  • order.id
  • trace.id

性能考虑

  1. 异步导出:追踪数据异步发送,不阻塞业务请求
  2. 批量发送:多个 Span 打包发送,减少网络开销
  3. 采样控制:生产环境合理设置采样率
  4. 避免大 Span:不要在 Span 中记录大量数据

常见问题

问题 1:追踪数据不完整

原因:手动创建了 RestTemplateWebClient,未使用自动配置的 Builder

解决:使用 RestTemplateBuilderWebClient.Builder Bean

问题 2:日志中没有 Trace ID

原因:未配置日志格式

解决:配置 logging.pattern.correlation

问题 3:跨服务调用没有关联

原因:上下文未传播

解决:检查是否使用了正确的 HTTP Client 配置,确保使用了自动配置的 Builder

与其他组件对比

特性Micrometer TracingSpring Cloud SleuthJaeger Client
Spring Boot 版本3.x2.x任意
维护状态活跃停止维护活跃
Tracer 支持OpenTelemetry, BraveBraveOpenTracing
社区支持已停止
学习曲线中等简单中等

小结

本章我们学习了:

  1. 链路追踪的作用:可视化调用链、性能分析、故障定位
  2. 核心概念:Trace、Span、上下文传播
  3. Micrometer Tracing 集成:与 Zipkin、Jaeger 的对接
  4. 服务间追踪:OpenFeign、WebClient、RestTemplate 的自动追踪
  5. 自定义 Span:使用 Observation API 创建自定义追踪
  6. Baggage 传播:跨服务传递上下文数据
  7. 最佳实践:采样策略、标签设计、性能优化

参考资料

练习

  1. 搭建一个包含三个服务的微服务系统,集成 Zipkin 追踪
  2. 实现一个自定义 Span,记录某个业务操作的耗时
  3. 使用 Baggage 传递用户 ID,并在日志中输出
  4. 故意在某个服务中制造错误,在 Zipkin 中定位错误发生的位置