资讯专栏INFORMATION COLUMN

分布式调用跟踪实战

jlanglang / 2128人阅读

摘要:为了追踪一个请求完整的流转过程,我可以给请求分配一个唯一的,当请求调用其他服务时,我们传递这个。这是一个简单的实现分布式调用追踪的实践,以上。

背景

分布式环境下,跨服务之间的调用错综复杂,如果突然爆出一个错误,虽然有日志记录,但到底是哪个服务出了问题呢?是移动端传的参数有错误,还是系统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> copyOnInheritThreadLocal = new InheritableThreadLocal>();
    ...
    ...
}

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

preHandle: 实现处理器的预处理

afterCompletion: 整个请求处理完毕回调方法,可以进行一些资源清理

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

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

如上图,A服务的某个方法连续调用了B服务的某个接口3次(可能是重试机制导致,也有可能确实是业务逻辑),如何区分这3次调用呢?只通过traceId无法区分,因为这三次的traceId都相同,所以每次调用时UUID生成ctxOpId,来区分这三次调用。

然后在logback.xml文件中配置pattern,如下:

%d %-5level [%X{ctxTraceId}][%thread] %logger{5} - %msg%n

具体打印日志时,会根据pattern格式打印,各字段的含义可自行百度。

最后,当我们在调用其他Http服务时,先获取当前线程的ThreadLocal上下文,将traceId写入http clientheader中,从而达到跨服务传递traceId

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

原文链接

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70501.html

相关文章

  • Java调用跟踪关键技术(一)总体介绍

    摘要:微服务中调用栈的获取,使用的开发者会很自然想到用来拦截,但是拦截同一个类的多个方法之间的调用很不方便,有侵入性,因此并不适合。调用栈的跟踪也提供了这个能力,可以获得当前方法的调用栈信息。 一、调用链跟踪的作用 调用链跟踪包括 1.前端到后端的调用链 2.单个服务内部方法之间的调用链 3.微服务之间的调用链 4.应用服务和数据库之间的调用链 5.应用服务和第三方服务中...

    gaara 评论0 收藏0
  • Java调用跟踪关键技术(三)线程变量

    摘要:除了以上级别的成员变量共享,在调用链跟踪时要能识别不同分层下的多个类实例的调用是同一个请求,而这个请求的调用都在一个独立线程内完成,此时就要用到线程级变量共享。 一、Java类成员作用域 JAVA类成员作用域参考下图: showImg(https://segmentfault.com/img/bVbvWlh?w=1695&h=925); Java虚拟机级作用域,通过在类成员变量前加...

    ThreeWords 评论0 收藏0
  • Web全栈应用之旅-启程篇

    摘要:但能拷贝图粘贴后不失真通常是收费富文本编辑器才具备的能力。是否支持编程语言高亮,例如按,语言高亮是否支持数学公式等等因此选择了两款富文本编辑器,支持截屏粘贴,当做跟踪系统时这个功能特别有用。 一、Web应用技术栈 在开发Web应用时,通常会使用到以下技术栈: showImg(https://segmentfault.com/img/bVbwceG);对应这些技术栈都已有相应的开源产品...

    longmon 评论0 收藏0
  • Flink实战(八) - Streaming Connectors 编程

    摘要:默认情况下,当数据元到达时,分段接收器将按当前系统时间拆分,并使用日期时间模式命名存储区。如果需要,可以使用数据元或元组的属性来确定目录。这将调用传入的数据元并将它们写入部分文件,由换行符分隔。消费者的消费者被称为或等。 1 概览 1.1 预定义的源和接收器 Flink内置了一些基本数据源和接收器,并且始终可用。该预定义的数据源包括文件,目录和插socket,并从集合和迭代器摄取数据...

    beita 评论0 收藏0

发表评论

0条评论

jlanglang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<