【架构实战】链路追踪与可观测性实践:从日志孤岛到全链路可见
分布式系统出问题的时候,最让人崩溃的不是"服务挂了",而是"不知道哪里挂了"。一个请求经过网关→认证→业务→数据库→缓存→消息队列,涉及七八个服务,任何一个环节出问题都可能导致最终响应慢或报错。但日志散落在各个服务里,查一个问题要在多个终端之间来回跳转,效率极低。
链路追踪(Distributed Tracing)解决的就是这个问题------给每个请求打上唯一ID,全程记录调用链路,让"请求去了哪里、卡在哪里"一目了然。这篇从原理到实践,把可观测性体系讲清楚。
一、可观测性的三大支柱
可观测性(Observability)不等于监控,它包括三个维度:
1. Metrics(指标):聚合后的数值,如QPS、延迟P99、CPU使用率。回答"系统健康吗"。
2. Logs(日志):离散的事件记录,如ERROR日志、用户操作日志。回答"发生了什么"。
3. Traces(链路追踪):请求在系统中的完整流转路径。回答"为什么慢/失败了"。
三者的关系:Metrics发现问题(告警),Traces定位问题(根因),Logs验证细节(上下文)。缺任何一环都不完整。
二、链路追踪核心概念
2.1 Trace与Span
- Trace:一次完整的请求调用链,从入口到出口,所有服务和组件的调用串联起来;
- Span:Trace中的一个节点,代表一个操作(如"查询用户信息"、"调用支付接口")。每个Span记录:操作名、开始时间、结束时间、标签(Tags)、关联的子Span。
一个典型的Trace长这样:
Trace ID: abc123
└── Span: [HTTP] GET /api/orders 0ms - 850ms
├── Span: [MySQL] SELECT orders 50ms - 200ms
├── Span: [Redis] GET cache 10ms - 15ms
└── Span: [HTTP] POST /pay 220ms - 600ms
└── Span: [gRPC] PaymentSvc 240ms - 580ms
2.2 TraceContext的传播
链路追踪的关键是TraceContext在服务间的传递 。入口服务生成TraceID和SpanID,后续每次服务调用,通过HTTP Header(通常是traceparent、x-b3-traceid)或gRPC Metadata把Context往下游传。下游服务提取Header,继续记录自己的Span,形成完整的调用链。
主流标准有两个:
- W3C TraceContext (新标准):
traceparent: 00-<traceid>-<spanid>-<flags> - B3 Propagation (Zipkin风格):
X-B3-TraceId、X-B3-SpanId
三、选型:Jaeger vs Zipkin vs SkyWalking vs OpenTelemetry
| 方案 | 特点 | 适用场景 |
|---|---|---|
| Zipkin | 轻量,最早的追踪系统,社区成熟 | 简单场景,快速上手 |
| Jaeger | CNCF项目,K8s友好,支持多种存储 | 云原生,推荐 |
| SkyWalking | 国产 APM,UI强大,集成Metrics/Logs | 大型企业,国产化要求 |
| OpenTelemetry | CNCF标准,统一数据采集,支持多后端 | 推荐,未来趋势 |
强烈推荐用OpenTelemetry(OTel):它是CNCF的观测标准,融合了OpenTracing和OpenCensus,统一了Traces/Metrics/Logs的采集。一个SDK采集,三种数据全部覆盖,后端可以接Jaeger、Zipkin、Prometheus、Grafana等任意组合。
四、Spring Boot + OpenTelemetry实战
4.1 引入依赖
xml
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
4.2 配置application.yml
yaml
otel:
exporter:
otlp:
endpoint: http://otel-collector:4317 # OTel Collector地址
service:
name: order-service
4.3 手动埋点(关键操作)
java
// 方式1:自动埋点(Spring Boot Web自动拦截)
// 引入starter后,所有HTTP请求自动追踪
// 方式2:手动埋点(自定义业务逻辑)
import io.opentelemetry.api.trace.Tracer;
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(Tracer tracer) {
this.tracer = tracer;
}
public OrderResult createOrder(OrderDTO dto) {
// 开始一个Span
Span span = tracer.spanBuilder("createOrder")
.setAttribute("user.id", dto.getUserId())
.setAttribute("order.amount", dto.getAmount())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 业务逻辑
validateOrder(dto);
deductStock(dto);
Order order = saveOrder(dto);
span.setAttribute("order.id", order.getId());
span.setAttribute("order.status", "success");
return order;
} catch (Exception e) {
span.recordException(e);
span.setAttribute("order.status", "failed");
throw e;
} finally {
span.end();
}
}
}
4.4 异步任务的追踪
异步任务(如线程池、消息队列)是链路追踪的难点------子任务会创建新的Span,但需要正确关联到父Trace:
java
// 使用Context.current()保持链路关联
Span parentSpan = Span.current();
CompletableFuture.supplyAsync(() -> {
// 在子线程中继续父Span的链路
try (Scope scope = parentSpan.makeCurrent()) {
return sendNotification(orderId);
}
}, asyncExecutor);
消息队列的追踪也一样,消费者需要从MQ Header里提取TraceContext:
java
@RabbitListener(queues = "order.notify")
public void handleOrderNotify(Message msg) {
// 从消息Header中提取TraceContext
Context extracted = GlobalOpenTelemetry.getPropagators()
.getTextMapPropagator()
.extract(Context.current(), msg, GETTER);
try (Scope scope = extracted.makeCurrent()) {
// 处理消息,自动关联到原始Trace
processNotification(msg);
}
}
五、K8s中部署Jaeger + OpenTelemetry Collector
追踪数据量大,不能直接让应用发往Jaeger,需要经过Collector做聚合、采样、转发。
5.1 OTel Collector的Deployment配置
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
namespace: observability
spec:
replicas: 1
template:
spec:
containers:
- name: otelcol
image: otel/opentelemetry-collector-contrib:latest
args:
- "--config=/etc/otelcol-contrib/config.yaml"
ports:
- containerPort: 4317 # OTLP gRPC
- containerPort: 4318 # OTLP HTTP
- containerPort: 8888 # Prometheus metrics
volumeMounts:
- name: otel-config
mountPath: /etc/otelcol-contrib
volumes:
- name: otel-config
configMap:
name: otel-collector-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-config
namespace: observability
data:
config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
jaeger:
endpoint: jaeger-collector:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [jaeger]
5.2 部署Jaeger
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
namespace: observability
spec:
replicas: 1
selector:
matchLabels:
app: jaeger
template:
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:latest
env:
- name: COLLECTOR_OTLP_ENABLED
value: "true"
ports:
- containerPort: 16686 # Jaeger UI
- containerPort: 14250 # gRPC (Collector)
- containerPort: 14268 # Thrift (legacy)
---
apiVersion: v1
kind: Service
metadata:
name: jaeger
namespace: observability
spec:
type: NodePort
ports:
- port: 16686
targetPort: 16686
nodePort: 31686
selector:
app: jaeger
部署后访问http://<node>:31686打开Jaeger UI,输入服务名或TraceID即可查询。
六、采样策略:别让追踪数据把你淹没
生产环境请求量极大,100%采样会把存储打爆,也推高计算成本。采样策略是关键。
6.1 常见采样策略
1. 固定采样(Probabilistic)
yaml
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10 # 采样10%
2. 尾部采样(Tail-Based Sampling)
这是最实用的策略------先收集所有Trace,请求结束后根据结果决定是否保留。只保留慢请求(>1s)、错误请求、有特定标签的请求,极大减少存储量:
yaml
processors:
tail_sampling:
decision_wait: 10s
policies:
# 保留所有错误请求
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
# 保留超过1秒的慢请求
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 1000
# 保留特定接口的请求
- name: payment-policy
type: string_attribute
string_attribute:
key: http.route
values: ["/api/pay/*"]
# 兜底:10%采样
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
6.2 服务级别的采样配置
不同服务请求量差异很大,可以按服务配置采样率:
yaml
# 低流量服务100%采样,高流量服务1%采样
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
配合Prometheus自动发现,让高频服务自动降低采样率。
七、可视化:让数据说话
链路追踪的最终价值在可视化。Jaeger提供:
- Trace视图:查看单个请求的完整调用链,每个Span的耗时、状态一目了然;
- 服务依赖图:Service Graph,自动从Trace数据中生成服务依赖关系图;
- 延迟分析:看P50/P95/P99的Span耗时分布,找出性能瓶颈。
bash
# 查询最近10分钟内超过2秒的慢Trace
jaeger-cli traces --limit=20 \
--service=order-service \
--lookback=10m \
--min-duration=2s
配合Grafana可以做成仪表盘,把Metrics(QPS、延迟)+ Traces(慢Trace详情)放在同一张报表里:
json
// Grafana面板配置(Jaeger数据源)
{
"datasource": "Jaeger",
"query": {
"service": "order-service",
"operation": "POST /api/orders",
"limit": 20
}
}
八、与Metrics、Logs的联动
可观测性的真正威力在于三者联动。OpenTelemetry + Grafana + Loki(存Logs)+ Prometheus(存Metrics)+ Jaeger(存Traces)是目前最主流的可观测性技术栈。
告警触发 → 自动跳转Trace :
Prometheus告警"order-service P99 > 2s"时,自动带上PromQL查出的时间窗口,在Grafana里点一下就能跳到对应时间段的Trace列表,查看哪些请求慢、慢在哪一步。这就是可观测性的闭环。
九、小结
链路追踪是分布式系统不可缺的工具。核心要点:
- 统一标准:用OpenTelemetry作为采集层,一次接入,后端随意切换;
- 正确埋点:HTTP/数据库/消息队列默认自动埋,自定义业务逻辑手动埋,别遗漏异步任务;
- 采样策略:尾部采样优先,保留有价值的Trace(慢的、错的),不要100%采样;
- 三大支柱联动:Metrics发现异常 → Traces定位根因 → Logs验证细节,缺一不可;
- 统一存储:Jaeger/SkyWalking存Trace,Prometheus存Metrics,Loki存Logs,Grafana做统一可视化。
可观测性不是可选项,是分布式系统的必备能力。越早建,线上问题排查效率越高,熬夜越少,头发越多。
下篇预告:从本地缓存到多级缓存架构------ Caffeine + Redis + 本地热点缓存的实战设计。