文章目录
引言:为什么需要全局案宗号?
想象一下,你是一个侦探,正在调查一起"奶茶订单失踪案"。这个请求需要经过订单、库存、支付和物流四个部门(微服务):
🕵️♂️:“订单部,请调取一下订单ID为123的记录”
📦:“好的,订单已处理,已转交给库存部”🕵️♂️:“库存部,订单123的库存扣减了吗?”
🧾:“今天处理了8923个请求,您说的是哪一个?”🕵️♂️:“支付部,订单123的支付成功了吗?”
💳:“我这边有7564条支付记录,找不到您说的这条”
这就是没有traceId的困境:在微服务架构中,一个请求会经过多个服务,如果没有全局标识,排查问题就像大海捞针。
而traceId就是为解决这个问题而生的"全局案宗号",它能够贯穿整个请求链路,让所有相关日志都可以被串联起来。
一、Spring Cloud Sleuth 实战:快速上手
1. 添加依赖
在项目的 pom.xml
中添加以下依赖:
<dependencies>
<!-- Sleuth 自动配置核心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Zipkin 客户端(可选,用于可视化) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
</dependencies>
2. 基础配置
在 application.yml
中配置:
spring:
application:
name: order-service # 服务名称很重要,会在日志中显示
sleuth:
sampler:
probability: 1.0 # 采样率:1.0表示100%采集,生产环境建议0.1
zipkin:
base-url: http://localhost:9411 # Zipkin服务器地址
enabled: true
3. 查看效果
启动应用后,查看日志输出:
2023-05-01 10:30:25.123 INFO [order-service,3e6c5b7a5c2a7c0b,8f7d6e5a4b3c2d1e] 12504 --- [nio-8080-exec-1] c.e.order.OrderController : 创建新订单
2023-05-01 10:30:25.456 INFO [order-service,3e6c5b7a5c2a7c0b,d4e5f6a7b8c9d0e1] 12504 --- [nio-8080-exec-1] c.e.order.InventoryService : 调用库存服务
日志格式:[应用名,traceId,spanId]
- traceId:
3e6c5b7a5c2a7c0b
(全局唯一,整个链路保持不变) - spanId:
8f7d6e5a4b3c2d1e
,d4e5f6a7b8c9d0e1
(每个环节都有唯一标识)
4. 安装并启动 Zipkin
# 使用 Docker 快速启动
docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin
# 或者下载 Jar 包
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
访问 http://localhost:9411 即可看到可视化界面。
二、原理解析:traceId 的生成与传递
1. 核心概念
- Trace: 代表一个完整的请求链路,包含多个 Span
- Span: 代表链路中的一个环节,有开始和结束时间
- TraceId: 整个链路的唯一标识
- SpanId: 单个环节的唯一标识
2. 工作流程时序图
3. 源码浅析
TraceId 生成器
// 源码位置:org.springframework.cloud.sleuth.Tracer
public interface Tracer {
Span nextSpan(Span parent);
}
// 默认实现使用随机数生成TraceId
public class RandomTraceIdGenerator implements TraceIdGenerator {
@Override
public String generateTraceId() {
return RandomUtils.getRandom().nextLong() + "";
}
}
自动注入过滤器
// Servlet过滤器自动处理HTTP请求
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 从Header中提取或生成TraceId
Span span = this.tracer.nextSpan(extractOrNull(request));
try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) {
chain.doFilter(request, response);
} finally {
span.end(); // 记录结束时间
}
}
}
Feign 客户端拦截器
// Feign调用时自动传递TraceId
public class FeignTraceInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 将当前TraceId注入到Feign请求头中
if (tracer.currentSpan() != null) {
template.header("X-B3-TraceId",
tracer.currentSpan().context().traceId());
template.header("X-B3-SpanId",
tracer.currentSpan().context().spanId());
}
}
}
三、循环调用场景:traceId 如何保持一致性?
考虑这样一个场景:订单服务(A) → 库存服务(B) → 订单服务(A)
// 订单服务
@RestController
public class OrderController {
@Autowired
private InventoryService inventoryService;
@PostMapping("/order")
public Order createOrder(@RequestBody Order order) {
log.info("创建订单");
inventoryService.lockInventory(order); // 第一次调用库存服务
return order;
}
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable String id) {
return orderRepository.findById(id);
}
}
// 库存服务
@RestController
public class InventoryController {
@Autowired
private OrderService orderService;
@PostMapping("/inventory/lock")
public void lockInventory(@RequestBody InventoryLockRequest request) {
log.info("锁定库存");
// 需要回调订单服务获取详细信息
Order order = orderService.getOrder(request.getOrderId()); // 回调订单服务
// ...处理库存逻辑
}
}
即使存在循环调用,traceId 也保持一致,因为:
- 第一次调用 A→B:TraceId=ABC-123, SpanId=100
- B 接收请求:继承 TraceId=ABC-123, 创建新 SpanId=200
- B 回调 A→B:自动携带 TraceId=ABC-123, ParentSpanId=200, 新 SpanId=300
- A 处理回调:继承 TraceId=ABC-123, SpanId=300
四、常见问题与解决方案
1. 线程池中 traceId 丢失问题
问题现象:异步任务中无法获取 traceId
解决方案:使用 Sleuth 提供的包装器
// 错误方式 - traceId会丢失
@Async
public void asyncProcess() {
log.info("异步处理"); // 这里没有traceId
}
// 正确方式 - 使用TraceableExecutorService
@Bean
public Executor traceableExecutor() {
return new LazyTraceExecutor(AsyncConfigurer().getAsyncExecutor());
}
@Async
public void asyncProcess() {
log.info("异步处理"); // 保持traceId
}
2. 手动管理 span
@Autowired
private Tracer tracer;
public void complexProcess() {
// 创建新span
Span newSpan = tracer.nextSpan().name("complexOperation").start();
try (Tracer.SpanInScope ws = tracer.withSpan(newSpan)) {
// 业务逻辑
log.info("复杂操作");
} finally {
newSpan.end(); // 必须手动结束
}
}
3. 自定义采样策略
@Bean
public Sampler customSampler() {
return new Sampler() {
@Override
public boolean isSampled(Span span) {
// 只采样重要请求
return span.tags().get("important") != null;
}
};
}
五、最佳实践
- 日志配置优化:在 logback-spring.xml 中配置 traceId 输出
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%X{traceId:-},%X{spanId:-}] %msg%n</pattern>
网关统一入口:确保 API 网关也集成 Sleuth
非 HTTP 协议支持:对消息队列、定时任务等也要集成
生产环境采样率:设置 probability=0.1 减少性能影响
业务日志关联:在业务记录中也存储 traceId
public void saveOrder(Order order) {
order.setTraceId(tracer.currentSpan().context().traceId());
orderRepository.save(order);
}
总结
Spring Cloud Sleuth 通过自动化的 traceId 管理,让分布式系统日志跟踪变得简单高效。记住几个关键点:
- 添加依赖即可获得基础能力
- traceId 自动传递,无需手动处理
- Zipkin 提供可视化,方便问题排查
- 注意异步场景中的上下文传递
现在,当你的微服务出现问题时,你就可以像神探一样,通过 traceId 这个"全局案宗号"轻松追踪整个请求链路,快速定位问题所在!
真正的强大不在于代码多复杂,而在于用简单的方案解决复杂的问题。Spring Cloud Sleuth 正是这样的工具,它让分布式系统跟踪变得如此简单,以至于你几乎感受不到它的存在,直到你需要它的时候。