因爲Node.js的單線程模型的限制,咱們沒法設置全局 traceid 來聚合請求,即實現輸出日誌與請求的綁定。若是不實現日誌和請求的綁定,咱們難以判斷日誌輸出與對應用戶請求的對應關係,這對線上問題排查帶來了困難。javascript
例如,在用戶訪問 retrieveOne
API 時,其會調用 retrieveOneSub
函數,若是咱們想在 retrieveOneSub
函數中輸出當前請求對應的學生信息,是繁瑣的。在 course-se 現有實現下,咱們針對此問題的解決方法是:java
retrieveOneSub
函數的父函數,即 retrieveOne
內,對 paramData
進行解構,輸出學生相關信息,但該方案沒法細化日誌輸出粒度。retrieveOneSub
函數簽名,接收 paramData
爲其參數,該方案能確保日誌輸出粒度,但在調用鏈很深的狀況下,須要給各函數修改函數簽名,使其接收 paramData
,頗具工做量,並不太可行。/** * 返回獲取一份提交的函數 * @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);
// 經過 paramData.user 獲取 user 相關信息,如 user_id ,
// 但沒法細化日誌輸出粒度,除非修改 retrieveOneSub 的簽名,
// 添加 paramData 爲其參數。
const { user_id } = paramData.user;
console.log(`${user_id} is trying to retreive one submission.`);
// 調用了 retrieveOneSub 函數。
const sub = await retrieveOneSub(sub_asgn_id, subModel);
const submission = sub;
assign(sub, { sub_asgn_id });
assign(paramData, { submission, sub });
return sub;
}
/** * 從數據庫獲取一份提交 * @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;
}
複製代碼
其實,針對以上的問題,咱們還能夠從 Node 的 Async Hooks 實驗性 API 方面入手。在 Node.js v8.x 後,官方提供了可用於監聽異步行爲的 Async Hooks(異步鉤子)API 的支持。數據庫
Async Hooks 對每個(同步或異步)函數提供了一個 Async Scope ,咱們可調用 executionAsyncId
方法獲取當前函數的 Async ID ,調用 triggerAsyncId
獲取當前函數調用者的 Async ID。bash
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
模擬一個異步調用過程,且在該異步過程當中咱們調用了 handler
同步函數,咱們在每一個函數內都輸出其對應的 Async ID 和 Trigger Async ID 。執行上述代碼後,其運行結果以下。異步
top level: 1 0
f: 1 0
setTimeout: 7 1
setTimeout: 9 1
inner setTimeout: 11 7
inner setTimeout: 13 9
複製代碼
經過上述日誌輸出,咱們得出如下信息:async
f
內的 Async ID 和其調用者的 Async ID 相同。g
函數。正如咱們前面所說的,Async Hooks 可用於追蹤異步資源。爲了實現此目的,咱們須要瞭解 Async Hooks 的相關 API ,具體說明參照如下代碼中的註釋。函數
const asyncHooks = require("async_hooks");
// 建立一個 AsyncHooks 實例。
const hooks = asyncHooks.createHook({
// 對象構造時會觸發 init 事件。
init: function(asyncId, type, triggerId, resource) {},
// 在執行回調前會觸發 before 事件。
before: function(asyncId) {},
// 在執行回調後會觸發 after 事件。
after: function(asyncId) {},
// 在銷燬對象後會觸發 destroy 事件。
destroy: function(asyncId) {}
});
// 容許該實例中對異步函數啓用 hooks 。
hooks.enable();
// 關閉對異步資源的追蹤。
hooks.disable();
複製代碼
咱們在調用 createHook
時,可注入 init
、before
、after
和 destroy
函數,用於追蹤異步資源的不一樣生命週期。性能
基於 Async Hooks API ,咱們便可設計如下解決方案,實現日誌與請求記錄的綁定,即 Trace ID 的全局綁定。ui
const asyncHooks = require("async_hooks");
const { executionAsyncId } = asyncHooks;
// 保存異步調用的上下文。
const contexts = {};
const hooks = asyncHooks.createHook({
// 對象構造時會觸發 init 事件。
init: function(asyncId, type, triggerId, resource) {
// triggerId 即爲當前函數的調用者的 asyncId 。
if (contexts[triggerId]) {
// 設置當前函數的異步上下文與調用者的異步上下文一致。
contexts[asyncId] = contexts[triggerId];
}
},
// 在銷燬對象後會觸發 destroy 事件。
destroy: function(asyncId) {
if (!contexts[asyncId]) return;
// 銷燬當前異步上下文。
delete contexts[asyncId];
}
});
// 關鍵!容許該實例中對異步函數啓用 hooks 。
hooks.enable();
// 模擬業務處理函數。
function handler(params) {
// 設置 context ,可在中間件中完成此操做(如 Logger Middleware)。
contexts[executionAsyncId()] = params;
// 如下是業務邏輯。
console.log(`handler ${JSON.stringify(params)}`);
f();
}
function f() {
setTimeout(() => {
// 輸出所屬異步過程的 params 。
console.log(`setTimeout ${JSON.stringify(contexts[executionAsyncId()])}`);
});
}
// 模擬兩個異步過程(兩個請求)。
setTimeout(handler, 0, { id: 0 });
setTimeout(handler, 0, { id: 1 });
複製代碼
在上述代碼中,咱們先聲明瞭 contexts
用於存儲每一個異步過程當中的上下文數據(如 Trace ID),隨後咱們建立了一個 Async Hooks 實例。咱們在異步資源初始化時,設置當前 Async ID 對應的上下文數據,使得其數據爲調用者的上下文數據;咱們在異步資源被銷燬時,刪除其對應的上下文數據。spa
經過這種方式,咱們只需在一開始設置上下文數據,便可在其引起的各個過程(同步和異步過程)中,得到上下文數據,從而解決了問題。
執行上述代碼,其運行結果以下。根據輸出日誌可知,咱們的解決方案是可行的。
handler {"id":0}
handler {"id":1}
setTimeout {"id":0}
setTimeout {"id":1}
複製代碼
不過須要注意的是,Async Hooks 是實驗性 API ,存在必定的性能損耗,但 Node 官方正努力將其變得生產可用。所以,在機器資源足夠的狀況下,使用本解決方案,犧牲部分性能,換取開發體驗。