监控之前只总结了一篇《微服务-监控》,比较宏观。其中很多细节没有过深关注到,主要还是没有实践过,更没有去深度思考,所以很多有意思的技术点都错过了,比如traceid的生成,传递
大牛圈总的大作《微服务系统架构之分布式traceId追踪参考实现》已经给出解决方案,但还是再主动总结一下
意义
为什么需要traceid,为了查看完整的调用链,一旦调用过程中出现问题,可以第一时间定位到问题现场
整个调用链是一棵树形结构,traceid的传递涉及到主干与支干,进程内与进程外
生成
原则是唯一不重复,比如现成的UUID
但UUID一是丑、无意义,二是string;
从字面意义以及未来落盘都不能说是最佳方案,比如想让traceid包含信息更丰富一些,能一眼看出此traceid是主干还是分支
此traceid有没有最终落盘(这儿涉及到落盘抽样率,每天服务处理海量请求,总不能每个traceid都落盘)
Random
这儿引申到如何更好地获取一个随机数又是一个课题,另开篇吧
传递
在《熔断机制》中提过,服务调用是一个1->N扇出,调用链展现出对应的树形结构,但调用嵌套都不会深,一般两层就差不多了
- traceId1
- traceId1.1
- traceId1.1.1
- traceId2.1
- traceId3.1
- traceId1.1
进程外
服务之间的传递
serverA --> serverB -- serverC
这儿在设计传输协议时,在协议头里面带上traceid
进程内
主干
这种场景ThreadLocal是最佳手法
支干
比如serviceA -- > remote.serviceB
trace是个树形结构,可以将remote.serviceB的traceId.parentId = serviceA.traceId
异步子任务
子线程可以通过InheritableThreadLocal传递traceid
顺带一下,InheritableThreadLocal的详细实现,先可补习一下ThreadLocal《解析ThreadLocal》
在创建Thread时,会从父线程的inheritableThreadLocals复制到子线程中去,这样在子线程中就能拿到在父线程中的赋值
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
线程池
如果没有线程池,以上就算是解决所有问题了,可实现毕竟是实现
/**
* 子线程从父线程中取值
* @throws InterruptedException
*/
private static void testThreadpool() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
final ThreadLocal<String> threadLocal = threadLocal();//new InheritableThreadLocal<>()
threadLocal.set("parent");
for(int i=0;i<1;i++) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +" get parent value:" + threadLocal.get());
threadLocal.set("sun");
System.out.println(Thread.currentThread().getId() + "==" + threadLocal.get());
}
};
executorService.execute(runnable);
Thread.sleep(100);
executorService.execute(runnable);
Thread.sleep(100);
System.out.println("main:" + threadLocal.get());
}
executorService.shutdown();
}
为了好重现问题,线程池大小为1,但会连续跑两次任务
pool-1-thread-1 get parent value:parent
11==sun
pool-1-thread-1 get parent value:sun
11==sun
main:parent
在第二次取父线程值时,却是第一次任务线程中的赋值,在线程池中子线程不能正常获取父线程值
线程池中,线程会复用,线程中的inheritableThreadLocals没有被清空
解决方法一是:池中线程数大于任务线程,让线程没有重用机会
ExecutorService executor = Executors.newFixedThreadPool(>=[任务线程数])
但在多线程应用中,明显不能解决问题,任务数肯定远远超过线程数
解决方法二是:自定义实现在使用完线程主动清空inheritableThreadLocals
阿里开源transmittable-thread-local就实现这样的功能
整体思路也是从主线程复制,使用,再清理
TtlRunnable 构造方法中,调用了 TransmittableThreadLocal.Transmitter.capture() 获取当前线程中所有的上下文,并储存在 AtomicReference 中
当线程执行时,调用 TtlRunnable run 方法,TtlRunnable 会从 AtomicReference 中获取出调用线程中所有的上下文,并把上下文给 TransmittableThreadLocal.Transmitter.replay 方法把上下文复制到当前线程。并把上下文备份
当线程执行完,调用 TransmittableThreadLocal.Transmitter.restore 并把备份的上下文传入,恢复备份的上下文,把后面新增的上下文删除,并重新把上下文复制到当前线程
transmittable-thread-local代码不多,但有很多亮点,可以自行膜拜
在此场景,transmittable-thread-local还是太重了,其实可以简单借鉴一下transmittable-thread-local的思路,自定义Runnable
public TransRunnable(Runnable runnable){
this.runnable = runnable;
//在创建时,获取父traceId
this.parentId = TranceContext.getParentTrace();
}
@Override
public void run() {
//
String old = TranceContext.getParentTrace();
//设置父traceid
TranceContext.setParentTrace(parentId);
runnable.run();
//还原
TranceContext.setParentTrace(old);
}
在创建子线程时,把父traceId带进去,就能在子线程业务方法中拿到父traceId,这样整个调用链也不会断
schedule
traceid生成,有主动请求时,会生成,但如果是个系统的定时任务呢?
- 让taskService调用一下入口,类似模拟用户行为
- 主动生成一个parent traceId
总结
到此,对于traceid的知识结构丰满了很多