分布式系统间请求跟踪
一、请求跟踪基本原理
现在的很多应用都是由很多系统系统在支持,这些系统还会部署很多个实例,用户的一个请求可能在多个系统的部署实例之间流转。为了跟踪一个请求的完整处理过程,我们可以给请求分配一个唯一的 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
。
当我们实例化 TraceableRunnable
或 TraceableCallable
时,是在旧的线程里执行的,因此它的 traceID
会保存在 TraceableTask
的 context
属性里。
当另一个线程执行这些任务实例时,首先执行的是 run
或 call
方法, TraceableTask
会把保存的 context
设置到当前线程的 ThreadLocal
里,这就完成了 traceID
的传递。