分布式链路追踪
在微服务架构中,一个请求可能经过多个服务的处理,每个服务可能还调用其他服务。当出现性能问题或错误时,如何快速定位是哪个服务出了问题?分布式链路追踪(Distributed Tracing)就是解决这个问题的核心技术。
为什么需要链路追踪?
微服务架构的挑战
假设一个电商系统的下单流程:
┌─────────────────────────────────────────────────────────────────────┐
│ 下单请求调用链 │
│ │
│ 客户端 │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ API网关 │ 100ms │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 订单服务 │ 200ms ───→ 库存服务 (150ms) │
│ └──────┬───────┘ └──→ 商品服务 (80ms) │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 支付服务 │ 300ms ───→ 账户服务 (200ms) │
│ └──────────────┘ └──→ 银行网关 (500ms) │
│ │
│ 总耗时:用户等待了多久?哪个服务最慢? │
│ 出错时:错误发生在哪个服务? │
└─────────────────────────────────────────────────────────────────────┘
核心问题:
- 一个请求经过了哪些服务?
- 每个服务处理耗时多少?
- 哪个服务是性能瓶颈?
- 错误发生在哪个服务、哪行代码?
链路追踪的作用
链路追踪可以帮我们:
- 可视化调用链:清晰展示请求在服务间的流转路径
- 性能分析:识别慢服务、慢接口,进行针对性优化
- 故障定位:快速定位错误发生的服务和代码位置
- 依赖分析:了解服务间的依赖关系
- 容量规划:基于调用数据进行容量评估
核心概念
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 + Zipkin | OpenTelemetry 是可观测性的行业标准,Zipkin 作为后端存储 |
| OpenTelemetry + OTLP | 使用 OpenTelemetry Protocol 发送到支持 OTLP 的后端 |
| Brave + Zipkin | OpenZipkin 的 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.method、http.status_code、order.type |
| 高基数(High Cardinality) | 值的种类很多,不适合聚合 | user.id、order.id、trace.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.methodhttp.status_codehttp.routeservice.namedb.operation
高基数标签(不适合聚合):
user.idorder.idtrace.id
性能考虑
- 异步导出:追踪数据异步发送,不阻塞业务请求
- 批量发送:多个 Span 打包发送,减少网络开销
- 采样控制:生产环境合理设置采样率
- 避免大 Span:不要在 Span 中记录大量数据
常见问题
问题 1:追踪数据不完整
原因:手动创建了 RestTemplate 或 WebClient,未使用自动配置的 Builder
解决:使用 RestTemplateBuilder 或 WebClient.Builder Bean
问题 2:日志中没有 Trace ID
原因:未配置日志格式
解决:配置 logging.pattern.correlation
问题 3:跨服务调用没有关联
原因:上下文未传播
解决:检查是否使用了正确的 HTTP Client 配置,确保使用了自动配置的 Builder
与其他组件对比
| 特性 | Micrometer Tracing | Spring Cloud Sleuth | Jaeger Client |
|---|---|---|---|
| Spring Boot 版本 | 3.x | 2.x | 任意 |
| 维护状态 | 活跃 | 停止维护 | 活跃 |
| Tracer 支持 | OpenTelemetry, Brave | Brave | OpenTracing |
| 社区支持 | 强 | 已停止 | 强 |
| 学习曲线 | 中等 | 简单 | 中等 |
小结
本章我们学习了:
- 链路追踪的作用:可视化调用链、性能分析、故障定位
- 核心概念:Trace、Span、上下文传播
- Micrometer Tracing 集成:与 Zipkin、Jaeger 的对接
- 服务间追踪:OpenFeign、WebClient、RestTemplate 的自动追踪
- 自定义 Span:使用 Observation API 创建自定义追踪
- Baggage 传播:跨服务传递上下文数据
- 最佳实践:采样策略、标签设计、性能优化
参考资料
练习
- 搭建一个包含三个服务的微服务系统,集成 Zipkin 追踪
- 实现一个自定义 Span,记录某个业务操作的耗时
- 使用 Baggage 传递用户 ID,并在日志中输出
- 故意在某个服务中制造错误,在 Zipkin 中定位错误发生的位置