第 18 章:链路追踪
学习目标
- 理解分布式链路追踪的核心问题
- 掌握 TraceId / SpanId 体系
- 学会用 SkyWalking / Zipkin
- 避免采样率、上下文丢失的坑
一、为什么需要链路追踪?
text
请求流转
[客户端] → [网关] → [订单] → [支付] → [库存] → [物流]
↓
某一步慢了,但只看到"总耗时 5 秒"java
// 多服务调用,排查"慢请求"困难
OrderController.order() // 100ms
→ InventoryService.reserve() // 50ms
→ PaymentService.pay() // 3500ms ← 慢!但怎么定位?二、TraceId / SpanId 模型
text
一个请求 = 一个 Trace
一个调用 = 一个 Span
TraceId: 全局唯一,贯穿所有 span
SpanId: 单个调用,树状结构
ParentSpanId: 父 spantext
Trace(abc123)
├── Span(A 网关) Parent = -
│ └── Span(B 订单) Parent = A
│ ├── Span(C 库存) Parent = B
│ └── Span(D 支付) Parent = B
└── Span(E 异步通知) Parent = A三、MDC + 日志聚合
java
// 1. 拦截器注入 TraceId
@Component
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
String traceId = ((HttpServletRequest) req).getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("traceId", traceId);
((HttpServletResponse) res).setHeader("X-Trace-Id", traceId);
try {
chain.doFilter(req, res);
} finally {
MDC.remove("traceId");
}
}
}
// 2. logback 配置
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>log
2026-01-15 10:00:00.123 [http-nio-8080-exec-1] [abc123] INFO OrderService - 创建订单
2026-01-15 10:00:00.456 [http-nio-8080-exec-1] [abc123] INFO PaymentService - 调用支付⚠️ 坑 1:MDC 只在当前线程,异步任务或线程池切换后 traceId 丢失。手动传递或用
TaskDecorator。
四、SkyWalking 实战
xml
<!-- Java Agent 启动参数 -->
-javaagent:/opt/skywalking/skywalking-agent.jar
-Dskywalking.agent.service_name=order-service
-Dskywalking.collector.backend_service=skywalking:11800java
// 1. 自动埋点:Web、JDBC、Redis、MQ
// 无需改代码,Agent 自动处理
// 2. 自定义埋点
@Trace
public void customBusinessMethod() {
ActiveSpan.tag("userId", "123");
ActiveSpan.tag("orderId", "ORD001");
}
// 3. 异步任务
@TraceCrossThread
public CompletableFuture<Order> asyncQuery() {
// 跨线程传递 trace
}text
SkyWalking UI
├── 服务拓扑
├── 慢接口分析
├── Trace 详情
├── JVM 监控
└── 数据库/Redis 监控五、Zipkin 集成
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>yaml
spring:
zipkin:
base-url: http://zipkin:9411
sender:
type: web
sleuth:
sampler:
probability: 1.0 # 采样率 100%java
// 自动埋点:RestTemplate、Feign、MQ
// 日志自动带上 traceId
// 自定义埋点
@NewSpan("custom-operation")
public void doSomething() {
// ...
}java
// 异步传递
CompletableFuture.supplyAsync(() -> {
return Tracer.create(span.context()); // 手动传递
}).thenAccept(result -> {
// 业务处理
});六、OpenTelemetry 标准
java
// 标准化 SDK,兼容 Jaeger / Zipkin / SkyWalking
Tracer tracer = OpenTelemetry.getTracer("order-service");
Span span = tracer.spanBuilder("placeOrder")
.setAttribute("userId", userId)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
span.setAttribute("orderId", orderId);
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR);
} finally {
span.end();
}七、采样率
yaml
# 全量采样影响性能
spring:
sleuth:
sampler:
probability: 0.1 # 10% 采样
# 重要接口 100% 采样
@NewSpan("critical-operation")
public void criticalMethod() {
// 强制采样
}java
// 自定义采样策略
public class CustomSampler implements Sampler {
@Override
public boolean isSampled(long traceId) {
// 错误率 100% 采样
if (hasError(traceId)) return true;
// VIP 用户 100% 采样
if (isVipUser(traceId)) return true;
// 其他 1%
return ThreadLocalRandom.current().nextDouble() < 0.01;
}
}⚠️ 坑 2:
probability=1.0调试后忘了改回 0.1,生产环境 ES 直接被日志打爆。采样率必须配置。
八、TraceId 跨服务传递
java
// 1. HTTP:自动透传 Header
// Sleuth / SkyWalking 自动处理 X-B3-TraceId
// 2. Feign 拦截器(兜底)
@Component
public class FeignTraceInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
}
}
// 3. MQ:消息头携带 TraceId
Message msg = new Message("order-topic", body);
msg.putUserProperty("X-Trace-Id", MDC.get("traceId"));
rocketMQ.send(msg);java
// 4. 消费端恢复
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener<OrderMessage> {
@Override
public void onMessage(OrderMessage msg) {
String traceId = msg.getUserProperty("X-Trace-Id");
MDC.put("traceId", traceId);
try {
orderService.handle(msg);
} finally {
MDC.remove("traceId");
}
}
}本章小结
| 体系 | 工具 |
|---|---|
| Trace | SkyWalking、Zipkin、Jaeger |
| 日志 | Elk、Loki |
| 指标 | Prometheus、Grafana |
| 标准 | OpenTelemetry |
| 关键点 | 建议 |
|---|---|
| TraceId | UUID 或雪花 |
| 采样率 | 默认 10%,错误 100% |
| 跨线程 | 手工或 TaskDecorator |
| 跨 MQ | Header 传递 |
动手练习
- MDC 集成:写
TraceIdFilter,验证日志带 traceId - SkyWalking 部署:用 Docker 起 oap + ui,启动一个应用观察
- 异步传递:用
TaskDecorator让线程池继承 MDC - MQ 传递:在消息头放 traceId,消费端恢复 MDC
下一章:第 19 章:服务监控 →