SpringBoot应用链路追踪 traceId 技术方案

SpringBoot应用 链路追踪 traceId 技术方案

内容
文档版本 V1.0
编写日期 2026-07-19
适用范围 JANUS 项目全链路日志追踪(Gateway + 微服务 + Nginx)

1. 概述

JANUS 项目需要在全链路日志中关联 trace_id,实现从 Nginx → Gateway → 微服务的请求追踪。本文档详细说明 traceId 的生成、传播和采集机制,覆盖挂载/未挂载 SkyWalking Agent 两种场景。


2. SkyWalking Agent 工作机制

2.1 Agent 拦截请求的完整流程

复制代码
HTTP 请求进入服务
         │
         ▼
┌─────────────────────────────────────────────────┐
│  SkyWalking Agent 字节码增强(自动,无需代码)      │
│                                                   │
│  ① 拦截 Tomcat HTTP 入口                          │
│                                                   │
│  ② 检查请求头有没有 sw8                            │
│     有 sw8  → 解析出上游 traceId,延续链路          │
│     没有 sw8 → 创建新的根 traceId                  │
│                                                   │
│  ③ 创建 TraceContext(traceId 来自第②步)          │
│                                                   │
│  ④ 把 traceId 写入 MDC.TID     ← 给 logback 用   │
│  ⑤ 把 traceId 存入线程上下文     ← 给代码用        │
│                                                   │
│  ⑥ 后续发 HTTP 请求给下游时 → 自动加 sw8 请求头    │
└─────────────────────────────────────────────────┘
         │
         ▼
   业务代码执行

关键点:② 是源头,③④⑤ 都是 ② 的结果。 没有上游 sw8 就创建新的根 traceId,有就延续。

2.2 traceId 的三种输出方式

Agent 创建的 traceId 通过三种方式暴露给不同消费者:

输出方式 消费者 用途 依赖 sw8?
MDC.TID logback 应用日志自动输出 trace_id 字段 ❌ 不依赖
TraceContext.traceId() 业务代码 MetricAspect / BusinessLogGlobalFilter 读取 ❌ 不依赖
sw8 请求头 下游服务的 Agent 服务间链路传播 ❌ 不依赖(但 sw8 里包含 traceId)

三者是同一个 traceId 的三种输出方式,不是谁依赖谁。

2.3 挂载 Agent 但不上报 OAP 的场景

操作 是否依赖 OAP Server
生成 traceId ❌ 本地生成
注入 MDC TID ❌ 本地操作
传播 sw8 头到下游 ❌ 本地操作
TraceContext.traceId() 返回值 ❌ 本地获取
上报 trace 数据到 OAP ✅ 依赖 OAP

结论:挂 Agent 就有 traceId,跟是否上报 OAP 无关。 但注意 agent.sample_n_per_3_secs=0 会完全禁用 trace 创建,此时 traceId 返回 N/A,等于没挂 Agent。


3. sw8 请求头详解

3.1 什么是 sw8

sw8 是 SkyWalking 8.x 版本的跨进程传播协议头,全称 SkyWalking v8 Cross Process Propagation Header

3.2 格式

复制代码
sw8: <sampled>-<traceId>-<segmentId>-<spanId>-<parentService>-<parentServiceInstance>-<parentEndpoint>-<peer>
索引 说明
sampled 0 采样标记(1=采样,0=不采样)
traceId 1 全局唯一链路 ID
segmentId 2 当前进程段 ID
spanId 3 当前 span 编号
parentService 4 父服务名(Base64)
parentServiceInstance 5 父实例标识(Base64)
parentEndpoint 6 父端点名(Base64)
peer 7 目标地址(Base64)

3.3 sw8 与 X-Trace-Id 的区别

sw8 X-Trace-Id
谁定义 SkyWalking 协议 JANUS 自定义
内容 8 段完整链路信息 只有 traceId 一个值
谁写入 SkyWalking Agent 自动 TraceFilter 代码写入
谁消费 SkyWalking Agent 自动 Nginx + 未挂 Agent 的下游
用途 Agent 间链路传播 让非 Agent 组件也能拿到 traceId

简单说:sw8 是 Agent 之间的"暗号",X-Trace-Id 是给没挂 Agent 的服务准备的"翻译"。


4. 全场景 traceId 传播分析

4.1 场景一:前端 → Gateway → 微服务(挂 Agent)

复制代码
前端 → Nginx → Gateway(挂Agent) → janus-product(挂Agent)

1. 请求进入 Gateway:
   Agent 拦截 → 没有 sw8 → 创建根 traceId="abc123"
   → MDC.TID = "abc123"                    ✅ Gateway 应用日志有 trace_id
   → TraceContext.traceId() = "abc123"     ✅ Gateway 业务日志有 trace_id

2. TraceFilter 执行:
   resolveTraceId() 优先级:
     ① traceparent 头 → 无
     ② sw8 头 → Agent 已处理,但 TraceFilter 也能从请求头读到
     ③ X-Trace-Id 头 → 无
     ④ TraceContext.traceId() → "abc123"   ✅ 命中
   → 写 X-Trace-Id 响应头 = "abc123"
   → 写 X-Trace-Id 请求头 = "abc123"(转发给下游)

3. Gateway 转发请求到 janus-product:
   Agent 自动在请求头加 sw8(包含 traceId="abc123")

4. 请求进入 janus-product:
   Agent 拦截 → 有 sw8 → 解析出 traceId="abc123",延续链路
   → MDC.TID = "abc123"                    ✅ janus-product 应用日志有 trace_id
   → TraceContext.traceId() = "abc123"     ✅ janus-product 业务日志有 trace_id

5. 响应返回 Nginx:
   Nginx 读 $upstream_http_x_trace_id = "abc123"  ✅ Nginx access.log 有 trace_id

全链路 traceId 一致,完美。

4.2 场景二:前端 → Gateway → 微服务(未挂 Agent)

复制代码
前端 → Nginx → Gateway(未挂Agent) → janus-product(未挂Agent)

1. 请求进入 Gateway:
   无 Agent → 无 MDC.TID                    ❌ Gateway 应用日志无 trace_id
   TraceFilter.resolveTraceId():
     ① traceparent → 无
     ② sw8 → 无
     ③ X-Trace-Id → 无
     ④ TraceContext.traceId() → "N/A"
     ⑤ UUID 兜底 → 生成 "uuid-abc123"
   → 写 X-Trace-Id 响应头 = "uuid-abc123"
   → 写 X-Trace-Id 请求头 = "uuid-abc123"(转发给下游)

2. Gateway 转发请求到 janus-product:
   无 Agent → 无 sw8 请求头
   但有 X-Trace-Id 请求头 = "uuid-abc123"

3. 请求进入 janus-product:
   无 Agent → 无 MDC.TID                    ❌ janus-product 应用日志无 trace_id
   MetricAspect.resolveTraceId():
     ① TraceContext.traceId() → "N/A"
     ② X-Trace-Id 请求头 → "uuid-abc123"    ✅ 命中
   → janus-product 业务日志 trace_id = "uuid-abc123"

4. 响应返回 Nginx:
   Nginx 读 $upstream_http_x_trace_id = "uuid-abc123"  ✅ Nginx access.log 有 trace_id

业务日志和 Nginx 日志有 traceId(UUID),应用日志无 traceId。

4.3 场景三:前端 → Nginx → 微服务(不经过 Gateway,挂 Agent)

复制代码
前端 → Nginx → janus-product(挂Agent)

1. 请求进入 janus-product:
   Agent 拦截 → 没有 sw8 → 创建根 traceId="abc123"
   → MDC.TID = "abc123"                    ✅ 应用日志有 trace_id
   → TraceContext.traceId() = "abc123"     ✅ 业务日志有 trace_id

2. 响应返回 Nginx:
   Agent 不往响应头写 X-Trace-Id            ❌
   janus-product 没有 TraceFilter              ❌
   Nginx 读 $upstream_http_x_trace_id → 空  ❌ Nginx access.log 无 trace_id

应用日志和业务日志有 traceId,但 Nginx access.log 没有。 需要在微服务中添加写 X-Trace-Id 响应头的逻辑才能解决。

4.4 场景四:前端 → Nginx → 微服务(不经过 Gateway,未挂 Agent)

复制代码
前端 → Nginx → janus-product(未挂Agent)

1. 请求进入 janus-product:
   无 Agent → 无 MDC.TID                    ❌ 应用日志无 trace_id
   MetricAspect.resolveTraceId():
     ① TraceContext.traceId() → "N/A"
     ② X-Trace-Id 请求头 → 无(没有 Gateway 写入)
   → trace_id = ""                          ❌ 业务日志无 trace_id

2. 响应返回 Nginx:
   Nginx 读 $upstream_http_x_trace_id → 空  ❌ Nginx access.log 无 trace_id

全链路无 traceId。 这种场景下需要额外方案(如 Nginx 自行生成 traceId 并通过请求头传递)。


5. 各组件 traceId 采集方式汇总

5.1 Gateway(WebFlux)

日志类型 traceId 来源 实现方式
应用日志(log.info SkyWalking Agent → MDC.TID logback includeMdcKeyName: TID,Logstash 重命名为 trace_id
业务日志(API) TraceFilter.resolveTraceId() BusinessLogGlobalFilter 读取 exchange attribute
Nginx access.log TraceFilter 写 X-Trace-Id 响应头 Nginx 配置 $upstream_http_x_trace_id

5.2 微服务(MVC,如 janus-product)

日志类型 traceId 来源 实现方式
应用日志(log.info SkyWalking Agent → MDC.TID logback includeMdcKeyName: TID,Logstash 重命名为 trace_id
业务日志(API) MetricAspect.resolveTraceId() 优先 TraceContext.traceId(),兜底 X-Trace-Id 请求头
Nginx access.log ❌ 无(微服务未写 X-Trace-Id 响应头) 需额外添加 Interceptor

5.3 Nginx

日志类型 traceId 来源 实现方式
access.log Gateway 响应头 X-Trace-Id nginx.conf 配置 $upstream_http_x_trace_id
error.log ❌ 无 error.log 无法获取响应头

6. TraceFilter.resolveTraceId() 优先级

java 复制代码
private String resolveTraceId(ServerHttpRequest request) {
    // 1. W3C traceparent 头(标准协议,未来跨服务通信用)
    //    格式:00-<traceId>-<spanId>-<flags>
    String traceparent = request.getHeaders().getFirst("traceparent");
    if (traceparent != null && traceparent.length() >= 35) { ... }

    // 2. SkyWalking sw8 头(解析出真实 traceId,不是整个 sw8 字符串)
    //    格式:<sampled>-<traceId>-<segmentId>-<spanId>-...
    String sw8 = request.getHeaders().getFirst("sw8");
    if (sw8 != null && !sw8.isEmpty()) { ... }

    // 3. 自定义 X-Trace-Id 头(兼容旧客户端或上游网关)
    String customTraceId = request.getHeaders().getFirst("X-Trace-Id");
    if (customTraceId != null && !customTraceId.isEmpty()) { return customTraceId; }

    // 4. SkyWalking Agent 当前上下文(挂了 agent 后有值)
    String swTraceId = TraceContext.traceId();
    if (swTraceId != null && !"N/A".equals(swTraceId)) { return swTraceId; }

    // 5. UUID 兜底(未挂 agent 且无上游链路时)
    return UUID.randomUUID().toString().replace("-", "");
}

为什么优先级这样设计:

  • traceparent 最优先:W3C 标准协议,跨系统兼容性最好
  • sw8 第二:SkyWalking 协议,从上游 Agent 传来的真实 traceId
  • X-Trace-Id 第三:自定义协议,未挂 Agent 的上游网关传来的
  • TraceContext.traceId() 第四:当前 Agent 自己生成的(挂 Agent 时有值)
  • UUID 兜底:所有方式都拿不到时,至少保证有一个 ID

7. MetricAspect.resolveTraceId() 优先级

java 复制代码
private String resolveTraceId(HttpServletRequest request) {
    // 1. SkyWalking Agent 当前上下文(挂 agent 后自动有值)
    String traceId = TraceContext.traceId();
    if (traceId != null && !"N/A".equals(traceId)) { return traceId; }

    // 2. gateway 透传的 X-Trace-Id 请求头(未挂 agent 时由 gateway TraceFilter 写入)
    String headerTraceId = request.getHeader("X-Trace-Id");
    if (headerTraceId != null && !headerTraceId.isEmpty()) { return headerTraceId; }

    // 3. 空字符串(兜底)
    return "";
}

与 TraceFilter 的区别:

  • 微服务不需要解析 sw8 和 traceparent(Gateway 已经处理了)
  • 微服务不需要 UUID 兜底(没有 traceId 就留空,不伪造)
  • 微服务是 MVC 模式,用 HttpServletRequest 而非 ServerHttpRequest

8. 未挂 Agent 时各场景 traceId 情况

场景 应用日志 业务日志 Nginx access.log
前端 → Gateway → 微服务 ❌ 无 TID ✅ UUID(Gateway 生成) ✅ UUID
前端 → Nginx → 微服务 ❌ 无 TID ❌ 空 ❌ 无
微服务内部调用 ❌ 无 TID ❌ 空 N/A

挂 Agent 后:

场景 应用日志 业务日志 Nginx access.log
前端 → Gateway → 微服务 ✅ 真实 traceId ✅ 真实 traceId ✅ 真实 traceId
前端 → Nginx → 微服务 ✅ 真实 traceId ✅ 真实 traceId ❌ 无(需加 Interceptor)
微服务内部调用 ✅ 真实 traceId ✅ 真实 traceId N/A

9. 待优化项

9.1 微服务未写 X-Trace-Id 响应头

问题:前端直接调用微服务(不经过 Gateway)时,Nginx access.log 无法获取 traceId。

方案:在微服务中添加 Interceptor 或在 MetricAspect 中写 X-Trace-Id 响应头:

java 复制代码
// 在 MetricAspect.sendBusinessLog() 中添加
if (traceId != null && !traceId.isEmpty()) {
    HttpServletResponse response = getResponse();
    if (response != null) {
        response.setHeader("X-Trace-Id", traceId);
    }
}

优先级:低。当前架构下前端请求都走 Gateway,此场景暂不出现。

9.2 Nacos gRPC DEBUG 日志噪声

问题 :Nacos 客户端 2.x 使用 shaded gRPC,gRPC 使用 java.util.logging (JUL) 而非 logback,logback 的 <logger> 配置无法完全控制 JUL 层的日志生成。

方案:在 Spring Boot 启动时设置 JUL logger level:

java 复制代码
@Configuration
public class LoggingConfig {
    @PostConstruct
    public void setJulLogLevel() {
        LogManager.getLogManager().getLogger("io.grpc").setLevel(Level.WARNING);
        LogManager.getLogManager().getLogger("com.alibaba.nacos.shaded.io.grpc").setLevel(Level.WARNING);
    }
}

优先级:中。不影响功能,但会产生大量无用日志推送到 ES。


10. 附录:全链路 traceId 流转图

复制代码
                    ┌─────────────────────────────────────────────────────────┐
                    │                    前端请求                              │
                    └────────────────────────┬────────────────────────────────┘
                                             │
                                             ▼
                    ┌─────────────────────────────────────────────────────────┐
                    │                    Nginx                                │
                    │  access.log 记录 $upstream_http_x_trace_id              │
                    └────────────────────────┬────────────────────────────────┘
                                             │
                                             ▼
              ┌──────────────────────────────────────────────────────────────┐
              │                      Gateway (WebFlux)                       │
              │                                                              │
              │  挂 Agent:                                                   │
              │    Agent 拦截 → 检查 sw8 → 创建/延续 traceId                │
              │    → MDC.TID = traceId (应用日志)                            │
              │    → TraceContext.traceId() = traceId (业务日志)             │
              │                                                              │
              │  未挂 Agent:                                                 │
              │    TraceFilter.resolveTraceId() → UUID 兜底                  │
              │                                                              │
              │  TraceFilter 写 X-Trace-Id 响应头 + 请求头                   │
              │  Agent 自动加 sw8 请求头(挂 Agent 时)                       │
              └──────────────────────────┬───────────────────────────────────┘
                                         │
                                         ▼
              ┌──────────────────────────────────────────────────────────────┐
              │                    微服务 (MVC, 如 janus-product)              │
              │                                                              │
              │  挂 Agent:                                                   │
              │    Agent 拦截 → 解析 sw8 → 延续 traceId                     │
              │    → MDC.TID = traceId (应用日志)                            │
              │    → TraceContext.traceId() = traceId (业务日志)             │
              │                                                              │
              │  未挂 Agent:                                                 │
              │    MetricAspect.resolveTraceId() → 读 X-Trace-Id 请求头      │
              │                                                              │
              │  ❌ 未写 X-Trace-Id 响应头(待优化)                          │
              └──────────────────────────────────────────────────────────────┘
相关推荐
耀耀_很无聊2 小时前
13_Spring Boot 3.5.8 + Redisson 3.45.1 导致 Sa-Token 登录 StackOverflowError
spring boot·后端·bootstrap
霸道流氓气质3 小时前
SpringBoot中集成阿里云消息队列 ApsaraMQ for RabbitMQ 全面指南
spring boot·阿里云·java-rabbitmq
霸道流氓气质3 小时前
SpringBoot中实现告警判定算法-位报警与模拟量阈值-技术详解
spring boot·算法·jquery
YDS8293 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
阿拉雷️3 小时前
部署实战】Docker + AI Agent:让AI一键部署Spring Boot到服务器,从打包到上线只要一条指令
人工智能·spring boot·docker
IT_Octopus4 小时前
一次由 RestTemplate 引发的 Connection reset:被 XML 截胡的请求体
xml·java·spring boot
重生之我是Java开发战士5 小时前
【Java EE】Spring Boot配置文件
java·spring boot·java-ee
Linux-187415 小时前
分布式链路追踪系统之二进制安装skywalking
elasticsearch·云原生·skywalking·分布式链路追踪系统·应用程序性能监控
钱六两20 小时前
#3、SpringAI 接入deepSeek大模型(喂饭版)
java·spring boot·ai编程