fastbee/docs/knowledge-base/07-TraceId 链路追踪实现.md
2026-09-15 15:25:50 +08:00

14 KiB
Raw Permalink Blame History

任务:TraceId 链路追踪与上下文传播能力

你现在需要为我的 Java 单体项目实现一套“统一 TraceId 链路追踪基础设施”。

一、项目背景

当前项目是 Java 单体应用,技术栈:

  • JDK 8
  • Spring Boot 2.5.x
  • Spring MVC
  • Logback / SLF4J
  • MySQL
  • Redis
  • Quartz
  • MQ
  • Netty
  • MQTT
  • 后续会接入 Python AI 服务、LLM、微服务、OpenTelemetry

项目未来会逐步发展为:

单体应用
    ↓
IoT + SaaS + AI
    ↓
微服务
    ↓
OpenTelemetry / 分布式链路追踪

因此这次实现不能只是简单地增加一个 MDC 字段,而要设计成可长期演进的基础框架。


二、核心目标

实现统一的:

TraceContext

核心字段至少包括:

traceId
tenantId
userId

其中当前阶段最重要的是:

traceId

核心原则:

一次完整业务执行链使用同一个 traceId。

例如:

前端请求
   ↓
Controller
   ↓
Service
   ↓
线程池
   ↓
MQ
   ↓
MQ Consumer
   ↓
AI 服务
   ↓
数据库
   ↓
通知

如果它们属于同一次业务执行,则全部使用:

traceId = A

三、TraceId 生命周期规则

必须严格遵循以下规则。

1. 有上游 TraceId

优先继承。

例如 HTTP:

X-Trace-Id: 01KABC...

后端收到后:

TraceId = 01KABC...

不要重新生成。


2. 没有上游 TraceId

自动创建新的 TraceId。

例如:

浏览器
  ↓
HTTP
  ↓
Java

如果 HTTP 没有:

X-Trace-Id

则 Java 自动生成。


3. 定时任务没有上游 TraceId

Quartz / @Scheduled 等任务启动时自动生成:

traceId

例如:

traceId=T202609150001

整个任务执行过程保持相同 TraceId。


四、TraceId 生成器

设计独立:

TraceIdGenerator

不要把 TraceId 生成逻辑写死在 Filter 中。

要求:

TraceContext
TraceIdGenerator

职责分离。

优先考虑 ULID 或项目中已有的高质量 ID 生成方案。

要求:

  • 唯一
  • 高并发安全
  • 尽量时间有序
  • 适合日志检索
  • 不依赖数据库
  • 后续可以替换实现

五、TraceContext 设计

创建统一:

TraceContext

例如:

TraceContext
├── getTraceId()
├── setTraceId()
├── getOrCreateTraceId()
├── set()
├── get()
├── clear()
└── snapshot()

不要让业务代码直接到处:

MDC.put("traceId", ...)

业务代码应该依赖:

TraceContext

MDC 只是 TraceContext 的一种底层日志传播实现。

核心原则:

TraceContext ≠ MDC

而应该是:

TraceContext
     ↓
MDC
HTTP Header
MQ Header
ThreadPool
Future

六、Web 请求

实现 Spring MVC Filter。

建议:

OncePerRequestFilter

功能:

  1. 从 HTTP Header 获取:
X-Trace-Id
  1. 如果存在且合法:
继续使用
  1. 如果不存在:
生成 TraceId
  1. 放入:
TraceContext
MDC
  1. 将 TraceId 写入 HTTP Response Header:
X-Trace-Id: 01KABC...
  1. 请求结束后必须清理:
MDC
TraceContext

防止 Tomcat 线程池复用造成 TraceId 污染。


七、日志输出

修改 Logback 日志格式,使所有日志自动包含:

traceId

例如:

2026-09-15 11:20:31.123 INFO
[http-nio-8080-exec-5]
traceId=01KABC123
DeviceService
-
查询设备成功 deviceId=DEV001

要求:

  • 控制台日志包含 traceId
  • 普通文件日志包含 traceId
  • error 日志包含 traceId
  • 不要求业务代码修改每一条 log.info()
  • 使用 MDC 自动完成

同时考虑 JSON 日志扩展能力。


八、线程池上下文传播

这是本任务的重点。

普通:

ThreadPoolExecutor

或者:

CompletableFuture

不能保证 MDC 自动传播。

必须实现统一的上下文传播机制。

例如:

TraceContext
     ↓
snapshot
     ↓
Runnable / Callable
     ↓
restore
     ↓
执行
     ↓
clear

要求实现类似:

TraceContextRunnable
TraceContextCallable
TraceableExecutor

或者使用 Spring 提供的:

TaskDecorator

如果项目使用 Spring ThreadPoolTaskExecutor,优先通过:

TaskDecorator

实现。

目标:

Web线程
traceId=A
    ↓
线程池
traceId=A

例如:

executor.execute(() -> {
    log.info("异步处理");
});

日志必须仍然是:

traceId=A

九、CompletableFuture

重点处理:

CompletableFuture.runAsync(...)
CompletableFuture.supplyAsync(...)
thenApply(...)
thenAccept(...)
thenRun(...)

分析项目中现有异步代码。

不要简单粗暴地修改所有业务代码。

优先通过统一线程池:

TraceableExecutor

解决。

如果无法完全覆盖 CompletableFuture 的默认线程池,需要明确指出风险,并提供统一解决方案。

禁止让业务代码到处手动:

MDC.put(...)
MDC.clear(...)

十、MQ TraceId 传播

这是第二个重点。

当 Java 发送 MQ 消息时:

必须把当前:

traceId

写入 MQ Message Header / Properties。

例如:

traceId=A

发送:

MQ Message
├── Header
│   └── traceId=A
└── Body

Consumer 收到消息:

MQ Header
   ↓
读取 traceId
   ↓
TraceContext.setTraceId()
   ↓
MDC
   ↓
业务处理

这样:

Web
traceId=A
   ↓
MQ Producer
traceId=A
   ↓
MQ
traceId=A
   ↓
Consumer
traceId=A

如果 MQ 消息没有 TraceId:

Consumer
   ↓
自动生成新的 TraceId

但正常情况下必须继承 Producer 的 TraceId。


十一、MQ 不同消息中间件

先检查当前项目实际使用的 MQ。

不要凭空假设。

如果项目目前只有一种 MQ:

优先实现当前实际使用的 MQ。

同时将设计抽象成:

TraceMessageInterceptor

未来可以支持:

RabbitMQ
Kafka
RocketMQ
Redis Stream
其他 MQ

不要为了扩展性一次性实现大量没有使用的 MQ。


十二、HTTP 调用链

如果 Java 单体项目会调用其他服务:

例如:

Java
 ↓ HTTP
Python AI

必须自动传递:

X-Trace-Id: A

如果项目使用:

RestTemplate
WebClient
OpenFeign
HttpClient
OkHttp

检查实际使用情况。

优先通过统一 Interceptor 实现。

例如:

Java
traceId=A
   ↓
HTTP Header
X-Trace-Id=A
   ↓
Python AI
traceId=A

这样 Java 和 Python 日志可以使用同一个 TraceId 检索。


十三、Netty / MQTT / TCP / UDP

这是 IoT 项目的重要场景。

检查项目现有:

Netty
MQTT
TCP
UDP
CoAP

入口。

对于每一个独立的消息 / 请求:

如果存在上游 TraceId:

继承

否则:

生成

然后进入:

TraceContext

例如:

设备 DEV001
   ↓
MQTT
   ↓
traceId=M001
   ↓
数据解析
   ↓
物模型
   ↓
数据库
   ↓
告警

整条处理链使用:

traceId=M001

注意:

设备每次独立上报消息属于独立业务事件,因此通常应该生成新的 TraceId,而不是让一个设备永久使用同一个 TraceId。


十五、SaaS 上下文

项目未来是 SaaS,因此 TraceContext 预留:

traceId
tenantId
userId

如果当前项目已经有:

tenantId
userId

则在请求认证成功后自动放入上下文。

例如:

traceId=A
tenantId=10001
userId=9527

但不要因为本次任务而大范围修改现有权限系统。

只需要提供统一上下文承载能力。


十六、线程安全

这是重点。

必须考虑:

Tomcat线程池复用
Quartz线程池
业务线程池
CompletableFuture
MQ Consumer线程
Netty EventLoop

任何任务结束后都必须:

clear

禁止出现:

请求A
traceId=A

请求结束

请求B
traceId=A

这种严重的数据串链问题。


十七、异常处理

异常情况下 TraceId 也必须保留。

例如:

traceId=A
ERROR
设备数据处理失败

完整异常堆栈必须能够通过:

traceId=A

检索出来。

Web 异常响应最好也返回:

X-Trace-Id: A

这样前端报错:

TraceId: A

我就可以直接去 Kibana / 日志文件搜索:

traceId=A

定位问题。


十八、日志检索目标

最终必须支持:

grep "traceId=01KABC" app.log

或者 Kibana:

traceId:"01KABC"

一次找到:

HTTP请求
Service
数据库
线程池
MQ Producer
MQ Consumer
AI
异常

十九、代码结构要求

根据当前项目实际结构,合理放置。

如果存在类似:

common
framework
core
starter

优先放到公共基础模块。

推荐结构:

xxx-core
└── trace
    ├── TraceContext
    ├── TraceIdGenerator
    ├── TraceContextSnapshot
    ├── TraceContextRunnable
    └── TraceContextCallable

xxx-web-starter
└── trace
    └── TraceIdFilter

xxx-log-starter
└── trace
    └── MDC / Logback

xxx-schedule-starter
└── trace
    └── Quartz Trace

xxx-mq-starter
└── trace
    ├── Producer Trace
    └── Consumer Trace

具体目录必须根据当前项目实际结构调整,不要机械套用。


二十、不要过度设计

当前项目是单体项目。

第一阶段:

必须实现:

TraceId
MDC
Web
ThreadPool
Quartz
MQ
HTTP

暂时不要自行实现完整:

Span
Trace Graph
Jaeger
Zipkin
SkyWalking
OpenTelemetry Collector

但是代码设计要为未来 OpenTelemetry 留出扩展点。

也就是说:

现在:
TraceId + MDC

未来:
TraceId + SpanId + ParentSpanId + OpenTelemetry

不要把当前 TraceContext 设计成未来无法迁移的死结构。


二十一、需要先做代码分析

在开始修改代码之前:

  1. 扫描整个项目结构。
  2. 找到 Spring Boot 启动模块。
  3. 找到 Web 请求入口。
  4. 找到现有 Filter / Interceptor。
  5. 找到现有线程池。
  6. 找到 CompletableFuture。
  7. 找到 Quartz / @Scheduled。
  8. 找到 MQ Producer。
  9. 找到 MQ Consumer。
  10. 找到 RestTemplate / Feign / HTTP Client。
  11. 找到 Netty / MQTT / TCP / UDP 入口。
  12. 找到 Logback 配置。
  13. 找到现有 MDC / ThreadLocal / RequestContext 实现。
  14. 检查项目是否已经存在类似 TraceId / RequestId 机制。

如果已经存在类似能力:

优先重构和统一,不要重复造轮子。


二十二、实现原则

遵守:

低侵入
高复用
自动传播
自动清理
统一入口
统一出口

业务代码尽可能不需要修改。

理想效果:

原来的代码:

log.info("开始处理设备");

完全不用修改。

自动变成:

traceId=A 开始处理设备

二十三、测试要求

实现完成后必须增加测试。

至少测试:

Test 1:HTTP

请求进入
↓
生成 TraceId
↓
Controller
↓
Service
↓
日志
↓
Response Header

确认 TraceId 一致。

Test 2:HTTP携带 TraceId

X-Trace-Id=A

确认后端仍然使用:

A

Test 3:线程池

Web traceId=A
↓
ThreadPool

确认:

traceId=A

Test 4:MQ

Producer traceId=A
↓
MQ Header
↓
Consumer

确认 Consumer:

traceId=A

Test 5:MQ无 TraceId

确认 Consumer 自动生成。

Test 6:定时任务

确认:

TASK traceId=A

整个任务一致。

Test 7:线程池污染

执行:

请求A
请求B
请求C

确认:

A ≠ B ≠ C

并且不会出现线程复用导致 TraceId 串线。

Test 8:异常

异常发生后:

traceId

仍然存在于 ERROR 日志。

Test 9:并发

同时执行大量请求:

100+
1000+

确认 TraceId 不串线。


二十四、最终输出要求

完成后请给我:

1. 修改文件列表

例如:

新增:
xxx/TraceContext.java

修改:
xxx/Logback.xml
xxx/xxxFilter.java

2. 架构说明

说明:

Web
 ↓
TraceContext
 ↓
MDC

ThreadPool
 ↓
Context Propagation

MQ
 ↓
Message Header

HTTP
 ↓
X-Trace-Id

3. 使用示例

告诉我业务代码以后应该怎么写。

目标是:

log.info("开始处理");

而不是让业务代码手动管理 TraceId。

4. 测试结果

说明所有测试是否通过。

5. 风险

明确指出:

  • 哪些场景已经支持
  • 哪些场景暂未支持
  • 是否存在 MDC 丢失风险
  • 是否存在线程池上下文污染风险
  • 是否存在 MQ TraceId 丢失风险

6. 后续演进建议

最后说明未来如何从:

TraceId + MDC

演进到:

TraceId
+
SpanId
+
ParentSpanId
+
OpenTelemetry

但本次不要提前实现完整分布式链路追踪系统。


最终验收标准

只有同时满足以下条件才算完成:

                    traceId=A
                        │
                ┌───────┴───────┐
                │               │
              Web            Task
                │
                ▼
             Service
                │
        ┌───────┴───────┐
        ▼               ▼
     ThreadPool         MQ
        │               │
        │               ▼
        │            Consumer
        │               │
        └───────┬───────┘
                ▼
             AI/HTTP
                │
                ▼
              DB

同一业务链可以通过:

traceId=A

在日志文件、IDEA Console、Kibana 中完整检索。

尤其必须保证:

跨线程、跨线程池、跨 MQ、跨 HTTP 的 TraceId 不丢失;任务执行完成后上下文必须清理,不允许线程复用造成 TraceId 污染。

请先分析当前代码,再实施修改。

不要在没有检查项目现有实现的情况下直接创建重复的 TraceId / MDC / ThreadLocal 体系。