分布式应用可观测全链路追踪技术

在由数百个微服务构成的现代应用中,一次简单的点击背后,可能是一场横跨数十个服务的奇幻漂流。当页面加载缓慢或报错时,我们如何才能看清这场漂流的全貌,精准定位问题所在?答案就是------全链路追踪。

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

这张"流水单"清晰地展示了你看病的完整路径、每个环节的耗时和执行者。全链路追踪,就是为我们每一次用户请求,生成这样一张"就诊流水单"。

随着微服务和云原生架构成为主流,系统变得高度分布式。这带来了巨大的挑战:

  1. 问题定位犹如大海捞针: 一个接口报错,可能是由调用链上第N个服务、数据库或网络抖动引起的。
  2. 性能瓶颈难以洞察: 请求整体变慢,耗时到底浪费在哪个服务、哪个数据库查询上?
  3. 系统依赖错综复杂: 服务间的依赖关系动态变化,人工绘制的架构图很快过时。

核心基石: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,从而实现了跨系统的请求追踪和链路还原。

相关推荐
数据的世界011 小时前
JAVA和C#的语法对比
java·windows·c#
渡我白衣2 小时前
深入理解 OverlayFS:用分层的方式重新组织 Linux 文件系统
android·java·linux·运维·服务器·开发语言·人工智能
百***92652 小时前
java进阶1——JVM
java·开发语言·jvm
虫师c2 小时前
字节码(Bytecode)深度解析:跨平台运行的魔法基石
java·jvm·java虚拟机·跨平台·字节码
q***72192 小时前
Spring Boot环境配置
java·spring boot·后端
洛_尘2 小时前
数据结构--7:排序(Sort)
java·数据结构
JIngJaneIL2 小时前
就业|高校就业|基于ssm+vue的高校就业信息系统的设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·高校就业
一 乐2 小时前
社区互助|社区交易|基于springboot+vue的社区互助交易系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·小区互助系统
q***57743 小时前
Spring Boot 实战:轻松实现文件上传与下载功能
java·数据库·spring boot