2

背景

分布式环境下,跨服务之间的调用错综复杂,如果突然爆出一个错误,虽然有日志记录,但到底是哪个服务出了问题呢?是移动端传的参数有错误,还是系统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)来实现,logbacklog4j支持MDCMDC的底层实现其实很容易理解,就是通过ThreadLocal来维护key-value,源码如下:

public final class LogbackMDCAdapter implements MDCAdapter {

    final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>>();
    ...
    ...
}

WebLogMdcHandlerInterceptor继承了HandlerInterceptorAdapterHandlerInterceptorAdapter是一个拦截器适配器,我们实现了它其中的2个方法:

  • preHandle: 实现处理器的预处理
  • afterCompletion: 整个请求处理完毕回调方法,可以进行一些资源清理

我们在afterCompletion方法中对MDC进行了clear操作,底层调用了ThreadLocalremove方法,清除当前线程中的线程局部变量。其作用有两个,一是防止ThreadLocal导致的内存溢出,二是Tomcat容器线程复用时,新请求会依旧使用原来的MDC中的traceId,会导致traceId的"串码"现象。

我们再来讲一下preHandle方法中的ctxOpId,即我们向MDC中不仅仅写入http header中的traceId,还通过UUID生成了一个ctxOpId

alt text

如上图,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 clientheader中,从而达到跨服务传递traceId

这是一个简单的实现分布式调用追踪的实践,以上。

原文链接

https://segmentfault.com/a/11...


扑火的蛾
272 声望30 粉丝

与世界分享你的装逼经验。