監(jiān)控之前只總結(jié)了一篇《微服務(wù)-監(jiān)控》,比較宏觀。其中很多細(xì)節(jié)沒(méi)有過(guò)深關(guān)注到,主要還是沒(méi)有實(shí)踐過(guò),更沒(méi)有去深度思考,所以很多有意思的技術(shù)點(diǎn)都錯(cuò)過(guò)了,比如traceid的生成,傳遞
大牛圈總的大作《微服務(wù)系統(tǒng)架構(gòu)之分布式traceId追蹤參考實(shí)現(xiàn)》已經(jīng)給出解決方案,但還是再主動(dòng)總結(jié)一下
意義
為什么需要traceid,為了查看完整的調(diào)用鏈,一旦調(diào)用過(guò)程中出現(xiàn)問(wèn)題,可以第一時(shí)間定位到問(wèn)題現(xiàn)場(chǎng)
整個(gè)調(diào)用鏈?zhǔn)且豢脴?shù)形結(jié)構(gòu),traceid的傳遞涉及到主干與支干,進(jìn)程內(nèi)與進(jìn)程外
生成
原則是唯一不重復(fù),比如現(xiàn)成的UUID
但UUID一是丑、無(wú)意義,二是string;
從字面意義以及未來(lái)落盤(pán)都不能說(shuō)是最佳方案,比如想讓traceid包含信息更豐富一些,能一眼看出此traceid是主干還是分支
此traceid有沒(méi)有最終落盤(pán)(這兒涉及到落盤(pán)抽樣率,每天服務(wù)處理海量請(qǐng)求,總不能每個(gè)traceid都落盤(pán))
Random
這兒引申到如何更好地獲取一個(gè)隨機(jī)數(shù)又是一個(gè)課題,另開(kāi)篇吧
傳遞
在《熔斷機(jī)制》中提過(guò),服務(wù)調(diào)用是一個(gè)1->N扇出,調(diào)用鏈展現(xiàn)出對(duì)應(yīng)的樹(shù)形結(jié)構(gòu),但調(diào)用嵌套都不會(huì)深,一般兩層就差不多了
- traceId1
- traceId1.1
- traceId1.1.1
- traceId2.1
- traceId3.1
- traceId1.1
進(jìn)程外
服務(wù)之間的傳遞
serverA --> serverB -- serverC
這兒在設(shè)計(jì)傳輸協(xié)議時(shí),在協(xié)議頭里面帶上traceid
進(jìn)程內(nèi)
主干
這種場(chǎng)景ThreadLocal是最佳手法
支干
比如serviceA -- > remote.serviceB
trace是個(gè)樹(shù)形結(jié)構(gòu),可以將remote.serviceB的traceId.parentId = serviceA.traceId
異步子任務(wù)
子線程可以通過(guò)InheritableThreadLocal傳遞traceid
順帶一下,InheritableThreadLocal的詳細(xì)實(shí)現(xiàn),先可補(bǔ)習(xí)一下ThreadLocal《解析ThreadLocal》
在創(chuàng)建Thread時(shí),會(huì)從父線程的inheritableThreadLocals復(fù)制到子線程中去,這樣在子線程中就能拿到在父線程中的賦值
/* 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);
線程池
如果沒(méi)有線程池,以上就算是解決所有問(wèn)題了,可實(shí)現(xiàn)畢竟是實(shí)現(xiàn)
/**
* 子線程從父線程中取值
* @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();
}
為了好重現(xiàn)問(wèn)題,線程池大小為1,但會(huì)連續(xù)跑兩次任務(wù)
pool-1-thread-1 get parent value:parent
11==sun
pool-1-thread-1 get parent value:sun
11==sun
main:parent
在第二次取父線程值時(shí),卻是第一次任務(wù)線程中的賦值,在線程池中子線程不能正常獲取父線程值
線程池中,線程會(huì)復(fù)用,線程中的inheritableThreadLocals沒(méi)有被清空
解決方法一是:池中線程數(shù)大于任務(wù)線程,讓線程沒(méi)有重用機(jī)會(huì)
ExecutorService executor = Executors.newFixedThreadPool(>=[任務(wù)線程數(shù)])
但在多線程應(yīng)用中,明顯不能解決問(wèn)題,任務(wù)數(shù)肯定遠(yuǎn)遠(yuǎn)超過(guò)線程數(shù)
解決方法二是:自定義實(shí)現(xiàn)在使用完線程主動(dòng)清空inheritableThreadLocals
阿里開(kāi)源transmittable-thread-local就實(shí)現(xiàn)這樣的功能
整體思路也是從主線程復(fù)制,使用,再清理
TtlRunnable 構(gòu)造方法中,調(diào)用了 TransmittableThreadLocal.Transmitter.capture() 獲取當(dāng)前線程中所有的上下文,并儲(chǔ)存在 AtomicReference 中
當(dāng)線程執(zhí)行時(shí),調(diào)用 TtlRunnable run 方法,TtlRunnable 會(huì)從 AtomicReference 中獲取出調(diào)用線程中所有的上下文,并把上下文給 TransmittableThreadLocal.Transmitter.replay 方法把上下文復(fù)制到當(dāng)前線程。并把上下文備份
當(dāng)線程執(zhí)行完,調(diào)用 TransmittableThreadLocal.Transmitter.restore 并把備份的上下文傳入,恢復(fù)備份的上下文,把后面新增的上下文刪除,并重新把上下文復(fù)制到當(dāng)前線程
transmittable-thread-local代碼不多,但有很多亮點(diǎn),可以自行膜拜
在此場(chǎng)景,transmittable-thread-local還是太重了,其實(shí)可以簡(jiǎn)單借鑒一下transmittable-thread-local的思路,自定義Runnable
public TransRunnable(Runnable runnable){
this.runnable = runnable;
//在創(chuàng)建時(shí),獲取父traceId
this.parentId = TranceContext.getParentTrace();
}
@Override
public void run() {
//
String old = TranceContext.getParentTrace();
//設(shè)置父traceid
TranceContext.setParentTrace(parentId);
runnable.run();
//還原
TranceContext.setParentTrace(old);
}
在創(chuàng)建子線程時(shí),把父traceId帶進(jìn)去,就能在子線程業(yè)務(wù)方法中拿到父traceId,這樣整個(gè)調(diào)用鏈也不會(huì)斷
schedule
traceid生成,有主動(dòng)請(qǐng)求時(shí),會(huì)生成,但如果是個(gè)系統(tǒng)的定時(shí)任務(wù)呢?
- 讓taskService調(diào)用一下入口,類(lèi)似模擬用戶(hù)行為
- 主動(dòng)生成一個(gè)parent traceId
總結(jié)
到此,對(duì)于traceid的知識(shí)結(jié)構(gòu)豐滿(mǎn)了很多