背景
在目前的微服务体系中,服务应用多,调用链复杂,相应的排查问题的难度也随之上升。当应用发生异常时,我们需要快速定位问题日志,这就需要我们对请求链路进行追踪,在请求到达系统时产生一个能够标识整个请求生命周期的ID。
MDC(Mapped Diagnostic Contexts)介绍
MDC是Slf4J类日志系统中实现分布式多线程日志数据传递的重要工具,用户可利用MDC将一些运行时的上下文数据打印出来。目前只有log4j和logback提供原生的MDC支持
源码分析
MDC在线程上下文中了维护一个 Map<String,String> 属性,可以理解为一个线程级别的容器,在logback中,可以通过%X{key}获取MDC上下文中的值
MDC提供的接口
public interface MDCAdapter {
// 获取当前线程MDC上下文中指定key的值
void put(String var1, String var2);
// 往当前线程MDC上下文中
String get(String var1);
// 移除当前线程MDC上下文中指定key的键值
void remove(String var1);
// 清空MDC上下文
void clear();
// 获取MDC上下文
Map<String, String> getCopyOfContextMap();
// 设置MDC上下文呢
void setContextMap(Map<String, String> var1);
}
logback的实现LogbackMDCAdapter
public class LogbackMDCAdapter implements MDCAdapter {
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
private static final int WRITE_OPERATION = 1;
private static final int MAP_COPY_OPERATION = 2;
final ThreadLocal<Integer> lastOperation = new ThreadLocal();
public LogbackMDCAdapter() {
}
...
可以看到LogbackMDC声明了类型为ThreadLocal的map。ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本,也就是说ThreadLocal变量在线程之间隔离而在方法或类间能够共享
LogbackMDCAdapter中的put()
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
} else {
Map<String, String> oldMap = (Map)this.copyOnThreadLocal.get();
Integer lastOp = this.getAndSetLastOperation(1);
if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
oldMap.put(key, val);
} else {
Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
}
}
}
校验并把键值设值到ThreadLocal的容器中
duplicateAndInsertNewMap()
private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
Map<String, String> newMap = Collections.synchronizedMap(new HashMap());
if (oldMap != null) {
synchronized(oldMap) {
newMap.putAll(oldMap);
}
}
this.copyOnThreadLocal.set(newMap);
return newMap;
}
创建线程安全的HashMap作为容器,并放到ThreadLocal中
方案与实现
声明过滤器对请求拦截
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean registFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DyeFilter());
registration.addUrlPatterns("/*");
registration.setName("DyeFilter");
registration.setOrder(1);
return registration;
}
}
构建上下文对象,并赋值到MDC
@WebFilter(filterName = "DyeFilter")
public class DyeFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
initGlobalContext(request);
try {
chain.doFilter(req, resp);
} finally {
ContextUtil.clearContext();
}
}
private static void initGlobalContext(HttpServletRequest servletRequest) {
GlobalContext context = new GlobalContext();
String contextStr = servletRequest.getHeader(ContextConstant.REQUEST_CONTEXT);
if (!StringUtils.isEmpty(contextStr)){
context = JSON.parseObject(contextStr,GlobalContext.class);
}else{
context.setTraceId(UUID.randomUUID().toString());
context.setClientIp(IpUtil.getClientAddress(servletRequest));
}
ContextUtil.setCurrentContext(context);
}
@Override
public void init(FilterConfig config){
}
@Override
public void destroy() {
}
}
在业务入口构建GlobalContext,在后续的调用链中,将从请求头中获取构建好的GlobalContext
public class ContextUtil {
private static ThreadLocal<GlobalContext> currentThreadLocal = ThreadLocal.withInitial(GlobalContext::new);
public static void setCurrentContext(GlobalContext context) {
currentThreadLocal.set(context);
String traceId = context.getTraceId();
if (traceId != null && traceId.length() > 0 && MDC.get(ContextConstant.TRACK_ID) == null) {
MDC.put(ContextConstant.TRACK_ID, traceId);
}
}
public static GlobalContext getCurrentContext() {
return currentThreadLocal.get();
}
public static void clearContext() {
MDC.clear();
currentThreadLocal.remove();
}
}
获取GlobalContext,并通过请求头透传
public class FeignConfig {
@Bean
public RequestInterceptor header(){
return this::setRequestHeader;
}
private void setRequestHeader(RequestTemplate requestTemplate){
GlobalContext context = ContextUtil.getCurrentContext();
if (context!=null){
requestTemplate.header(ContextConstant.REQUEST_CONTEXT, JSONParser.quote(JSON.toJSONString(context)));
}
}
}
这里只举例通过Feign调用的方式,服务之间调用还有很多种,主要思路就是把GlobalContext透传到下一个服务
在logback配置文件中配置MDC容器中的变量%X{trackId}
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %X{trackId} [%15.15t] %class{39}.%method[%L] : %m%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>f
</encoder>
</appender>l
DEMO地址: github.com/chenxuancod…