上周有个客户连续两天打电话投诉:"我点开课程详情页就报错,你们自己试试。"我们把客户的操作路径走了三遍,一切正常,日志里也搜不到任何异常。客户报了个大概时间——下午三点十二分——我们在 ELK 里按那个时间拉了几百条日志,根本分不清哪条是这个用户的,像大海捞针。
最后定位到的问题其实很简单:某一门课的视频地址配置错了,但因为日志里没有一条贯穿请求的标识,我们死活对不上。前后花了一天半把 traceId 这套东西补上,记录下来。
一、问题本质:日志是散的,串不起来
现在的系统都是多实例 + 异步线程 + 消息队列。一个用户请求进来:
- 网关打一条访问日志。
- 业务服务打一条处理日志。
- 发了个 MQ 消息,消费者在另一台机器打一条日志。
- 异常发生在某个异步线程里。
这四条日志散在不同机器、不同日志文件里,时间还不完全对齐。客户说"三点十二分",你只能按时间范围捞,根本不知道哪几条属于同一个请求。
二、解法:MDC 注入 traceId,一条请求一路带下去
核心思路:请求进来时生成一个全局唯一的 traceId,放在日志框架的 MDC(Mapped Diagnostic Context)里,这个请求经过的所有日志自动带上它:
// 网关/拦截器里生成 traceId
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId);
// 请求结束后清理
try {
// 整个业务处理
} finally {
MDC.clear();
}
日志格式里加上 %X{traceId}:
// logback pattern
%d{yyyy-MM-dd HH:mm:ss} [%thread] %X{traceId} %-5level %logger - %msg%n
这样每一条日志都长这样:
2026-10-12 15:12:03.211 [http-nio-8080-exec-3] a1b2c3d4... INFO 进入课程详情
2026-10-12 15:12:03.215 [http-nio-8080-exec-3] a1b2c3d4... ERROR 视频地址为空
客户只要能提供任意一条报错截图上的时间或订单号,我们就能反查 traceId,把这一次请求在所有服务、所有异步线程里的日志全捞出来。
三、两个必踩的坑:异步线程和 MQ 会丢 MDC
MDC 本质是 ThreadLocal,一旦切线程就没了。
坑 1:异步线程池。业务里用 @Async 或自己开线程池处理后续逻辑,子线程里 MDC 是空的,traceId 断了。要给线程池包一层任务装饰器,提交任务时把父线程的 MDC 复制过去:
// 包装 Runnable,执行前把父线程 MDC 放进子线程
public class MdcAwareRunnable implements Runnable {
private final Map<String, String> context = MDC.getCopyOfContextMap();
private final Runnable delegate;
public void run() {
if (context != null) MDC.setContextMap(context);
try { delegate.run(); } finally { MDC.clear(); }
}
}
坑 2:MQ 消息。发消息时把 traceId 塞进消息头,消费者收到后从消息头取出来放回 MDC,跨服务的链路就接上了。
四、ELK 里怎么按 traceId 查
日志统一收集到 ELK 后,查询很直接:
- 先按用户报错时间 + 用户ID 粗筛。
- 在结果里找到那条 ERROR 日志,复制它的 traceId。
- 用
traceId:a1b2c3d4...精确搜索,这一次请求在所有机器、所有服务的日志就全出来了。
这次那个客户的问题,就是补上 traceId 后,从一条"视频地址为空"的 ERROR 反查整条链路,三分钟定位到是课程配置里视频 URL 字段存成了空字符串。
五、踩坑清单(5 条实际踩过的)
- 日志格式不加
%X{traceId},MDC 里放了也白放,打印不出来。 - 异步线程池一定要包装 MDC 传递,否则 traceId 一到异步就断。
- MQ 跨服务要把 traceId 放消息头,别指望 ThreadLocal 跨进程。
- 请求结束必须
MDC.clear(),否则线程复用会串到下一个请求。 - traceId 要返回给前端(放响应头或报错弹窗里),客户报错截图直接带 traceId,省得反查。
| 场景 | traceId 传递方式 |
|---|---|
| 同服务同步调用 | MDC 自动带 |
| 异步线程池 | 包装 Runnable 复制 MDC |
| HTTP 跨服务调用 | 放请求头,下游拦截器取出 |
| MQ 消息 | 放消息头,消费者取出 |
这次线上日志链路补全是在乔拓云的教育系统后台上做的,我主要负责加网关 traceId 生成、包装异步线程池和 MQ 消息头透传那套逻辑,补完后同类"客户能复现我们查不到"的问题,平均定位时间从半天缩到十分钟。
复盘
- 线上问题查不到,多半不是没日志,而是日志散在多实例多线程里串不起来。
- MDC 注入 traceId 是成本最低收益最大的可观测改造,但要处理好异步线程和 MQ 两个断点。
- traceId 最好回传前端,客户报错截图自带链路标识。
以上是个人实践记录,各平台具体功能以官方实时信息为准。
你们做链路追踪是用 MDC + ELK 自己拼,还是直接上 SkyWalking / Jaeger 这类 APM?