描述
在微服务的体系中,服务应用较多,调用链复杂多变,相应的日志排查难度也随之提升。在整个请求的生命周期,链路追踪能够生成一个全局的唯一标识ID。每一条log日志记录携带整个唯一ID输出到控制台或者日志文件中,就可以快速定位到问题所在的整个请求链。
MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文),即将一些运行时的上下文数据通过logback打印输出,是一种方便在多线程条件下记录日志的功能。和SiftingAppender一起,可以实现根据运行时的上下文数据,将日志保存到不同的文件中。其底层通过ThreadLocal线程隔离实现的,保证了线程安全问题。但也ThreadLocal类实现了弱引用,可能会导致内存溢出等问题,所以在使用完之后需要进行手动移除。
工具类
/**
* 链路追踪工具类
*
* @author: 苦瓜不苦
* @date: 2022/6/28 21:09
**/
public class MdcUtil {
/**
* 链路追踪唯一key值
*/
public final static String UNIQUE_ID = "uniqueId";
/**
* 添加
*
* @param value 唯一ID
*/
public static void pull(String value) {
MDC.put(UNIQUE_ID, Objects.isNull(value) ? IdUtil.objectId() : value);
}
/**
* 添加
*/
public static void pull() {
MDC.put(UNIQUE_ID, IdUtil.objectId());
}
/**
* 获取
*
* @return
*/
public static String get() {
return MDC.get(UNIQUE_ID);
}
/**
* 获取
*
* @param key 键值
* @return
*/
public static String get(String key) {
String value = MDC.get(UNIQUE_ID);
if (Objects.isNull(value)) {
value = IdUtil.objectId();
}
return value;
}
/**
* 获取所有
*
* @return
*/
public static Map<String, String> getMap() {
return MDC.getCopyOfContextMap();
}
/**
* 添加所有
*
* @param contextMap mdc对象
*/
public static void setMap(Map<String, String> contextMap) {
if (!contextMap.isEmpty()) {
MDC.setContextMap(contextMap);
} else {
pull();
}
}
/**
* 清除
*
* @param key 键值
*/
public static void remove(String key) {
MDC.remove(key);
}
/**
* 清除所有
*/
public static void clear() {
MDC.clear();
}
}
日志文件配置
重点是%X{uniqueId},和MDC中的键名称保持一致。
%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%-5level){green} %X{uniqueId} %-5.5L %clr(%-40.40logger{36}){cyan} : %clr(%M){magenta} - %msg%xEx%n
拦截器
/**
* web配置
*
* @author: 苦瓜不苦
* @date: 2022/1/4 20:34
*/
@Slf4j
@Configuration
public class WebMcvConfig implements WebMvcConfigurer {
/**
* 重写拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptors())
.addPathPatterns("/**")
.excludePathPatterns("/doc.html", "/webjars/**", "/favicon.ico", "/swagger-resources/**");
}
/**
* 自定义拦截器
*
* @return
*/
private HandlerInterceptor interceptors() {
return new HandlerInterceptor() {
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
// 获取请求头中的链路追踪唯一ID,避免HTTP远程调用
MdcUtil.pull(request.getHeader(MdcUtil.UNIQUE_ID));
// 放行
return true;
}
@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) throws Exception {
// 清空MDC日志数据,防止内存泄露
MdcUtil.clear();
}
};
}
}
存在问题
至此基本功能已经实现,但是存在以下问题
- 多线程情况下,开启子线程打印日志会丢失链路追踪唯一ID
- HTTP跨服务远程调用也会丢失全局唯一ID
线程丢失情况
原因:MDC底层使用ThreadLocal线程隔离,如果切换线程则会造成数据丢失的问题
解决:重写MDCAdapter接口,使其通过阿里所提供的TransmittableThreadLocal类
TransmittableThreadLocal 是 Alibaba 开源的、用于解决 “在使用线程池等会缓存线程的组件情况下传递 ThreadLocal” 问题的 InheritableThreadLocal 扩展。
源码: gitee.com/smallSevenC… 项目名:logback-transmittable-threadLocal
百度云盘: 链接: pan.baidu.com/s/1Y5TJmHzv… 提取码: 7pcq
添加maven依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.6</version>
</dependency>
<!--
gitee地址: https://gitee.com/smallSevenCode/starter.git
项目名:logback-transmittable-threadLocal
打包到本地maven仓库中引入,里面对MDCAdapter进行重写
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>logback-transmittable-threadLocal</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
远程调用情况
原因:feign进行HTTP远程调用时,未将唯一ID携带过去。下游服务无法从header请求头中获取到唯一ID值
解决:重写RequestInterceptor接口,将上游的MDC链路追踪的唯一ID存放到HttpServletRequest的header请求头中。这样,下游服务就会从头信息中获取到唯一ID值。下游服务在拦截器中,获取值并且将ID值重新设置到MDC中
/**
* feign远程调用,请求头下游转发
*
* @author: 苦瓜不苦
* @date: 2021/8/14 0:20
**/
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
// 获取链路追踪ID值,并设置到下游请求头中
requestTemplate.header(MdcUtil.UNIQUE_ID,MdcUtil.get());
// cookie
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
requestTemplate.header(cookie.getName(), cookie.getValue());
}
}
// header
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
// 便利请求头属性,转发到下游服务
requestTemplate.header(name, value);
}
}
}
}
结束语
链路追踪最关键的点在于,保证整个请求的生命周期全局的唯一ID性。从主线程开启子线程以及调用其他的微服务,整个唯一ID值都不能够存在丢失的问题。上诉两种重写方法,解决了将数据传递下去,保证数据丢失,保证链路追踪的请求完整性。最后一点就是在使用完MDC数据后,记得清空数据,避免内存泄漏问题。