ThreadLocal變量的說法來自於Java,這是在多線程模子下湧現併發題目的一種處置懲罰計劃。
ThreadLocal變量作為線程內的局部變量,在多線程下能夠堅持自力,它存在於
線程的生命周期內,能夠在線程運轉階段多個模塊間同享數據。那末,ThreadLocal變量
又怎樣與node.js扯上關聯呢?
node模子
node的運轉模子無需再贅言: “事宜輪迴 + 異步實行”,但是node開闢工程師比較感興緻的點
大多集合在 “編碼形式”上,即異步代碼同步編寫,由此提出了多種處置懲罰回調地獄的處置懲罰計劃:
- yield
- thunk
- promise
- await
但是假如從代碼實行流程的微觀視角中跳出來,宏觀上對待node效勞器處置懲罰每一個HTTP要求,就會
發明這實際上是多線程web效勞器的另一種表現,雖然設想上並不像多線程模子那末直觀。在單核cpu中
每一時候node效勞器只能處置懲罰一個要求,但是node在當前要求中實行異步挪用時,就會“中綴”進入下一個
事宜輪迴處置懲罰另一個要求,直到上一個要求的異步使命事宜觸發實行對應回調,繼續實行該要求的後續邏輯。
這在某種程度上相似於CPU的時候片搶佔機制,微觀上的遞次實行,宏觀上倒是同步實行。
node在單歷程單線程(js實行線程)中“模仿”了罕見的多線程處置懲罰邏輯,雖然在單個node歷程中沒法
充分應用CPU的多核及超線程特徵,但是卻避免了多線程模子下的臨界資本同步和線程上下文
切換的題目,同時內存資本開支相對較小,因而在I/O密集型的營業下運用node開闢web效勞
每每有着意想不到的優點。
但是在node開闢中須要追蹤每一個要求的挪用鏈路,經由過程獵取要求頭的traceId字段在每一級
的挪用鏈路中通報該字段,包括“http要求、dubbo挪用、dao操縱、redis和日記辦理”等操縱。
如許經由過程追蹤traceId,就能夠剖析要求所經由的一切中間鏈路,評價每一個環節的時延與瓶頸,
更輕易舉行機能優化和毛病排查。
那末,怎樣在營業代碼中無侵入性的獵取到相干的traceId呢?這就引出了本文的ThreadLocal變量。
傳統的日記追蹤形式
需手動通報traceId給日記中間件:
var koa = require('koa');
var app = new koa();
var Logger = {
info(msg,traceId){
console.log(msg,traceId);
}
};
let business = async function(ctx){
let v = await new Promise((res)=>{
setTimeout(()=>{
Logger.info('service實行完畢',ctx.request.headers['traceId'])
res(123);
},1000);
});
ctx.body = 'hello world';
Logger.info('要求返回',ctx.request.headers['traceId'])
};
app.use(async(ctx,next)=>{
ctx.request.headers['traceId'] = Date.now() + Math.random();
await next();
});
app.use(async(ctx,next)=>{
await business(ctx);
});
app.listen(8080);
在business營業處置懲罰函數中,在service實行完畢和body返回后都舉行日記辦理,同時手動
通報要求頭traceId給日記模塊,輕易相干聯統追蹤鏈路。
現在如許編碼沒法規範化日記接口,同時也對開闢人員造成了很大的攪擾。關於營業開闢人員他們
理應不關心怎樣舉行鏈路追蹤,而現在的編碼則直接侵入了營業代碼中,這塊功用應該由日記模塊
Logger來完成,但是在與要求上下文沒有任何聯絡的Logger模塊怎樣獵取每一個要求的traceId呢?
這就須要依託node.js中的ThreadLocal變量。文章開首提到,多線程下ThreadLocal變量是與
每一個線程的生命周期對應的,那末假如在node.js的“單線程+異步挪用+事宜輪迴”的特徵下完成
相似的ThreadLocal變量,不就能夠在每一個要求的異步回調實行時獵取到對應的ThreadLocal變量,
拿到相干的上下文信息嗎?
ThreadLocal的node完成
純真完成web效勞器的中間鏈路要求追蹤實在並不龐雜,運用全局變量Map並經由過程每一個要求的唯一標識
存儲上下文信息,當實行到該要求的下一個異步挪用時便經由過程在全局Map中獵取到與該要求綁定的ThreadLocal
變量,不過這是在應用層面的一種投契行動,是與要求緊耦合的淺易完成。
最完全的計劃則是在node應用層完成一種棧幀,在該棧幀內重寫一切的異步函數,並增加各個
hook在異步函數的各個生命周期實行,完成異步函數實行上下文與棧幀的映照,這便是最為
完全的ThreadLocal完成,而不是僅僅停留在與HTTP要求的映照過程當中。
現在已經有zone.js庫完成了node應用層棧幀的可控編碼,同時能夠在該棧幀存活階段綁定
相干數據,我們便能夠應用這類特徵完成相似多線程下的ThreadLocal變量。
我們的目的是完成無侵入的編寫包括鏈路追蹤的營業代碼,以下所示:
app.use(async(ctx,next)=>{
let v = await new Promise((res)=>{
setTimeout(()=>{
Logger.info('service實行完畢')
res(123);
},1000);
});
ctx.body = 'hello world';
Logger.info('要求返回')
});
相比較,Logger.info中不須要手動通報traceId變量,由日記模塊經由過程接見ThreadLocal變量獵取。
經由過程zone.js供應的建立Zone(對應於棧幀)功用,我們不僅能夠獵取當前要求(相似於多線程下的單個線程)的
ThreadLocal變量,還能夠獵取上一個要求的相干信息。
require('zone.js');
var koa = require('koa');
var app = new koa();
var Logger = {
info(msg){
console.log(msg,Zone.current.get('traceId'));
}
};
var koaZoneProperties = {
requestContext: null
};
var koaZone = Zone.current.fork({
name: 'koa',
properties: koaZoneProperties
});
let business = async function(ctx){
let v = await new Promise((res)=>{
setTimeout(()=>{
Logger.info('service實行完畢')
res(123);
},1000);
});
ctx.body = 'hello world';
Logger.info('要求返回')
};
koaZone.run(()=>{
app.use(async(ctx,next)=>{
console.log(koaZone.get('requestContext'))
ctx.request.headers['traceId'] = Date.now();
await next();
});
app.use(async(ctx,next)=>{
await new Promise((resolve)=>{
let koaMidZone = koaZone.fork({
name: 'koaMidware',
properties: {
traceId: ctx.request.headers['traceId']
}
}).run(async()=>{
// 保留要求上下文至parent zone
koaZoneProperties.requestContext = ctx;
await business(ctx);
resolve();
});
});
});
app.listen(8080);
});
建立了兩個有繼續關聯的zone(棧幀),koaZone的requestContext屬性存儲上一個要求的上下文信息;
koaMidZone的traceId屬性存儲traceId變量,這是一個ThreadLocal變量。
Logger.info中經由過程Zone.current.get(‘traceId’) 獵取當前“線程”的
ThreadLocal變量,無需開闢人員手動通報traceId變量。
關於zone.js的其他用法,讀者有興緻能夠自行研討。本文重要應用zone.js保留一個實行棧幀
內的多個異步函數的實行上下文與特定數據(即ThreadLocal變量)的映照。
申明
現在,這套模子已在線上營業中用來追蹤各級鏈路,各級中間件包括dubbo client、dubbo provider、
設置中間等都依靠ThreadLocal變量完成數據透傳和挪用通報,因而能夠放心運用。