副标题:Zipkin、Skywalking、Jaeger三剑客,谁能拯救你的调用链迷宫?🗺️
🎬 开场:一个令人头疼的故障
凌晨3点,张工被电话吵醒 ☎️:
老板:用户反馈下单很慢,赶紧查!
张工:好的!(打开监控系统)
系统架构:
用户 → 网关 → 订单服务 → 商品服务 → 库存服务 → 支付服务
↓ ↓ ↓ ↓
用户服务 优惠服务 积分服务 通知服务
张工:😱 10个微服务,到底哪个慢了?
每个服务的日志都在不同的机器上
请求ID也不统一
完全不知道从哪里查起...
结果:排查了2小时才定位到是优惠券服务数据库连接池满了
张工:😭 要是有个东西能看到整个调用链路就好了...
这就是我们今天要讲的:分布式链路追踪!
📚 什么是链路追踪?
官方定义
分布式链路追踪(Distributed Tracing):在分布式系统中,追踪一个请求从进入系统到返回的完整路径,记录经过的所有服务节点、调用关系、耗时等信息。
通俗解释
就像快递追踪 📦:
你在淘宝下单买了一本书
传统监控(只能看到某个环节):
- 仓库:✅ 已发货
- 物流:❓ 不知道
- 配送:❓ 不知道
链路追踪(看到完整路径):
14:00 [北京仓库] 已发货
14:30 [北京分拨中心] 已到达
15:00 [北京分拨中心] 已发出
18:00 [上海转运中心] 已到达 ⚠️ 等了3小时!
18:10 [上海转运中心] 已发出
20:00 [上海配送站] 派送中
21:00 [你家门口] 已签收
一眼就能看出:上海转运中心效率低!
🔑 核心概念
1️⃣ Trace(追踪)
一次完整的请求链路
Trace ID: 550e8400-e29b-41d4-a716-446655440000
用户下单请求 → 经过多个服务 → 返回结果
↑ ↓
└──────────── 一个Trace ───────┘
生活比喻 🎬:
- Trace = 一部完整的电影
- 你从买票进场到散场离开的整个过程
2️⃣ Span(跨度)
一个服务内的操作单元
Trace
└── Span (订单服务)
├── Span (查询用户)
├── Span (查询商品)
├── Span (扣减库存)
└── Span (创建订单)
Span的属性:
public class Span {
private String traceId; // 追踪ID
private String spanId; // 跨度ID
private String parentSpanId; // 父Span ID
private String serviceName; // 服务名
private String operationName; // 操作名
private long startTime; // 开始时间
private long duration; // 持续时间
private Map<String, String> tags; // 标签
private List<Log> logs; // 日志
}
生活比喻 🎬:
- Span = 电影中的一个场景
- 每个场景有开始时间、结束时间、演员、台词等
3️⃣ Annotation(注解)
Span中的关键事件
Span生命周期:
CS (Client Send) → 客户端发送请求
SR (Server Receive) → 服务端接收请求
SS (Server Send) → 服务端返回响应
CR (Client Receive) → 客户端接收响应
时间线:
CS ───────────→ SR ────处理──── SS ───────→ CR
│ │ │ │
└─ 网络耗时 ─┘ └─ 网络耗时 ─┘
└──── 服务处理时间 ────┘
可视化理解
Trace: 用户下单 (TraceId: abc123)
│
├── Span1: 订单服务 (100ms)
│ │
│ ├── Span2: 查询用户服务 (20ms)
│ │
│ ├── Span3: 查询商品服务 (30ms)
│ │ │
│ │ └── Span4: 查询数据库 (25ms) ⚠️ 慢!
│ │
│ └── Span5: 扣减库存服务 (40ms)
│
└── Span6: 发送MQ消息 (10ms)
总耗时:100ms
瓶颈:Span4查询数据库慢
🏗️ 链路追踪的实现原理
核心机制:上下文传播
服务A 服务B 服务C
│ │ │
│ 生成TraceId │ │
│ TraceId: T1 │ │
│ SpanId: S1 │ │
│ │ │
├─── HTTP请求 ─────────→ │ │
│ Header: │ │
│ X-B3-TraceId: T1 │ 解析Header │
│ X-B3-SpanId: S2 │ TraceId: T1 │
│ X-B3-ParentSpanId:S1│ SpanId: S2 │
│ │ │
│ ├─── HTTP请求 ─────────→ │
│ │ Header: │
│ │ X-B3-TraceId: T1 │
│ │ X-B3-SpanId: S3 │
│ │ X-B3-ParentSpanId:S2│
│ │ │
关键点:
- 生成TraceId:第一个服务生成全局唯一的TraceId
- 传递上下文:通过HTTP Header传递TraceId、SpanId
- 记录Span:每个服务记录自己的Span信息
- 上报数据:异步上报到追踪系统
代码示例:手动实现
// 1. 请求入口:生成或提取TraceId
@Component
public class TracingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 尝试从Header中获取TraceId
String traceId = httpRequest.getHeader("X-B3-TraceId");
String spanId = httpRequest.getHeader("X-B3-SpanId");
String parentSpanId = httpRequest.getHeader("X-B3-ParentSpanId");
// 如果没有TraceId,说明是链路起点,生成新的
if (traceId == null) {
traceId = generateTraceId();
spanId = generateSpanId();
parentSpanId = null;
} else {
// 如果有TraceId,生成新的SpanId
parentSpanId = spanId;
spanId = generateSpanId();
}
// 存入ThreadLocal
TraceContext.setTraceId(traceId);
TraceContext.setSpanId(spanId);
TraceContext.setParentSpanId(parentSpanId);
// 记录Span开始
Span span = Span.builder()
.traceId(traceId)
.spanId(spanId)
.parentSpanId(parentSpanId)
.serviceName("order-service")
.operationName(httpRequest.getRequestURI())
.startTime(System.currentTimeMillis())
.build();
try {
chain.doFilter(request, response);
} finally {
// 记录Span结束
span.setDuration(System.currentTimeMillis() - span.getStartTime());
// 上报Span
SpanReporter.report(span);
// 清理ThreadLocal
TraceContext.clear();
}
}
}
// 2. 远程调用:传递TraceId
@Component
public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) {
// 从ThreadLocal获取TraceId
String traceId = TraceContext.getTraceId();
String parentSpanId = TraceContext.getSpanId();
String spanId = generateSpanId();
// 添加到请求头
request.getHeaders().add("X-B3-TraceId", traceId);
request.getHeaders().add("X-B3-SpanId", spanId);
request.getHeaders().add("X-B3-ParentSpanId", parentSpanId);
// 记录调用
Span span = Span.builder()
.traceId(traceId)
.spanId(spanId)
.parentSpanId(parentSpanId)
.serviceName("order-service")
.operationName("HTTP " + request.getMethod() + " " + request.getURI())
.startTime(System.currentTimeMillis())
.build();
try {
ClientHttpResponse response = execution.execute(request, body);
span.setDuration(System.currentTimeMillis() - span.getStartTime());
SpanReporter.report(span);
return response;
} catch (Exception e) {
span.setError(true);
span.addTag("error", e.getMessage());
SpanReporter.report(span);
throw e;
}
}
}
🥊 三大链路追踪系统对比
Zipkin(Twitter出品)🐦
架构图
应用程序
↓
┌─────────┐
│ Reporter │ ← 数据上报(HTTP/Kafka)
└────┬────┘
↓
┌──────────────┐
│ Collector │ ← 数据收集器
└──────┬───────┘
↓
┌──────────────┐
│ Storage │ ← 存储(MySQL/ES/Cassandra)
└──────┬───────┘
↓
┌──────────────┐
│ UI │ ← Web界面
└──────────────┘
特点
优点 ✅:
- 历史悠久,生态成熟
- 轻量级,部署简单
- 支持多种语言(Java/Go/Python等)
- 社区活跃
缺点 ❌:
- 功能相对简单
- UI不够美观
- 不支持告警
- 性能监控能力弱
快速上手
1. 启动Zipkin服务
# 使用Docker启动
docker run -d -p 9411:9411 openzipkin/zipkin
2. Spring Boot集成
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
spring:
application:
name: order-service
zipkin:
base-url: http://localhost:9411/ # Zipkin地址
sleuth:
sampler:
probability: 1.0 # 采样率100%(生产环境建议0.1)
3. 查看链路
访问:http://localhost:9411
界面功能:
├── 查找追踪 (Find Traces)
├── 依赖分析 (Dependencies)
└── 服务列表 (Services)
4. 自定义Span
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public Order createOrder(OrderRequest request) {
// 创建自定义Span
Span span = tracer.nextSpan().name("createOrder").start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
// 添加标签
span.tag("orderId", request.getOrderId().toString());
span.tag("userId", request.getUserId().toString());
// 业务逻辑
Order order = doCreateOrder(request);
// 添加事件
span.annotate("order.created");
return order;
} catch (Exception e) {
span.tag("error", e.getMessage());
throw e;
} finally {
span.finish();
}
}
}
SkyWalking(Apache项目)🦅
架构图
应用程序(探针Agent)
↓
┌──────────────┐
│ SkyWalking │
│ Agent │ ← 字节码增强,无侵入
└──────┬───────┘
↓
┌──────────────┐
│ OAP │ ← 后端分析平台
│ (Collector) │ (Observability Analysis Platform)
└──────┬───────┘
↓
┌──────────────┐
│ Storage │ ← 存储(ES/H2)
└──────┬───────┘
↓
┌──────────────┐
│ UI │ ← Web界面
└──────────────┘
核心优势:无侵入
Zipkin/Jaeger:
需要在代码中引入依赖 ❌
需要手动埋点 ❌
SkyWalking:
无需修改代码!✅
启动时添加JVM参数即可:
java -javaagent:/path/to/skywalking-agent.jar -jar app.jar
特点
优点 ✅:
- 无侵入:字节码增强,无需修改代码
- 功能强大:支持链路、指标、日志、告警
- UI美观:界面友好,功能丰富
- 国产化:中文文档完善
- 性能监控:JVM、数据库、MQ等
缺点 ❌:
- 架构相对复杂
- 资源占用较高
- Agent有性能开销
快速上手
1. 下载SkyWalking
# 下载并解压
wget https://archive.apache.org/dist/skywalking/8.9.1/apache-skywalking-apm-8.9.1.tar.gz
tar -zxvf apache-skywalking-apm-8.9.1.tar.gz
# 目录结构
skywalking/
├── agent/ # Agent探针
├── bin/ # 启动脚本
├── config/ # 配置文件
└── oap-libs/ # OAP服务
2. 启动OAP和UI
# 启动OAP服务
cd bin
./oapService.sh
# 启动UI(另一个终端)
./webappService.sh
# 访问:http://localhost:8080
3. 应用集成Agent
# 方式1:启动参数
java -javaagent:/path/to/skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-Dskywalking.collector.backend_service=127.0.0.1:11800 \
-jar order-service.jar
# 方式2:环境变量
export SW_AGENT_NAME=order-service
export SW_AGENT_COLLECTOR_BACKEND_SERVICES=127.0.0.1:11800
java -javaagent:/path/to/skywalking-agent.jar -jar order-service.jar
4. 自定义监控
@Service
public class OrderService {
/**
* 使用注解标记
*/
@Trace // SkyWalking注解
@Tags({
@Tag(key = "param", value = "arg[0]"),
@Tag(key = "result", value = "returnedObj")
})
public Order createOrder(OrderRequest request) {
return doCreateOrder(request);
}
/**
* 手动创建Span
*/
public void manualTrace() {
AbstractSpan span = ContextManager.createLocalSpan("custom-operation");
try {
span.tag("key", "value");
span.log(System.currentTimeMillis(), "event");
// 业务逻辑
} catch (Exception e) {
span.errorOccurred();
span.log(e);
} finally {
ContextManager.stopSpan();
}
}
}
5. 告警配置
# config/alarm-settings.yml
rules:
# 服务响应时间告警
service_resp_time_rule:
metrics-name: service_resp_time
op: ">"
threshold: 1000 # 超过1秒
period: 10 # 10分钟内
count: 3 # 触发3次
message: "服务响应时间过长: {name}"
# 服务错误率告警
service_percent_rule:
metrics-name: service_percent
op: "<"
threshold: 95 # 成功率低于95%
period: 10
count: 2
message: "服务成功率过低: {name}"
# 告警通道
webhooks:
- url: http://your-webhook-url
Jaeger(Uber出品)🚗
架构图
应用程序
↓
┌─────────┐
│ Agent │ ← 本地代理(UDP上报)
└────┬────┘
↓
┌──────────────┐
│ Collector │ ← 收集器(接收、处理、存储)
└──────┬───────┘
↓
┌──────────────┐
│ Storage │ ← 存储(Cassandra/ES)
└──────┬───────┘
↓
┌──────────────┐
│ Query │ ← 查询服务
└──────┬───────┘
↓
┌──────────────┐
│ UI │ ← Web界面
└──────────────┘
特点
优点 ✅:
- CNCF毕业项目,云原生首选
- 支持OpenTracing标准
- 性能优异
- 采样策略灵活
- 适配性强(支持多种存储)
缺点 ❌:
- 架构复杂(组件多)
- 部署成本高
- 国内资料少
快速上手
1. 启动Jaeger(All-in-One)
# Docker启动(适合开发测试)
docker run -d \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
jaegertracing/all-in-one:latest
# 访问UI:http://localhost:16686
2. Spring Boot集成
<dependency>
<groupId>io.opentracing.contrib</groupId>
<artifactId>opentracing-spring-jaeger-cloud-starter</artifactId>
</dependency>
opentracing:
jaeger:
service-name: order-service
udp-sender:
host: localhost
port: 6831
probabilistic-sampler:
sampling-rate: 1.0 # 采样率100%
3. 使用OpenTracing API
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public Order createOrder(OrderRequest request) {
// 创建Span
Span span = tracer.buildSpan("createOrder").start();
try (Scope scope = tracer.scopeManager().activate(span)) {
// 添加标签
span.setTag("orderId", request.getOrderId());
span.setTag("userId", request.getUserId());
// 添加日志
Map<String, Object> fields = new HashMap<>();
fields.put("event", "order.validation");
fields.put("message", "订单校验通过");
span.log(fields);
// 业务逻辑
Order order = doCreateOrder(request);
return order;
} catch (Exception e) {
Tags.ERROR.set(span, true);
span.log(Collections.singletonMap("event", "error"));
throw e;
} finally {
span.finish();
}
}
}
4. 采样策略
// 概率采样:10%的请求被追踪
SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv()
.withType("probabilistic")
.withParam(0.1);
// 速率限制采样:每秒最多追踪10个请求
SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv()
.withType("ratelimiting")
.withParam(10);
// 自定义采样
SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv()
.withType("remote")
.withManagerHostPort("localhost:5778");
⚔️ 三大系统详细对比
对比表
| 维度 | Zipkin | SkyWalking | Jaeger |
|---|---|---|---|
| 侵入性 | 需要SDK | ⭐⭐⭐⭐⭐ 无侵入(Agent) | 需要SDK |
| 性能开销 | 低 | 中(Agent有开销) | 低 |
| 功能丰富度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| UI体验 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 告警功能 | ❌ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 社区活跃度 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 部署难度 | ⭐⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐ 复杂 |
| 中文文档 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 适用场景 | 简单追踪 | 全能型 | 云原生 |
性能对比
吞吐量测试(Spans/秒):
Zipkin: 50000 ⭐⭐⭐
SkyWalking: 45000 ⭐⭐⭐
Jaeger: 60000 ⭐⭐⭐⭐
存储空间占用(相同数据量):
Zipkin: 1GB ⭐⭐⭐
SkyWalking: 1.2GB ⭐⭐
Jaeger: 0.8GB ⭐⭐⭐⭐
Agent内存占用:
Zipkin: 无Agent -
SkyWalking: ~50MB ⭐⭐⭐
Jaeger: 无Agent -
🤔 如何选择?
决策树 🌲
开始选择链路追踪系统
│
├─ 不想修改代码?
│ └─ 是 → SkyWalking ⭐⭐⭐⭐⭐
│
├─ 需要告警功能?
│ └─ 是 → SkyWalking ⭐⭐⭐⭐⭐
│
├─ 云原生(K8s)环境?
│ └─ 是 → Jaeger ⭐⭐⭐⭐⭐
│
├─ 快速上手,简单场景?
│ └─ 是 → Zipkin ⭐⭐⭐⭐
│
├─ 需要全面的APM功能?
│ └─ 是 → SkyWalking ⭐⭐⭐⭐⭐
│
└─ 团队熟悉OpenTracing?
└─ 是 → Jaeger ⭐⭐⭐⭐
典型场景推荐
场景1:中小型Java项目
需求:
- 快速上手
- 不想大改代码
- 基本的链路追踪功能
推荐: Zipkin ⭐⭐⭐⭐
理由:
1. 集成超级简单(引入依赖即可)
2. 轻量级,资源占用少
3. 满足基本需求
场景2:大型微服务项目
需求:
- 100+微服务
- 需要完整的APM功能
- 需要告警
- 不想改代码
推荐: SkyWalking ⭐⭐⭐⭐⭐
理由:
1. 无侵入,Agent模式
2. 功能全面(链路+指标+日志)
3. 告警完善
4. 中文文档友好
场景3:云原生项目
需求:
- Kubernetes部署
- 多语言(Java/Go/Python)
- 标准化(OpenTracing)
推荐: Jaeger ⭐⭐⭐⭐⭐
理由:
1. CNCF毕业项目
2. K8s原生支持
3. OpenTracing标准
4. 性能优异
💡 最佳实践
1. 采样策略
/**
* 生产环境不要全量采样!
*/
// ❌ 错误:100%采样
spring.sleuth.sampler.probability=1.0
// ✅ 正确:根据流量调整
// 流量小(<1000 QPS): 50%
spring.sleuth.sampler.probability=0.5
// 流量中(1000-10000 QPS): 10%
spring.sleuth.sampler.probability=0.1
// 流量大(>10000 QPS): 1%
spring.sleuth.sampler.probability=0.01
/**
* 自定义采样策略
*/
@Component
public class CustomSampler extends Sampler {
@Override
public boolean isSampled(long traceId) {
// 重要接口100%采样
if (isImportantApi()) {
return true;
}
// 错误请求100%采样
if (hasError()) {
return true;
}
// 其他请求10%采样
return traceId % 10 == 0;
}
}
2. 异步上报
/**
* 不要同步上报,影响性能!
*/
@Configuration
public class TracingConfig {
@Bean
public AsyncReporter<Span> spanReporter() {
return AsyncReporter.builder(sender)
.closeTimeout(1, TimeUnit.SECONDS)
.messageMaxBytes(1000000) // 1MB
.messageTimeout(1, TimeUnit.SECONDS)
.queuedMaxSpans(10000) // 队列最大10000
.queuedMaxBytes(10000000) // 队列最大10MB
.build();
}
}
3. 敏感信息脱敏
@Component
public class SensitiveDataFilter implements SpanHandler {
@Override
public boolean end(TraceContext context, MutableSpan span, Cause cause) {
// 脱敏手机号
span.tags().replaceAll((k, v) -> {
if (k.contains("phone")) {
return maskPhone(v);
}
return v;
});
return true;
}
private String maskPhone(String phone) {
if (phone != null && phone.length() == 11) {
return phone.substring(0, 3) + "****" + phone.substring(7);
}
return phone;
}
}
4. 链路与日志关联
@Slf4j
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public Order createOrder(OrderRequest request) {
// 获取TraceId
String traceId = tracer.currentSpan().context().traceIdString();
// 记录日志时带上TraceId
MDC.put("traceId", traceId);
log.info("创建订单: {}", request);
try {
return doCreateOrder(request);
} finally {
MDC.remove("traceId");
}
}
}
日志格式:
# logback-spring.xml
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%X{traceId}] %logger{36} - %msg%n
</pattern>
# 输出效果
2024-01-01 10:00:00 [http-nio-8080-exec-1] INFO [550e8400-e29b-41d4-a716] c.e.OrderService - 创建订单: ...
🎯 面试高频问题
Q1:链路追踪如何保证低开销?
A:
1. 采样策略:
不是所有请求都追踪
生产环境通常采样1%-10%
2. 异步上报:
Span数据异步批量上报
不阻塞业务线程
3. 本地聚合:
Agent本地聚合数据
减少网络传输
4. 存储优化:
使用时序数据库
数据压缩
定期清理
Q2:TraceId如何生成和传递?
A:
生成:
// 方式1:UUID
String traceId = UUID.randomUUID().toString();
// 方式2:雪花算法
String traceId = SnowflakeIdWorker.generateId();
// 方式3:时间戳+随机数
String traceId = System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt();
传递:
HTTP: 通过Header传递
X-B3-TraceId: xxx
X-B3-SpanId: yyy
X-B3-ParentSpanId: zzz
gRPC: 通过Metadata传递
MQ: 通过Message Header传递
Q3:如何追踪异步调用?
A:
@Service
public class OrderService {
@Autowired
private Tracer tracer;
@Autowired
private Executor executor;
public void asyncProcess() {
// 获取当前Span
Span parentSpan = tracer.currentSpan();
executor.execute(() -> {
// 创建新Span,指定父Span
Span span = tracer.nextSpan(
TraceContextOrSamplingFlags.create(parentSpan.context())
).start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
// 异步业务逻辑
doAsyncWork();
} finally {
span.finish();
}
});
}
}
🎉 总结
核心要点 ✨
-
链路追踪核心概念:
- Trace:完整链路
- Span:操作单元
- Annotation:关键事件
-
三大系统特点:
- Zipkin:简单轻量
- SkyWalking:功能全面
- Jaeger:云原生首选
-
选型建议:
- 简单场景 → Zipkin
- 无侵入需求 → SkyWalking
- 云原生 → Jaeger
记忆口诀 📝
链路追踪三要素,
Trace、Span和Annotation。
TraceId全局唯一,
SpanId记录每一步。
Zipkin简单易上手,
适合小型快速搞。
SkyWalking功能强,
无侵入来帮大忙。
Jaeger云原生,
K8s首选没商量。
采样策略要合理,
异步上报性能好。
敏感信息要脱敏,
日志关联好排查!
📚 参考资料
最后送你一句话:
"在微服务的世界里,没有链路追踪,就像在黑夜里开车没有车灯。"
愿你的链路清晰可见,问题一眼定位! 🔍✨
表情包时间 🎭
没有链路追踪前:
😱 服务挂了,不知道哪个服务的问题
🤯 请求慢了,不知道慢在哪里
😭 排查问题,凌晨3点还在加班
有了链路追踪后:
🔍 一眼看出问题在哪个服务
⚡ 瞬间定位性能瓶颈
😴 安心睡觉,第二天再处理
程序员的幸福生活从链路追踪开始!