【架构实战】链路追踪与可观测性实践:从日志孤岛到全链路可见
分布式系统出问题的时候,最让人崩溃的不是"服务挂了",而是"不知道哪里挂了"。一个请求经过网关→认证→业务→数据库→缓存→消息队列,涉及七八个服务,任何一个环节出问题都可能导致最终响应慢或报错。但日志散落在各个服务里,查一个问题要在多个终端之间来回跳转,效率极低。
链路追踪(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 - 580ms2.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 引入依赖
<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
otel:exporter:otlp:endpoint:http://otel-collector:4317# OTel Collector地址service:name:order-service4.3 手动埋点(关键操作)
// 方式1:自动埋点(Spring Boot Web自动拦截)// 引入starter后,所有HTTP请求自动追踪// 方式2:手动埋点(自定义业务逻辑)importio.opentelemetry.api.trace.Tracer;@ServicepublicclassOrderService{privatefinalTracertracer;publicOrderService(Tracertracer){this.tracer=tracer;}publicOrderResultcreateOrder(OrderDTOdto){// 开始一个SpanSpanspan=tracer.spanBuilder("createOrder").setAttribute("user.id",dto.getUserId()).setAttribute("order.amount",dto.getAmount()).startSpan();try(Scopescope=span.makeCurrent()){// 业务逻辑validateOrder(dto);deductStock(dto);Orderorder=saveOrder(dto);span.setAttribute("order.id",order.getId());span.setAttribute("order.status","success");returnorder;}catch(Exceptione){span.recordException(e);span.setAttribute("order.status","failed");throwe;}finally{span.end();}}}4.4 异步任务的追踪
异步任务(如线程池、消息队列)是链路追踪的难点——子任务会创建新的Span,但需要正确关联到父Trace:
// 使用Context.current()保持链路关联SpanparentSpan=Span.current();CompletableFuture.supplyAsync(()->{// 在子线程中继续父Span的链路try(Scopescope=parentSpan.makeCurrent()){returnsendNotification(orderId);}},asyncExecutor);消息队列的追踪也一样,消费者需要从MQ Header里提取TraceContext:
@RabbitListener(queues="order.notify")publicvoidhandleOrderNotify(Messagemsg){// 从消息Header中提取TraceContextContextextracted=GlobalOpenTelemetry.getPropagators().getTextMapPropagator().extract(Context.current(),msg,GETTER);try(Scopescope=extracted.makeCurrent()){// 处理消息,自动关联到原始TraceprocessNotification(msg);}}五、K8s中部署Jaeger + OpenTelemetry Collector
追踪数据量大,不能直接让应用发往Jaeger,需要经过Collector做聚合、采样、转发。
5.1 OTel Collector的Deployment配置
apiVersion:apps/v1kind:Deploymentmetadata:name:otel-collectornamespace:observabilityspec:replicas:1template:spec:containers:-name:otelcolimage:otel/opentelemetry-collector-contrib:latestargs:-"--config=/etc/otelcol-contrib/config.yaml"ports:-containerPort:4317# OTLP gRPC-containerPort:4318# OTLP HTTP-containerPort:8888# Prometheus metricsvolumeMounts:-name:otel-configmountPath:/etc/otelcol-contribvolumes:-name:otel-configconfigMap:name:otel-collector-config---apiVersion:v1kind:ConfigMapmetadata:name:otel-collector-confignamespace:observabilitydata:config.yaml:|receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318processors:batch:timeout:1ssend_batch_size:1024memory_limiter:check_interval:1slimit_mib:512exporters:jaeger:endpoint:jaeger-collector:14250tls:insecure:trueservice:pipelines:traces:receivers:[otlp]processors:[memory_limiter,batch]exporters:[jaeger]5.2 部署Jaeger
apiVersion:apps/v1kind:Deploymentmetadata:name:jaegernamespace:observabilityspec:replicas:1selector:matchLabels:app:jaegertemplate:spec:containers:-name:jaegerimage:jaegertracing/all-in-one:latestenv:-name:COLLECTOR_OTLP_ENABLEDvalue:"true"ports:-containerPort:16686# Jaeger UI-containerPort:14250# gRPC (Collector)-containerPort:14268# Thrift (legacy)---apiVersion:v1kind:Servicemetadata:name:jaegernamespace:observabilityspec:type:NodePortports:-port:16686targetPort:16686nodePort:31686selector:app:jaeger部署后访问http://<node>:31686打开Jaeger UI,输入服务名或TraceID即可查询。
六、采样策略:别让追踪数据把你淹没
生产环境请求量极大,100%采样会把存储打爆,也推高计算成本。采样策略是关键。
6.1 常见采样策略
1. 固定采样(Probabilistic)
processors:tail_sampling:decision_wait:10spolicies:-name:probabilistic-policytype:probabilisticprobabilistic:sampling_percentage:10# 采样10%2. 尾部采样(Tail-Based Sampling)
这是最实用的策略——先收集所有Trace,请求结束后根据结果决定是否保留。只保留慢请求(>1s)、错误请求、有特定标签的请求,极大减少存储量:
processors:tail_sampling:decision_wait:10spolicies:# 保留所有错误请求-name:errors-policytype:status_codestatus_code:{status_codes:[ERROR]}# 保留超过1秒的慢请求-name:slow-traces-policytype:latencylatency:threshold_ms:1000# 保留特定接口的请求-name:payment-policytype:string_attributestring_attribute:key:http.routevalues:["/api/pay/*"]# 兜底:10%采样-name:probabilistic-policytype:probabilisticprobabilistic:sampling_percentage:106.2 服务级别的采样配置
不同服务请求量差异很大,可以按服务配置采样率:
# 低流量服务100%采样,高流量服务1%采样receivers:otlp:protocols:grpc:endpoint:0.0.0.0:4317配合Prometheus自动发现,让高频服务自动降低采样率。
七、可视化:让数据说话
链路追踪的最终价值在可视化。Jaeger提供:
- Trace视图:查看单个请求的完整调用链,每个Span的耗时、状态一目了然;
- 服务依赖图:Service Graph,自动从Trace数据中生成服务依赖关系图;
- 延迟分析:看P50/P95/P99的Span耗时分布,找出性能瓶颈。
# 查询最近10分钟内超过2秒的慢Tracejaeger-cli traces--limit=20\--service=order-service\--lookback=10m\--min-duration=2s配合Grafana可以做成仪表盘,把Metrics(QPS、延迟)+ Traces(慢Trace详情)放在同一张报表里:
// 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 + 本地热点缓存的实战设计。