业务链路监控(Google Dapper)和ThreadLocal

引言

一个复杂的分布式web系统,前端的一次用户操作,对应的是后端几十甚至上百个应用和服务的调用,这些调用有串行的、并行的,那么如何确定前端的一次操作背后调用了哪些应用、服务、接口,这些调用的先后顺序又是怎样,业务链路监控系统就是用来解决这个痛点的。

实现原理

从流量入口(通常是前端的一次Http调用)开始,传递Trace(TraceId,RpcId,UserData),在整个业务链路上传递Trace信息,从前端、服务层到数据层一层一层传递下去,这样根据TraceId就可以识别具体调用属于哪条链路

Trace信息如何在链路内透传

Trace信息相当于在业务链路中的埋点信息

如下图:链路的调用分2种,系统内部的调用通常是线程内的调用,而经过RPC、HTTP、异步消息调用都是不同系统(不同线程间)的调用

2种场景的Trace信息透传:

线程/进程间传递使用参数传递:客户端调用服务端、异步消息调用属于信息从一个应用的线程转移到另外一个应用的线程,在2个线程之间传递Trace信息使用参数传递
线程内传递使用ThreadLocal:线程内部的方法之间调用,无论调用了多少个方法,都是一个线程内部的调用,这些方法间传递Trace信息使用ThreadLocal线程间透传HTTP:通过Http head或者body传递Trace信息。RPC:通过自定义的rpc协议(根据rpc框架实现的不同,各个公司有不同的rpc协议实现)传递Trace信息。MQ:通过消息头或者消息体携带Trace信息实现Trace信息从消息的生产者向消费者传递。线程内透传:ThreadLocal:进入线程时,将Trace信息存储在ThreadLocal变量中,出线程时,从ThreadLocal变量中取出Trace信息,作为参数传递到下一个线程(应用系统)。

ThreadLocal 是什么?

上面讲了业务链路监控系统是如何实现无侵入式的Trace信息透传,那么ThreadLocal是什么,为什么可以实现线程内的数据传递。

首先,ThreadLocal是一个老家伙,它在jdk1.2的时候就已经存在了,首先看下ThreadLocal的注释:

ThreadLocal变量特殊的地方在于:对变量值的任何操作实际都是对这个变量在线程中的一份copy进行操作,不会影响另外一个线程中同一个ThreadLocal变量的值。

例如定义一个ThreadLocal变量,值类型为Integer:

ThreadLocal提供的几个主要接口:

范例代码:

执行结果:

结果分析:

thread0和thread1中对ThreadLocal变量seq的操作并没有相互影响。主线程在thread1启动前修改seq值对thread1无影响,thread1中seq初始值仍然是0。三个线程中调用get方法获取到的是不同的TestBean对象

ThreadLocal变量线程独立的原理:

直接看ThreadLocal变量的赋值:

getMap的作用是返回线程对象t的threadLocals属性的值

线程对象的threadLocals属性定义如下:

getMap返回的是线程对象t的threadLocals属性,一个ThreadLocalMap对象

createMap(t,value)的作用是初始化线程对象t的属性threadLocals的值:

综上看,ThreadLocal.set(T value)的逻辑是:首先获取当前线程对象t,然后调用getMap(t)获取t.threadLocals,如果获取到的t.threadLocals为空,就调用createMap(t,value)对t.threadLocals进行初始化赋值,否则调用map.set(this,value)覆盖t.threadLocals的值。

一个线程中调用ThreadLocal变量的get/set方法获取和修改的是当前线程中存储的value,当前线程无法修改另外一个线程的存储的value,这就是ThreadLocal变量线程独立的原因。

但是如果不同线程的value通过调用set方法指向同一个对象,ThreadLocal就丧失了线程独立性,范例代码:

和前面代码的区别在于,线程运行前,调用set方法将value置为外部的testBean变量,看运行结果:

所以ThreadLocal线程独立的前提是:不要使用set方法设置value为同一个对象,ThreadLocal对象会自动在线程第一次调用get方法中调用initialValue()方法生成一个类型的实例作为value。

ThreadLocal变量的特点是:线程独立,生命周期和线程的生命周期一致。正是这2个特点,决定了它可以在分布式的业务链路监控系统中用于Trace信息的传输。