在由数百个微服务构成的现代应用中,一次简单的点击背后,可能是一场横跨数十个服务的奇幻漂流。当页面加载缓慢或报错时,我们如何才能看清这场漂流的全貌,精准定位问题所在?答案就是------全链路追踪。
xml
全链路追踪的技术思想,其公认的起源是Google在2010年发表的《Dapper》 论文。这篇里程碑式的著作,首次系统性地阐述了大规模分布式系统下的追踪理念,并为此后十年的技术发展奠定了基石。
论文开篇便直指核心痛点:在Google的搜索场景中,一次普通的用户查询,背后可能涉及成千上万台服务器的协同工作。工程师们深知,任何微小的延迟都会影响用户体验,但当查询整体变慢时,他们却陷入了困境------无人能精准回答:"性能瓶颈究竟藏身于由上百个服务构成的调用链中的哪一个环节?"
正是为了照亮这片混沌,Dapper应运而生(Dapper是Google内研的全链路追踪系统)。它证明了在大规模分布式系统中,通过收集、整合和分析跨进程的追踪数据,从而清晰地描绘出请求的完整生命周期,不仅是必要的,而且是可行的
从"看病"说起:为什么需要全链路追踪?
想象一下,你去一家大型医院看病(这相当于一个外部请求)。
- 在没有追踪的系统里: 你进入医院,几小时后拿着药出来。你只知道最终看完了病,但完全不清楚中间去过哪些科室、每个科室等了多久、是哪个医生的诊断慢了、
- 在拥有追踪的系统里: 你会拿到一张无比详细的"就诊流水单":
- 10:00-10:02 挂号处,窗口3
- 10:05-10:15 内科,诊室201,李医生
- 10:20-10:40 检验科,血常规,设备B-02
- 10:45-10:50 内科,诊室201,李医生(查看报告)
- 10:55-11:00 药房,窗口5
这张"流水单"清晰地展示了你看病的完整路径、每个环节的耗时和执行者。全链路追踪,就是为我们每一次用户请求,生成这样一张"就诊流水单"。
随着微服务和云原生架构成为主流,系统变得高度分布式。这带来了巨大的挑战:
- 问题定位犹如大海捞针: 一个接口报错,可能是由调用链上第N个服务、数据库或网络抖动引起的。
- 性能瓶颈难以洞察: 请求整体变慢,耗时到底浪费在哪个服务、哪个数据库查询上?
- 系统依赖错综复杂: 服务间的依赖关系动态变化,人工绘制的架构图很快过时。
核心基石:Trace与Span
要理解追踪,必须先掌握两个核心概念:Trace 和 Span。
- Trace: 代表一个完整的请求链路,是最大的追踪单位。它由一个全局唯一的 TraceId 来标识。对应"看病"的整个流程。
- Span: 代表一个独立的工作单元,是追踪的基本组成单位。一个服务的一个方法调用、一次数据库查询,都可以是一个Span。一个Trace由多个Span组成。对应看病过程中的"在内科问诊"这个具体环节。
一个Span包含哪些信息? - 标识符: SpanId(自己), TraceId(所属流程), ParentSpanId(父亲是谁)
- 描述信息: 操作名(如 getUserInfo)
- 时间信息: 开始时间、持续时间
- 关键快照
- Tags: 键值对,描述Span的属性(如 http.method="GET", db.instance="users", error=true)。
- Logs: 时间戳事件,用于记录特定时刻发生的事(如异常堆栈、自定义调试信息)。
Span之间的关系主要有两种:
- ChildOf: 父子关系,父Span依赖子Span的结果,父Span的持续时间包含子Span。
- FollowsFrom: 跟随关系,父Span不关心子Span的结果,常用于异步消息处理。
追踪的工作流程
全链路追踪的实现,可以概括为三步曲:埋点、传播、收集。
- 代码埋点
- 自动埋点: 通过Java Agent、字节码增强等技术,无侵入式地对主流框架进行拦截,自动生成Span。这是目前的主流方式,对开发者透明。
- 手动埋点: 在业务关键逻辑处,调用追踪SDK手动创建Span,记录丰富的业务属性。
- 上下文传播
这是实现"分布式"追踪的关键。当服务A调用服务B时,如何让服务B生成的Span能和服务A的Span串联到同一个Trace下?
答案就是:传递上下文。
服务A在发起调用(如HTTP请求)前,会将当前的追踪上下文(主要是 TraceId 和 ParentSpanId)注入到请求的HTTP头部中。服务B在收到请求后,从HTTP头部中提取出这个上下文,并基于此创建新的子Span。
通过这种方式,无论请求到哪个服务,它们都能被串联到同一棵"调用树"上。 - 数据收集与可视化
- 各个服务上的Agent会异步、非阻塞地将Span数据发送到一个收集器。
- 收集器进行数据清洗、批处理和存储。
- 最终,通过UI界面进行可视化展示,最经典的就是火焰图,它能直观地展示调用层级和耗时。
技术选型
当前,全链路追踪领域已经有了非常成熟的技术方案和事实标准。
- OpenTelemetry:未来的统一标准
- CNCF毕业项目,旨在统一追踪、指标和日志的API和采集。
- 提供了与厂商无关的SDK、API和功能强大的收集器。
OpenTelemetry 简介
可观测性的通用语言
在了解了全链路追踪的起源(Dapper)和众多优秀的追踪工具(如 Jaeger、Zipkin)之后,我们不禁会遇到一个现实问题:技术选型困境 。市场上存在多种追踪、指标、日志的客户端库和格式,一旦选定一个厂商(如 Zipkin),后续更换的成本极高,很容易被"供应商绑定"。
而 OpenTelemetry 的使命,正是为了解决这一核心痛点。它旨在为可观测性数据(追踪、指标、日志)提供一套与厂商无关的、统一的标准化方案。它并不是一个像 Jaeger 那样的后端可视化工具,而是可观测性领域的"普通话"和"数据采集器"。
OpenTelemetry 是什么?
OpenTelemetry 是一个开源项目 ,也是 CNCF 的毕业项目(与 Kubernetes、Prometheus 同级),它提供了一系列 API、库、Agent 和收集器,用于采集和导出应用遥测数据。
您可以将其理解为可观测性世界的 USB-C 标准:
- 在过去,你的手机、电脑、耳机可能有不同的接口(Micro-USB, Lightning, 3.5mm),线缆无法通用。
- OpenTelemetry 定义了统一的"接口"(API)和"数据格式"(Protocol),让任何应用(手机)都能通过一种标准方式,将数据(音视频)发送给任何支持该标准的后端设备(音箱、显示器)。
核心架构与组件
OTel 的架构非常清晰,主要由以下几部分组成:
| 组件 | 角色说明 | 类比 |
|---|---|---|
| API | 定义了一组操作遥测数据(如创建 Span、记录指标)的接口。业务代码应依赖于此抽象层。 | 像 JDBC 接口,定义了标准方法。 |
| SDK | 实现了 API 的具体逻辑,负责采样、数据处理、并通过导出器发送数据。每个语言都有一个实现。 | 像 MySQL JDBC 驱动,是接口的具体实现。 |
| 导出器 | SDK 的插件,负责将数据转换成特定格式并发送到不同的后端。例如,Jaeger 导出器、Prometheus 导出器。 | 像 电源转换插头,将标准接口转换为特定插座。 |
| 收集器 | 一个独立的代理进程,负责接收、处理、批量和导出遥测数据到任何一个或多个后端。它是解耦应用与后端的关键。 | 像 物流中转中心,接收所有包裹,进行分类、打包,再发往不同目的地。 |
| 语义约定 | 一套统一的规范,定义了如何命名和标记遥测数据(例如,HTTP 请求的 Span 应该叫 http.method,数据库的叫 db.statement)。 | 像 国际标准单位,确保所有人都用"米"和"千克",避免混乱。 |
OpenTelemetry 的核心价值
- 使用 OTel 采集数据后,你可以通过更换导出器,轻松地将数据从 Jaeger 切换到 SkyWalking,或者同时发送到多个后端,再也不用担心被某个工具链深度绑定。
- OTel 雄心勃勃地旨在统一可观测性的三大支柱:追踪、指标和日志。这意味着你可以使用同一套客户端库来采集所有类型的遥测数据,极大地简化了代码依赖和运维复杂度。
- 作为 CNCF 力推的项目,几乎所有主流的可观测性后端(Jaeger, Prometheus, Elasticsearch, Datadog等)都官方支持或兼容 OTel 数据格式。它正在成为云原生时代可观测性数据的事实标准。
- OTel Collector 本身就是一个功能强大的数据处理管道。你可以在其中进行数据过滤、加密、添加属性、转换格式、甚至跨信号关联(如将日志与追踪关联),而无需修改应用代码。
架构图

最佳实践,Springboot集成 OpenTelemetry
Maven
yaml
<properties>
<opentelemetry.version>1.32.0</opentelemetry.version>
<opentelemetry.instrumentation.version>2.19.0</opentelemetry.instrumentation.version>
<micrometer.tracing.version>1.2.0</micrometer.tracing.version>
</properties>
<dependencies>
<!-- 统一 OpenTelemetry 依赖 -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-trace</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-metrics</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>${opentelemetry.version}</version>
</dependency>
<!-- Micrometer Tracing (Spring Boot 3.x 推荐) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<!-- 日志关联 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation</artifactId>
</dependency>
</dependencies>
logback.xml
xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 添加应用名属性 -->
<property name="APP_NAME" value="demo_1"/>
<!-- 增强日志模式,包含全链路追踪信息 -->
<property name="default_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [${APP_NAME}] [%X{traceId:-},%X{traceNo:-}] %-5level %c{1} - %m%n"/>
<property name="LOG_HOME" value="G:/home/100qu/logs/app/dsapi/"/>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder class="xxx.xxx.TraceNoPatternLayoutEncoder">
<pattern>${default_pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- info日志 -->
<appender name="infoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="xxx.xxx.ThresholdFilter">
<level>INFO</level>
</filter>
<File>${LOG_HOME}/xxx_info.log</File>
<encoder class="xxx.xxx.TraceNoPatternLayoutEncoder">
<pattern>${default_pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/xxx_info_%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<TimeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>512MB</maxFileSize>
</TimeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>
<root level="info">
<appender-ref ref="infoFile"/>
<appender-ref ref="consoleLog"/>
</root>
</configuration>
业务代码中的追踪ID使用
在Controller中自动生成追踪ID
java
@Operation(summary = "自动生成追踪ID")
@Observed(name = "demo") // 自动创建span
@PostMapping("/demo")
public void demo(DemoRequest request) {
// 日志会自动包含 traceId 和 spanId
log.info("DemoController, demo, 用户ID: {}", request.getUid());
service.demo( request);
}
调用结果

手动传递业务追踪ID
java
@Component
public class BusinessTraceUtil {
private static final String BUSINESS_TRACE_ID = "traceId";
// 设置业务追踪ID
public static void setBusinessTraceId(String traceId) {
MDC.put(BUSINESS_TRACE_ID, traceId);
}
// 获取业务追踪ID
public static String getBusinessTraceId() {
return MDC.get(BUSINESS_TRACE_ID);
}
// 清理
public static void clear() {
MDC.remove(BUSINESS_TRACE_ID);
}
}
java
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 从header中获取或生成业务追踪ID
String businessTraceId = request.getHeader("X-Business-Trace-Id");
if (StringUtils.isBlank(businessTraceId)) {
businessTraceId = generateBusinessTraceId();
}
BusinessTraceUtil.setBusinessTraceId(businessTraceId);
log.info("请求开始: {} {}, 业务追踪ID: {}",
request.getMethod(), request.getRequestURI(), businessTraceId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
String businessTraceId = BusinessTraceUtil.getBusinessTraceId();
log.info("请求结束: {} {}, 业务追踪ID: {}, 状态: {}",
request.getMethod(), request.getRequestURI(),
businessTraceId, response.getStatus());
BusinessTraceUtil.clear();
}
private String generateBusinessTraceId() {
return "BIZ_" + System.currentTimeMillis() + "_" +
ThreadLocalRandom.current().nextInt(1000, 9999);
}
}
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TraceInterceptor traceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(traceInterceptor)
.addPathPatterns("/**");
}
}
接口
java
@Operation(summary = "手动传递追踪ID")
@PostMapping("/demo1")
public void demo1(DemoRequest request) {
// 日志会自动包含 traceId 和 spanId
log.info("DemoController, demo1, 用户ID: {}", request.getUid());
}
调用结果

追加SpanId
xml
<property name="default_pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [${APP_NAME}] [%X{traceId:-},%X{spanId:-},%X{traceNo:-}] %-5level %c{1} - %m%n"/>
调用结果

如何在系统之间传递TraceId?
在A和B系统中都进行如下配置
java
package com.ulejf.bi.config;
import com.ulejf.bi.utils.BusinessTraceUtil;
import io.opentelemetry.api.trace.Span;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.ThreadLocalRandom;
@Slf4j
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从header中获取或生成业务追踪ID
String businessTraceId = request.getHeader("X-Business-Trace-Id");
if (StringUtils.isBlank(businessTraceId)){
businessTraceId = generateBusinessTraceId();
}
BusinessTraceUtil.setBusinessTraceId(businessTraceId);
// 获取当前的OpenTelemetry span信息
Span currentSpan = Span.current();
String traceId = currentSpan.getSpanContext().getTraceId();
String spanId = currentSpan.getSpanContext().getSpanId();
log.info("请求开始: {} {}, 业务追踪ID: {}, TraceId: {}, SpanId: {}",
request.getMethod(), request.getRequestURI(),
businessTraceId, traceId, spanId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
String businessTraceId = BusinessTraceUtil.getBusinessTraceId();
Span currentSpan = Span.current();
String traceId = currentSpan.getSpanContext().getTraceId();
String spanId = currentSpan.getSpanContext().getSpanId();
log.info("请求结束: {} {}, 业务追踪ID: {}, TraceId: {}, SpanId: {}, 状态: {}",
request.getMethod(), request.getRequestURI(),
businessTraceId, traceId, spanId, response.getStatus());
BusinessTraceUtil.clear();
}
private String generateBusinessTraceId(){
return "BIZ_" + System.currentTimeMillis() + "_" + ThreadLocalRandom.current().nextInt(1000, 9999);
}
}
A系统增加配置
java
package com.ulejf.bi.config;
import com.ulejf.bi.utils.BusinessTraceUtil;
import com.ulejf.bi.utils.OpenTelemetryUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.opentelemetry.context.Context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor openTelemetryFeignInterceptor(){
return new OpenTelemetryFeignInterceptor();
}
public static class OpenTelemetryFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 1. 注入OpenTelemetry追踪头(W3C Trace Context)
OpenTelemetryUtil.getPropagator().inject(
Context.current(),
template,
(requestTemplate, key, value) -> {
if (!requestTemplate.headers().containsKey(key)){
requestTemplate.header(key, value); }
}
);
// 2. 传递业务追踪ID
String businessTraceId = BusinessTraceUtil.getBusinessTraceId();
if (businessTraceId != null && !businessTraceId.isEmpty()){
template.header("X-Business-Trace-Id", businessTraceId);
}
// 3. 可选:传递其他需要的header
template.header("X-From-Service", "system-a");
}
}
}
java
package com.ulejf.bi.utils;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
@Component
public class OpenTelemetryUtil {
private static final TextMapPropagator propagator =
GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator();
// 将OpenTelemetry上下文注入到MDC,方便日志输出
public static void injectTraceContextToMdc() {
Span currentSpan = Span.current();
if (currentSpan.getSpanContext().isValid()){
String traceId = currentSpan.getSpanContext().getTraceId();
String spanId = currentSpan.getSpanContext().getSpanId();
MDC.put("traceId", traceId); MDC.put("spanId", spanId);
}
}
// 清理MDC中的Trace上下文
public static void clearTraceContextFromMdc(){
MDC.remove("traceId"); MDC.remove("spanId");
}
public static TextMapPropagator getPropagator(){
return propagator;
}
}
调用结果
A系统日志

B系统日志

可以看到A和B两个系统的 traceId 都是 BIZ_1762850614157 证明追踪Id确实已经跨服务传递下来了
总结:我觉得跨系统全链路追踪的核心机制就是通过HTTP请求头来传递TraceId。当A系统调用B系统时,会将当前的TraceId自动放入请求头中发送;B系统接收到请求后,从请求头中提取同一个TraceId并继续使用。这样,尽管每个系统会生成自己的SpanId来记录内部调用细节,但整个调用链共享同一个TraceId,从而实现了跨系统的请求追踪和链路还原。