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

1213 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 任务: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
项目未来会逐步发展为:
```text
单体应用
↓
IoT + SaaS + AI
↓
微服务
↓
OpenTelemetry / 分布式链路追踪
```
因此这次实现不能只是简单地增加一个 MDC 字段,而要设计成可长期演进的基础框架。
---
# 二、核心目标
实现统一的:
```text
TraceContext
```
核心字段至少包括:
```text
traceId
tenantId
userId
```
其中当前阶段最重要的是:
```text
traceId
```
核心原则:
> 一次完整业务执行链使用同一个 traceId。
例如:
```text
前端请求
↓
Controller
↓
Service
↓
线程池
↓
MQ
↓
MQ Consumer
↓
AI 服务
↓
数据库
↓
通知
```
如果它们属于同一次业务执行,则全部使用:
```text
traceId = A
```
---
# 三、TraceId 生命周期规则
必须严格遵循以下规则。
## 1. 有上游 TraceId
优先继承。
例如 HTTP:
```http
X-Trace-Id: 01KABC...
```
后端收到后:
```text
TraceId = 01KABC...
```
不要重新生成。
---
## 2. 没有上游 TraceId
自动创建新的 TraceId。
例如:
```text
浏览器
↓
HTTP
↓
Java
```
如果 HTTP 没有:
```http
X-Trace-Id
```
则 Java 自动生成。
---
## 3. 定时任务没有上游 TraceId
Quartz / @Scheduled 等任务启动时自动生成:
```text
traceId
```
例如:
```text
traceId=T202609150001
```
整个任务执行过程保持相同 TraceId。
---
# 四、TraceId 生成器
设计独立:
```java
TraceIdGenerator
```
不要把 TraceId 生成逻辑写死在 Filter 中。
要求:
```text
TraceContext
TraceIdGenerator
```
职责分离。
优先考虑 ULID 或项目中已有的高质量 ID 生成方案。
要求:
- 唯一
- 高并发安全
- 尽量时间有序
- 适合日志检索
- 不依赖数据库
- 后续可以替换实现
---
# 五、TraceContext 设计
创建统一:
```java
TraceContext
```
例如:
```text
TraceContext
├── getTraceId()
├── setTraceId()
├── getOrCreateTraceId()
├── set()
├── get()
├── clear()
└── snapshot()
```
不要让业务代码直接到处:
```java
MDC.put("traceId", ...)
```
业务代码应该依赖:
```java
TraceContext
```
MDC 只是 TraceContext 的一种底层日志传播实现。
核心原则:
```text
TraceContext ≠ MDC
```
而应该是:
```text
TraceContext
↓
MDC
HTTP Header
MQ Header
ThreadPool
Future
```
---
# 六、Web 请求
实现 Spring MVC Filter。
建议:
```java
OncePerRequestFilter
```
功能:
1. 从 HTTP Header 获取:
```http
X-Trace-Id
```
2. 如果存在且合法:
```text
继续使用
```
3. 如果不存在:
```text
生成 TraceId
```
4. 放入:
```text
TraceContext
MDC
```
5. 将 TraceId 写入 HTTP Response Header:
```http
X-Trace-Id: 01KABC...
```
6. 请求结束后必须清理:
```text
MDC
TraceContext
```
防止 Tomcat 线程池复用造成 TraceId 污染。
---
# 七、日志输出
修改 Logback 日志格式,使所有日志自动包含:
```text
traceId
```
例如:
```text
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 日志扩展能力。
---
# 八、线程池上下文传播
这是本任务的重点。
普通:
```java
ThreadPoolExecutor
```
或者:
```java
CompletableFuture
```
不能保证 MDC 自动传播。
必须实现统一的上下文传播机制。
例如:
```text
TraceContext
↓
snapshot
↓
Runnable / Callable
↓
restore
↓
执行
↓
clear
```
要求实现类似:
```text
TraceContextRunnable
TraceContextCallable
TraceableExecutor
```
或者使用 Spring 提供的:
```text
TaskDecorator
```
如果项目使用 Spring ThreadPoolTaskExecutor,优先通过:
```text
TaskDecorator
```
实现。
目标:
```text
Web线程
traceId=A
↓
线程池
traceId=A
```
例如:
```java
executor.execute(() -> {
log.info("异步处理");
});
```
日志必须仍然是:
```text
traceId=A
```
---
# 九、CompletableFuture
重点处理:
```java
CompletableFuture.runAsync(...)
CompletableFuture.supplyAsync(...)
thenApply(...)
thenAccept(...)
thenRun(...)
```
分析项目中现有异步代码。
不要简单粗暴地修改所有业务代码。
优先通过统一线程池:
```text
TraceableExecutor
```
解决。
如果无法完全覆盖 CompletableFuture 的默认线程池,需要明确指出风险,并提供统一解决方案。
禁止让业务代码到处手动:
```java
MDC.put(...)
MDC.clear(...)
```
---
# 十、MQ TraceId 传播
这是第二个重点。
当 Java 发送 MQ 消息时:
必须把当前:
```text
traceId
```
写入 MQ Message Header / Properties。
例如:
```text
traceId=A
```
发送:
```text
MQ Message
├── Header
│ └── traceId=A
└── Body
```
Consumer 收到消息:
```text
MQ Header
↓
读取 traceId
↓
TraceContext.setTraceId()
↓
MDC
↓
业务处理
```
这样:
```text
Web
traceId=A
↓
MQ Producer
traceId=A
↓
MQ
traceId=A
↓
Consumer
traceId=A
```
如果 MQ 消息没有 TraceId:
```text
Consumer
↓
自动生成新的 TraceId
```
但正常情况下必须继承 Producer 的 TraceId。
---
# 十一、MQ 不同消息中间件
先检查当前项目实际使用的 MQ。
不要凭空假设。
如果项目目前只有一种 MQ:
优先实现当前实际使用的 MQ。
同时将设计抽象成:
```text
TraceMessageInterceptor
```
未来可以支持:
```text
RabbitMQ
Kafka
RocketMQ
Redis Stream
其他 MQ
```
不要为了扩展性一次性实现大量没有使用的 MQ。
---
# 十二、HTTP 调用链
如果 Java 单体项目会调用其他服务:
例如:
```text
Java
↓ HTTP
Python AI
```
必须自动传递:
```http
X-Trace-Id: A
```
如果项目使用:
```text
RestTemplate
WebClient
OpenFeign
HttpClient
OkHttp
```
检查实际使用情况。
优先通过统一 Interceptor 实现。
例如:
```text
Java
traceId=A
↓
HTTP Header
X-Trace-Id=A
↓
Python AI
traceId=A
```
这样 Java 和 Python 日志可以使用同一个 TraceId 检索。
---
# 十三、Netty / MQTT / TCP / UDP
这是 IoT 项目的重要场景。
检查项目现有:
```text
Netty
MQTT
TCP
UDP
CoAP
```
入口。
对于每一个独立的消息 / 请求:
如果存在上游 TraceId:
```text
继承
```
否则:
```text
生成
```
然后进入:
```text
TraceContext
```
例如:
```text
设备 DEV001
↓
MQTT
↓
traceId=M001
↓
数据解析
↓
物模型
↓
数据库
↓
告警
```
整条处理链使用:
```text
traceId=M001
```
注意:
> 设备每次独立上报消息属于独立业务事件,因此通常应该生成新的 TraceId,而不是让一个设备永久使用同一个 TraceId。
---
# 十五、SaaS 上下文
项目未来是 SaaS,因此 TraceContext 预留:
```text
traceId
tenantId
userId
```
如果当前项目已经有:
```text
tenantId
userId
```
则在请求认证成功后自动放入上下文。
例如:
```text
traceId=A
tenantId=10001
userId=9527
```
但不要因为本次任务而大范围修改现有权限系统。
只需要提供统一上下文承载能力。
---
# 十六、线程安全
这是重点。
必须考虑:
```text
Tomcat线程池复用
Quartz线程池
业务线程池
CompletableFuture
MQ Consumer线程
Netty EventLoop
```
任何任务结束后都必须:
```text
clear
```
禁止出现:
```text
请求A
traceId=A
请求结束
请求B
traceId=A
```
这种严重的数据串链问题。
---
# 十七、异常处理
异常情况下 TraceId 也必须保留。
例如:
```text
traceId=A
ERROR
设备数据处理失败
```
完整异常堆栈必须能够通过:
```text
traceId=A
```
检索出来。
Web 异常响应最好也返回:
```http
X-Trace-Id: A
```
这样前端报错:
```text
TraceId: A
```
我就可以直接去 Kibana / 日志文件搜索:
```text
traceId=A
```
定位问题。
---
# 十八、日志检索目标
最终必须支持:
```bash
grep "traceId=01KABC" app.log
```
或者 Kibana:
```text
traceId:"01KABC"
```
一次找到:
```text
HTTP请求
Service
数据库
线程池
MQ Producer
MQ Consumer
AI
异常
```
---
# 十九、代码结构要求
根据当前项目实际结构,合理放置。
如果存在类似:
```text
common
framework
core
starter
```
优先放到公共基础模块。
推荐结构:
```text
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
```
具体目录必须根据当前项目实际结构调整,不要机械套用。
---
# 二十、不要过度设计
当前项目是单体项目。
第一阶段:
必须实现:
```text
TraceId
MDC
Web
ThreadPool
Quartz
MQ
HTTP
```
暂时不要自行实现完整:
```text
Span
Trace Graph
Jaeger
Zipkin
SkyWalking
OpenTelemetry Collector
```
但是代码设计要为未来 OpenTelemetry 留出扩展点。
也就是说:
```text
现在:
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 机制。
如果已经存在类似能力:
> 优先重构和统一,不要重复造轮子。
---
# 二十二、实现原则
遵守:
```text
低侵入
高复用
自动传播
自动清理
统一入口
统一出口
```
业务代码尽可能不需要修改。
理想效果:
原来的代码:
```java
log.info("开始处理设备");
```
完全不用修改。
自动变成:
```text
traceId=A 开始处理设备
```
---
# 二十三、测试要求
实现完成后必须增加测试。
至少测试:
### Test 1:HTTP
```text
请求进入
↓
生成 TraceId
↓
Controller
↓
Service
↓
日志
↓
Response Header
```
确认 TraceId 一致。
### Test 2:HTTP携带 TraceId
```text
X-Trace-Id=A
```
确认后端仍然使用:
```text
A
```
### Test 3:线程池
```text
Web traceId=A
↓
ThreadPool
```
确认:
```text
traceId=A
```
### Test 4:MQ
```text
Producer traceId=A
↓
MQ Header
↓
Consumer
```
确认 Consumer:
```text
traceId=A
```
### Test 5:MQ无 TraceId
确认 Consumer 自动生成。
### Test 6:定时任务
确认:
```text
TASK traceId=A
```
整个任务一致。
### Test 7:线程池污染
执行:
```text
请求A
请求B
请求C
```
确认:
```text
A ≠ B ≠ C
```
并且不会出现线程复用导致 TraceId 串线。
### Test 8:异常
异常发生后:
```text
traceId
```
仍然存在于 ERROR 日志。
### Test 9:并发
同时执行大量请求:
```text
100+
1000+
```
确认 TraceId 不串线。
---
# 二十四、最终输出要求
完成后请给我:
## 1. 修改文件列表
例如:
```text
新增:
xxx/TraceContext.java
修改:
xxx/Logback.xml
xxx/xxxFilter.java
```
## 2. 架构说明
说明:
```text
Web
↓
TraceContext
↓
MDC
ThreadPool
↓
Context Propagation
MQ
↓
Message Header
HTTP
↓
X-Trace-Id
```
## 3. 使用示例
告诉我业务代码以后应该怎么写。
目标是:
```java
log.info("开始处理");
```
而不是让业务代码手动管理 TraceId。
## 4. 测试结果
说明所有测试是否通过。
## 5. 风险
明确指出:
- 哪些场景已经支持
- 哪些场景暂未支持
- 是否存在 MDC 丢失风险
- 是否存在线程池上下文污染风险
- 是否存在 MQ TraceId 丢失风险
## 6. 后续演进建议
最后说明未来如何从:
```text
TraceId + MDC
```
演进到:
```text
TraceId
+
SpanId
+
ParentSpanId
+
OpenTelemetry
```
但本次不要提前实现完整分布式链路追踪系统。
---
# 最终验收标准
只有同时满足以下条件才算完成:
```text
traceId=A
│
┌───────┴───────┐
│ │
Web Task
│
▼
Service
│
┌───────┴───────┐
▼ ▼
ThreadPool MQ
│ │
│ ▼
│ Consumer
│ │
└───────┬───────┘
▼
AI/HTTP
│
▼
DB
```
同一业务链可以通过:
```text
traceId=A
```
在日志文件、IDEA Console、Kibana 中完整检索。
尤其必须保证:
> **跨线程、跨线程池、跨 MQ、跨 HTTP 的 TraceId 不丢失;任务执行完成后上下文必须清理,不允许线程复用造成 TraceId 污染。**
请先分析当前代码,再实施修改。
不要在没有检查项目现有实现的情况下直接创建重复的 TraceId / MDC / ThreadLocal 体系。