分布式系统间请求跟踪

一、请求跟踪基本原理

现在的很多应用都是由很多系统系统在支持,这些系统还会部署很多个实例,用户的一个请求可能在多个系统的部署实例之间流转。为了跟踪一个请求的完整处理过程,我们可以给请求分配一个唯一的 ID traceID,当请求调用到另一个系统时,我们传递这个 traceID。在输出日志时,把这个 traceID 也输出到日志里,这样,根据日志文件,提取出现这个 traceID 的日志就可以分析这个请求的完整调用过程,甚至进行性能分析。

当然,在一个系统内部,我们不希望每次调用一个方法时都要传递这个 traceID,因此在 Java 里,一般把这个 traceID 放到某种形式的 ThreadLocal 变量里。

日志类库在输出日志时,就从这个 ThreadLocal 变量里取出 traceID,跟要输出的日志信息一起写入日志文件。

这样对于应用的开发者来说,基本不需要关注这个 traceID

二、远程调用间传递跟踪信息

如何使用的是自定义的 RPC 实现,这些 RPC 一般都预留了扩展机制来传递一些自定义的信息,我们可以把 traceID 作为自定义信息进行传递。

对于 Hessian 这种基于 HTTP 协议的 RPC 方法,它把序列化后的调用信息作为 POST 的请求体。如果我们不想去修改 Hessian 的调用机制,可以把 traceID 放到 HTTP 的请求头里。

在客户端只需要提供封装好的 RPC 调用代理。

在服务端通过 Filter 得到 traceID 放入 ThreadLocal 变量。

三、线程间传递跟踪信息

在实际的应用中,我们还可能把请求交给别的线程去异步处理,这就涉及在线程间传递 traceID

当我们直接用 java.util.concurrent.ExecutorService.submit(Runnable task) 提交一个任务时,显然是不会自动传递这个 traceID 的,我们需要做一些封装来透明地传递 traceID

下面是一个可跟踪任务的定义,利用 org.slf4j.MDC 来保存旧线程里的 traceID,在新线程执行子类任务时初始化跟踪信息。

package net.coderbee.util.concurrent;

import org.slf4j.MDC;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * Created by coderbee on 2017/4/21.
 */
class TraceableTask {
    private Map<String, String> context;

    public TraceableTask() {
        context = MDC.getCopyOfContextMap();
    }

    protected void clearContext() {
        MDC.clear();
    }

    void initContext() {
        if (context == null || context.isEmpty()) {
            init();
        } else {
            MDC.setContextMap(context);
        }
    }

    private void init() {
        Map<String, String> context = new HashMap<String, String>();
        String traceID = UUID.randomUUID().toString().replace("-", "");
        context.put("trace_id", traceID);

        MDC.setContextMap(context);
    }

}

Runnable, Callable<T> 分别定义一个子类如下:

package net.coderbee.util.concurrent;

public abstract class TraceableRunnable extends TraceableTask
        implements Runnable {

    public final void run() {
        super.initContext();

        run0();
    }

    protected abstract void run0();
}


package net.coderbee.util.concurrent;

import java.util.concurrent.Callable;

public abstract class TraceableCallable<T> extends TraceableTask
        implements Callable<T> {

    public final T call() {
        super.initContext();

        return call0();
    }

    protected abstract T call0();
}

这两个子类利用了 Java 在对象实例化时总是会执行父类构造函数的特点,使得子类不需要显式保存 traceID

当我们实例化 TraceableRunnableTraceableCallable 时,是在旧的线程里执行的,因此它的 traceID 会保存在 TraceableTaskcontext 属性里。

当另一个线程执行这些任务实例时,首先执行的是 runcall 方法,TraceableTask 会把保存的 context 设置到当前线程的 ThreadLocal 里,这就完成了 traceID 的传递。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据