背景
分布式环境下,跨服务之间的调用错综复杂,如果突然爆出一个错误,虽然有日志记录,但到底是哪个服务出了问题呢?是移动端传的参数有错误,还是系统X或者系统Y提供的接口导致?在这种情况下,错误排查起来就非常费劲。
为了追踪一个请求完整的流转过程,我可以给请求分配一个唯一的traceId
,当请求调用其他服务时,我们传递这个traceId
。在输出日志时,将这个traceId
打印到日志文件中,这样,从日志文件中,根据traceId
就可以分析一个请求完整的调用过程,若更进一步,还可以做性能分析。
TraceID在Http服务中的实现
在一个服务的内部,我们不希望在调用每个方法时,都带上traceId
这个参数(这样实在太蠢了- . -)。
在Java中,我们一般将traceId
放到ThreadLocal
中,这样在打印日志时,日志框架从ThreadLocal
取出traceId
,和其他需要打印的信息一起打印出来。这样对框架的使用者来说,traceId
就是透明的,并不需要去关注它。
我们来看代码实现:
/**
* 建立日志MDC上下文属性的拦截器
*/
public class WebLogMdcHandlerInterceptor extends HandlerInterceptorAdapter {
/**
* traceId一般由前端的负载生成,比如Nignx
*/
private boolean generateTraceId = false;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ctxTraceId = null;
String ctxOpId = null;
// 判断Http header中是否有traceId字段,如果没有,则通过随机数生成
if (StringUtils.isNotBlank(request.getHeader(Conventions.TRACE_ID_HEADER))) {
ctxTraceId = request.getHeader(Conventions.TRACE_ID_HEADER);
} else if (generateTraceId) {
ctxTraceId = getTraceId();
}
ctxOpId = UUID.randomUUID().toString();
MDC.put(Conventions.CTX_TRACE_ID_MDC, ctxTraceId + "," + ctxOpId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
MDC.clear();
}
// 通过随机数生成traceId,也可以通过其他方式实现,只要保证唯一即可
private static String getTraceId() {
Random random = new Random();
String rs1 = String.valueOf(random.nextInt(10000));
String rs2 = String.valueOf(random.nextInt(10000));
return rs1 + rs2;
}
public void setGenerateTraceId(boolean generateTraceId) {
this.generateTraceId = generateTraceId;
}
}
实现其实比较简单,使用MDC
(Mapped Diagnostic Contexts)来实现,logback
和log4j
支持MDC
,MDC
的底层实现其实很容易理解,就是通过ThreadLocal
来维护key-value
,源码如下:
public final class LogbackMDCAdapter implements MDCAdapter {
final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>();
...
...
}
WebLogMdcHandlerInterceptor
继承了HandlerInterceptorAdapter
,HandlerInterceptorAdapter
是一个拦截器适配器,我们实现了它其中的2个方法:
- preHandle: 实现处理器的预处理
- afterCompletion: 整个请求处理完毕回调方法,可以进行一些资源清理
我们在afterCompletion
方法中对MDC
进行了clear
操作,底层调用了ThreadLocal
的remove
方法,清除当前线程中的线程局部变量。其作用有两个,一是防止ThreadLocal
导致的内存溢出,二是Tomcat
容器线程复用时,新请求会依旧使用原来的MDC
中的traceId
,会导致traceId
的"串码"现象。
我们再来讲一下preHandle
方法中的ctxOpId
,即我们向MDC
中不仅仅写入http header
中的traceId
,还通过UUID生成了一个ctxOpId
。
如上图,A服务的某个方法连续调用了B服务的某个接口3次(可能是重试机制导致,也有可能确实是业务逻辑),如何区分这3次调用呢?只通过traceId
无法区分,因为这三次的traceId
都相同,所以每次调用时UUID生成ctxOpId
,来区分这三次调用。
然后在logback.xml文件中配置pattern
,如下:
<pattern>%d %-5level [%X{ctxTraceId}][%thread] %logger{5} - %msg%n</pattern>
具体打印日志时,会根据pattern
格式打印,各字段的含义可自行百度。
最后,当我们在调用其他Http服务时,先获取当前线程的ThreadLocal
上下文,将traceId
写入http client
的header
中,从而达到跨服务传递traceId
。
这是一个简单的实现分布式调用追踪的实践,以上。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。