Node綁定全局TraceID的實(shí)現(xiàn)方法
問(wèn)題描述
由于Node.js的 單線程模型 的限制,我們無(wú)法設(shè)置全局 traceid 來(lái)聚合請(qǐng)求,即 實(shí)現(xiàn)輸出日志與請(qǐng)求的綁定 。如果不實(shí)現(xiàn)日志和請(qǐng)求的綁定,我們難以判斷日志輸出與對(duì)應(yīng)用戶(hù)請(qǐng)求的對(duì)應(yīng)關(guān)系,這對(duì) 線上問(wèn)題排查 帶來(lái)了困難。
例如,在用戶(hù)訪問(wèn) retrieveOne API 時(shí),其會(huì)調(diào)用 retrieveOneSub 函數(shù),如果我們想在 retrieveOneSub 函數(shù)中輸出當(dāng)前請(qǐng)求對(duì)應(yīng)的學(xué)生信息,是繁瑣的。在 course-se 現(xiàn)有實(shí)現(xiàn)下,我們針對(duì)此問(wèn)題的解決方法是:
- 方案1:在調(diào)用 retrieveOneSub 函數(shù)的父函數(shù),即 retrieveOne 內(nèi),對(duì) paramData 進(jìn)行 解構(gòu) ,輸出學(xué)生相關(guān)信息,但該方案 無(wú)法細(xì)化日志輸出粒度 。
- 方案2:修改 retrieveOneSub 函數(shù)簽名,接收 paramData 為其參數(shù),該方案 能確保日志輸出粒度 ,但 在調(diào)用鏈很深的情況下,需要給各函數(shù)修改函數(shù)簽名 ,使其接收 paramData ,頗具工作量,并不太可行。
/**
* 返回獲取一份提交的函數(shù)
* @param {ParamData} paramData
* @param {Context} ctx
* @param {string} id
*/
export async function retrieveOne(paramData, ctx, id) {
const { subModel } = paramData.ce;
const sub_asgn_id = Number(id);
// 通過(guò) paramData.user 獲取 user 相關(guān)信息,如 user_id ,
// 但無(wú)法細(xì)化日志輸出粒度,除非修改 retrieveOneSub 的簽名,
// 添加 paramData 為其參數(shù)。
const { user_id } = paramData.user;
console.log(`${user_id} is trying to retreive one submission.`);
// 調(diào)用了 retrieveOneSub 函數(shù)。
const sub = await retrieveOneSub(sub_asgn_id, subModel);
const submission = sub;
assign(sub, { sub_asgn_id });
assign(paramData, { submission, sub });
return sub;
}
/**
* 從數(shù)據(jù)庫(kù)獲取一份提交
* @param {number} sub_asgn_id
* @param {SubModel} model
*/
async function retrieveOneSub(sub_asgn_id, model) {
const [sub] = await model.findById(sub_asgn_id);
if (!sub) {
throw new ME.SoftError(ME.NOT_FOUND, '找不到該提交');
}
return sub;
}
Async Hooks
其實(shí),針對(duì)以上的問(wèn)題,我們還可以從 Node 的 Async Hooks 實(shí)驗(yàn)性 API 方面入手。在 Node.js v8.x 后,官方提供了可用于 監(jiān)聽(tīng)異步行為 的 Async Hooks(異步鉤子)API 的支持。
Async Scope
Async Hooks 對(duì)每一個(gè)(同步或異步)函數(shù)提供了一個(gè) Async Scope ,我們可調(diào)用 executionAsyncId 方法獲取當(dāng)前函數(shù)的 Async ID ,調(diào)用 triggerAsyncId 獲取當(dāng)前函數(shù)調(diào)用者的 Async ID。
const asyncHooks = require("async_hooks");
const { executionAsyncId, triggerAsyncId } = asyncHooks;
console.log(`top level: ${executionAsyncId()} ${triggerAsyncId()}`);
const f = () => {
console.log(`f: ${executionAsyncId()} ${triggerAsyncId()}`);
};
f();
const g = () => {
console.log(`setTimeout: ${executionAsyncId()} ${triggerAsyncId()}`);
setTimeout(() => {
console.log(`inner setTimeout: ${executionAsyncId()} ${triggerAsyncId()}`);
}, 0);
};
setTimeout(g, 0);
setTimeout(g, 0);
在上述代碼中,我們使用 setTimeout 模擬一個(gè)異步調(diào)用過(guò)程,且在該異步過(guò)程中我們調(diào)用了 handler 同步函數(shù),我們?cè)诿總€(gè)函數(shù)內(nèi)都輸出其對(duì)應(yīng)的 Async ID 和 Trigger Async ID 。執(zhí)行上述代碼后,其運(yùn)行結(jié)果如下。
top level: 1 0 f: 1 0 setTimeout: 7 1 setTimeout: 9 1 inner setTimeout: 11 7 inner setTimeout: 13 9
通過(guò)上述日志輸出,我們得出以下信息:
- 調(diào)用同步函數(shù),不會(huì)改變其 Async ID ,如函數(shù) f 內(nèi)的 Async ID 和其調(diào)用者的 Async ID 相同。
- 同一個(gè)函數(shù),被不同時(shí)刻進(jìn)行異步調(diào)用,會(huì)分配至不同的 Async ID ,如上述代碼中的 g 函數(shù)。
追蹤異步資源
正如我們前面所說(shuō)的,Async Hooks 可用于追蹤異步資源。為了實(shí)現(xiàn)此目的,我們需要了解 Async Hooks 的相關(guān) API ,具體說(shuō)明參照以下代碼中的注釋。
const asyncHooks = require("async_hooks");
// 創(chuàng)建一個(gè) AsyncHooks 實(shí)例。
const hooks = asyncHooks.createHook({
// 對(duì)象構(gòu)造時(shí)會(huì)觸發(fā) init 事件。
init: function(asyncId, type, triggerId, resource) {},
// 在執(zhí)行回調(diào)前會(huì)觸發(fā) before 事件。
before: function(asyncId) {},
// 在執(zhí)行回調(diào)后會(huì)觸發(fā) after 事件。
after: function(asyncId) {},
// 在銷(xiāo)毀對(duì)象后會(huì)觸發(fā) destroy 事件。
destroy: function(asyncId) {}
});
// 允許該實(shí)例中對(duì)異步函數(shù)啟用 hooks 。
hooks.enable();
// 關(guān)閉對(duì)異步資源的追蹤。
hooks.disable();
我們?cè)谡{(diào)用 createHook 時(shí),可注入 init 、 before 、 after 和 destroy 函數(shù),用于 追蹤異步資源的不同生命周期 。
全新解決方案
基于 Async Hooks API ,我們即可設(shè)計(jì)以下解決方案,實(shí)現(xiàn)日志與請(qǐng)求記錄的綁定,即 Trace ID 的全局綁定。
const asyncHooks = require("async_hooks");
const { executionAsyncId } = asyncHooks;
// 保存異步調(diào)用的上下文。
const contexts = {};
const hooks = asyncHooks.createHook({
// 對(duì)象構(gòu)造時(shí)會(huì)觸發(fā) init 事件。
init: function(asyncId, type, triggerId, resource) {
// triggerId 即為當(dāng)前函數(shù)的調(diào)用者的 asyncId 。
if (contexts[triggerId]) {
// 設(shè)置當(dāng)前函數(shù)的異步上下文與調(diào)用者的異步上下文一致。
contexts[asyncId] = contexts[triggerId];
}
},
// 在銷(xiāo)毀對(duì)象后會(huì)觸發(fā) destroy 事件。
destroy: function(asyncId) {
if (!contexts[asyncId]) return;
// 銷(xiāo)毀當(dāng)前異步上下文。
delete contexts[asyncId];
}
});
// 關(guān)鍵!允許該實(shí)例中對(duì)異步函數(shù)啟用 hooks 。
hooks.enable();
// 模擬業(yè)務(wù)處理函數(shù)。
function handler(params) {
// 設(shè)置 context ,可在中間件中完成此操作(如 Logger Middleware)。
contexts[executionAsyncId()] = params;
// 以下是業(yè)務(wù)邏輯。
console.log(`handler ${JSON.stringify(params)}`);
f();
}
function f() {
setTimeout(() => {
// 輸出所屬異步過(guò)程的 params 。
console.log(`setTimeout ${JSON.stringify(contexts[executionAsyncId()])}`);
});
}
// 模擬兩個(gè)異步過(guò)程(兩個(gè)請(qǐng)求)。
setTimeout(handler, 0, { id: 0 });
setTimeout(handler, 0, { id: 1 });
在上述代碼中,我們先聲明了 contexts 用于存儲(chǔ)每個(gè)異步過(guò)程中的上下文數(shù)據(jù)(如 Trace ID),隨后我們創(chuàng)建了一個(gè) Async Hooks 實(shí)例。我們?cè)诋惒劫Y源初始化時(shí),設(shè)置當(dāng)前 Async ID 對(duì)應(yīng)的上下文數(shù)據(jù),使得其數(shù)據(jù)為調(diào)用者的上下文數(shù)據(jù);我們?cè)诋惒劫Y源被銷(xiāo)毀時(shí),刪除其對(duì)應(yīng)的上下文數(shù)據(jù)。
通過(guò)這種方式,我們只需在一開(kāi)始設(shè)置上下文數(shù)據(jù),即可在其引發(fā)的各個(gè)過(guò)程(同步和異步過(guò)程)中,獲得上下文數(shù)據(jù),從而解決了問(wèn)題。
執(zhí)行上述代碼,其運(yùn)行結(jié)果如下。根據(jù)輸出日志可知,我們的解決方案是可行的。
handler {"id":0}
handler {"id":1}
setTimeout {"id":0}
setTimeout {"id":1}
不過(guò)需要注意的是,Async Hooks 是 實(shí)驗(yàn)性 API , 存在一定的性能損耗 ,但 Node 官方正努力將其變得生產(chǎn)可用。因此, 在機(jī)器資源足夠的情況下,使用本解決方案,犧牲部分性能,換取開(kāi)發(fā)體驗(yàn)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
NodeJS 將文件夾按照存放路徑變成一個(gè)對(duì)應(yīng)的JSON的方法
這篇文章主要介紹了NodeJS 將文件夾按照存放路徑變成一個(gè)對(duì)應(yīng)的JSON的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
node.js中express中間件body-parser的介紹與用法詳解
這篇文章主要給大家介紹了關(guān)于node.js中express中間件body-parser的相關(guān)資料,文章通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05
在node環(huán)境下parse Smarty模板的使用示例代碼
這篇文章主要介紹了在node環(huán)境下parse Smarty模板的使用示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
在Docker快速部署Node.js應(yīng)用的詳細(xì)步驟
這篇文章的目標(biāo)是為了向大家展示如何在Docker的container里運(yùn)行Node.js程序,文中通過(guò)圖文與示例代碼介紹的非常詳細(xì),有需要的朋友們可以參考借鑒。2016-09-09
Nodejs學(xué)習(xí)筆記之測(cè)試驅(qū)動(dòng)
本文是本系列文章的第二篇,主要是測(cè)試針對(duì)于web后端的驅(qū)動(dòng),在開(kāi)發(fā)過(guò)程中,在開(kāi)發(fā)完成一段代碼后如果負(fù)責(zé)任而不是說(shuō)完全把問(wèn)題交給測(cè)試人員去發(fā)現(xiàn)的話,這個(gè)時(shí)候通常都會(huì)去做一些手動(dòng)的測(cè)試。2015-04-04

