分布式环境下,跨服务之间的调用错综复杂,若是忽然爆出一个错误,虽然有日志记录,但究竟是哪一个服务出了问题呢?是移动端传的参数有错误,仍是系统X或者系统Y提供的接口致使?在这种状况下,错误排查起来就很是费劲。前端
为了追踪一个请求完整的流转过程,我能够给请求分配一个惟一的traceId
,当请求调用其余服务时,咱们传递这个traceId
。在输出日志时,将这个traceId
打印到日志文件中,这样,从日志文件中,根据traceId
就能够分析一个请求完整的调用过程,若更进一步,还能够作性能分析。segmentfault
在一个服务的内部,咱们不但愿在调用每一个方法时,都带上traceId
这个参数(这样实在太蠢了- . -)。app
在Java中,咱们通常将traceId
放到ThreadLocal
中,这样在打印日志时,日志框架从ThreadLocal
取出traceId
,和其余须要打印的信息一块儿打印出来。这样对框架的使用者来讲,traceId
就是透明的,并不须要去关注它。框架
咱们来看代码实现:dom
/** * 创建日志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个方法:ide
咱们在afterCompletion
方法中对MDC
进行了clear
操做,底层调用了ThreadLocal
的remove
方法,清除当前线程中的线程局部变量。其做用有两个,一是防止ThreadLocal
致使的内存溢出,二是Tomcat
容器线程复用时,新请求会依旧使用原来的MDC
中的traceId
,会致使traceId
的"串码"现象。性能
咱们再来说一下preHandle
方法中的ctxOpId
,即咱们向MDC
中不单单写入http header
中的traceId
,还经过UUID生成了一个ctxOpId
。this
如上图,A服务的某个方法连续调用了B服务的某个接口3次(多是重试机制致使,也有可能确实是业务逻辑),如何区分这3次调用呢?只经过traceId
没法区分,由于这三次的traceId
都相同,因此每次调用时UUID生成ctxOpId
,来区分这三次调用。spa
而后在logback.xml文件中配置pattern
,以下:
<pattern>%d %-5level [%X{ctxTraceId}][%thread] %logger{5} - %msg%n</pattern>
具体打印日志时,会根据pattern
格式打印,各字段的含义可自行百度。
最后,当咱们在调用其余Http服务时,先获取当前线程的ThreadLocal
上下文,将traceId
写入http client
的header
中,从而达到跨服务传递traceId
。
这是一个简单的实现分布式调用追踪的实践,以上。