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 传来的真实 traceIdX-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 响应头(待优化) │
└──────────────────────────────────────────────────────────────┘