痛点
查线上日志时,同一个 Pod 内多线程日志交错,很难追踪每个请求对应的日志信息。
日志收集工具将多个 Pod 的日志收集到同一个数据库中后,情况就更加混乱不堪了。
解决
TraceId + MDC
MDC:
https://logback.qos.ch/manual/mdc.html
- 前端每次请求时,添加
X-App-Trace-Id
请求头,X-App-Trace-Id
值的生成方式可以选择【时间戳 + UUID
】,保证 traceId 的唯一性。 - 后端在 TraceIdFilter 中取出
X-App-Trace-Id
的值:String traceId = httpServletRequest.getHeader(TRACE_ID_HEADER_KEY)
。如果请求没有携带X-App-Trace-Id
请求头,后端服务可以使用 UUID 或者 Snowflake 算法生成一个 traceId。 - 将 traceId 塞到 slf4j MDC 中:
MDC.put(MDC_TRACE_ID_KEY, traceId)
,在 logback pattern 中使用%X{traceId}
占位符打印 traceId。
整合 Feign
发起服务间调用时,需要将 MDC 中的 traceId 传递到被调用服务。我们项目中统一使用 Feign Client,实现服务间的 HTTP 远程调用,在 Feign RequestInterceptor
中,取出 MDC 中的 traceId,塞到请求头中:requestTemplate.header(TRACE_ID_HEADER_KEY, MDC.get(MDC_TRACE_ID_KEY));
多线程适配
Please note that MDC as implemented by logback-classic assumes that values are placed into the MDC with moderate frequency. Also note that a child thread does not automatically inherit a copy of the mapped diagnostic context of its parent.
在子线程执行任务前,将父线程的 MDC 内容设置到子线程的 MDC 中;在子线程任务执行完成后,清除子线程 MDC 中的内容。
适配 JDK ThreadPoolExecutor:
适配 Spring TaskDecorator:
MdcTaskUtils#adaptMdcRunnable()
:采用装饰者模式,装饰原生的 Runnable runnable
对象,在原生 Runnable 对象执行前,将父线程的 MDC 设置到子线程中,在原生 Runnable 对象执行结束后,清除子线程 MDC 中的内容。