分布式调用跟踪实战

背景

分布式环境下,跨服务之间的调用错综复杂,若是忽然爆出一个错误,虽然有日志记录,但究竟是哪一个服务出了问题呢?是移动端传的参数有错误,仍是系统X或者系统Y提供的接口致使?在这种状况下,错误排查起来就很是费劲。前端

为了追踪一个请求完整的流转过程,我能够给请求分配一个惟一的traceId,当请求调用其余服务时,咱们传递这个traceId。在输出日志时,将这个traceId打印到日志文件中,这样,从日志文件中,根据traceId就能够分析一个请求完整的调用过程,若更进一步,还能够作性能分析。segmentfault

TraceID在Http服务中的实现

在一个服务的内部,咱们不但愿在调用每一个方法时,都带上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)来实现,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个方法:ide

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

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

咱们再来说一下preHandle方法中的ctxOpId,即咱们向MDC中不单单写入http header中的traceId,还经过UUID生成了一个ctxOpIdthis

alt text

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

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

原文连接

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

相关文章
相关标签/搜索