Skip to content
第 18 章 架构 ⏱ 12 分钟阅读

第 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: 父 span
text
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:11800
java
// 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");
        }
    }
}

本章小结 ​

体系工具
TraceSkyWalking、Zipkin、Jaeger
日志Elk、Loki
指标Prometheus、Grafana
标准OpenTelemetry
关键点建议
TraceIdUUID 或雪花
采样率默认 10%,错误 100%
跨线程手工或 TaskDecorator
跨 MQHeader 传递

动手练习 ​

  1. MDC 集成:写 TraceIdFilter,验证日志带 traceId
  2. SkyWalking 部署:用 Docker 起 oap + ui,启动一个应用观察
  3. 异步传递:用 TaskDecorator 让线程池继承 MDC
  4. MQ 传递:在消息头放 traceId,消费端恢复 MDC

下一章:第 19 章:服务监控 →

本站基于 VitePress 构建 · 由 StackHub 团队维护