溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何使用Node.js的Async Hooks模塊追蹤異步資源

發(fā)布時(shí)間:2021-09-06 14:58:44 來源:億速云 閱讀:120 作者:小新 欄目:web開發(fā)

小編給大家分享一下如何使用Node.js的Async Hooks模塊追蹤異步資源,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!

Async Hooks 功能是 Node.js v8.x 版本新增加的一個(gè)核心模塊,它提供了 API 用來追蹤 Node.js  程序中異步資源的聲明周期,可在多個(gè)異步調(diào)用之間共享數(shù)據(jù)

executionAsyncId 和 triggerAsyncId

async hooks 模塊提供了 executionAsyncId() 函數(shù)標(biāo)志當(dāng)前執(zhí)行上下文的異步資源 Id,下文使用 asyncId 表示。還有一個(gè)  triggerAsyncId() 函數(shù)來標(biāo)志當(dāng)前執(zhí)行上下文被觸發(fā)的異步資源 Id,也就是當(dāng)前異步資源是由哪個(gè)異步資源創(chuàng)建的。每個(gè)異步資源都會(huì)生成  asyncId,該 id 會(huì)呈遞增的方式生成,且在 Node.js 當(dāng)前實(shí)例里全局唯一。

const asyncHooks = require('async_hooks'); const fs = require('fs'); const asyncId = () => asyncHooks.executionAsyncId(); const triggerAsyncId = () => asyncHooks.triggerAsyncId();  console.log(`Global asyncId: ${asyncHooks.executionAsyncId()}, Global triggerAsyncId: ${triggerAsyncId()}`);  fs.open('hello.txt', (err, res) => {   console.log(`fs.open asyncId: ${asyncId()}, fs.open triggerAsyncId: ${triggerAsyncId()}`); });

下面是我們運(yùn)行的結(jié)果,全局的 asyncId 為 1,fs.open 回調(diào)里打印的 triggerAsyncId 為 1 由全局觸發(fā)。

Global asyncId: 1, Global triggerAsyncId: 0 fs.open asyncId: 5, fs.open triggerAsyncId: 1

默認(rèn)未開啟的 Promise 執(zhí)行跟蹤

默認(rèn)情況下,由于 V8 提供的 promise introspection API 相對(duì)消耗性能,Promise 的執(zhí)行沒有分配  asyncId。這意味著默認(rèn)情況下,使用了 Promise 或 Async/Await 的程序?qū)⒉荒苷_的執(zhí)行和觸發(fā) Promise 回調(diào)上下文的  ID。即得不到當(dāng)前異步資源 asyncId 也得不到當(dāng)前異步資源是由哪個(gè)異步資源創(chuàng)建的 triggerAsyncId,如下所示:

Promise.resolve().then(() => {   // Promise asyncId: 0. Promise triggerAsyncId: 0   console.log(`Promise asyncId: ${asyncId()}. Promise triggerAsyncId: ${triggerAsyncId()}`); })

通過 asyncHooks.createHook 創(chuàng)建一個(gè) hooks 對(duì)象啟用 Promise 異步跟蹤。

const hooks = asyncHooks.createHook({}); hooks.enable();  Promise.resolve().then(() => {   // Promise asyncId: 7. Promise triggerAsyncId: 6   console.log(`Promise asyncId: ${asyncId()}. Promise triggerAsyncId: ${triggerAsyncId()}`); })

異步資源的生命周期

asyncHooks 的 createHook() 方法返回一個(gè)用于啟用(enable)和禁用(disable)hooks 的實(shí)例,該方法接收  init/before/after/destory 四個(gè)回調(diào)來標(biāo)志一個(gè)異步資源從初始化、回調(diào)調(diào)用之前、回調(diào)調(diào)用之后、銷毀整個(gè)生命周期過程。

init(初始化)

當(dāng)構(gòu)造一個(gè)可能發(fā)出異步事件的類時(shí)調(diào)用。

  • async:異步資源唯一 id

  • type:異步資源類型,對(duì)應(yīng)于資源的構(gòu)造函數(shù)名稱,更多類型參考 async_hooks_type

  • triggerAsyncId:當(dāng)前異步資源由哪個(gè)異步資源創(chuàng)建的異步資源 id

  • resource:初始化的異步資源

/**  * Called when a class is constructed that has the possibility to emit an asynchronous event.  * @param asyncId a unique ID for the async resource  * @param type the type of the async resource  * @param triggerAsyncId the unique ID of the async resource in whose execution context this async resource was created  * @param resource reference to the resource representing the async operation, needs to be released during destroy  */ init?(asyncId: number, type: string, triggerAsyncId: number, resource: object): void;

before(回調(diào)函數(shù)調(diào)用前)

當(dāng)啟動(dòng)異步操作(例如 TCP 服務(wù)器接收新鏈接)或完成異步操作(例如將數(shù)據(jù)寫入磁盤)時(shí),系統(tǒng)將調(diào)用回調(diào)來通知用戶,也就是我們寫的業(yè)務(wù)回調(diào)函數(shù)。在這之前會(huì)先觸發(fā) before 回調(diào)。

/**  * When an asynchronous operation is initiated or completes a callback is called to notify the user.  * The before callback is called just before said callback is executed.  * @param asyncId the unique identifier assigned to the resource about to execute the callback.  */ before?(asyncId: number): void;

after(回調(diào)函數(shù)調(diào)用后)

當(dāng)回調(diào)處理完成之后觸發(fā) after 回調(diào),如果回調(diào)出現(xiàn)未捕獲異常,則在觸發(fā) uncaughtException 事件或域(domain)處理之后觸發(fā)  after 回調(diào)。

/**  * Called immediately after the callback specified in before is completed.  * @param asyncId the unique identifier assigned to the resource which has executed the callback.  */ after?(asyncId: number): void;

destory(銷毀)

當(dāng) asyncId 對(duì)應(yīng)的異步資源被銷毀后調(diào)用 destroy 回調(diào)。一些資源的銷毀依賴于垃圾回收,因此如果對(duì)傳遞給 init  回調(diào)的資源對(duì)象有引用,則有可能永遠(yuǎn)不會(huì)調(diào)用 destory 從而導(dǎo)致應(yīng)用程序中出現(xiàn)內(nèi)存泄漏。如果資源不依賴?yán)厥眨@將不會(huì)有問題。

/**  * Called after the resource corresponding to asyncId is destroyed  * @param asyncId a unique ID for the async resource  */ destroy?(asyncId: number): void;

promiseResolve

當(dāng)傳遞給 Promise 構(gòu)造函數(shù)的 resolve() 函數(shù)執(zhí)行時(shí)觸發(fā) promiseResolve 回調(diào)。

/**   * Called when a promise has resolve() called. This may not be in the same execution id   * as the promise itself.   * @param asyncId the unique id for the promise that was resolve()d.   */ promiseResolve?(asyncId: number): void;

以下代碼會(huì)觸發(fā)兩次 promiseResolve() 回調(diào),第一次是我們直接調(diào)用的 resolve() 函數(shù),第二次是在 .then()  里雖然我們沒有顯示的調(diào)用,但是它也會(huì)返回一個(gè) Promise 所以還會(huì)被再次調(diào)用。

const hooks = asyncHooks.createHook({   promiseResolve(asyncId) {     syncLog('promiseResolve: ', asyncId);   } }); new Promise((resolve) => resolve(true)).then((a) => {});  // 輸出結(jié)果 promiseResolve:  2 promiseResolve:  3

注意 init 回調(diào)里寫日志造成 “棧溢出” 問題

一個(gè)異步資源的生命周期中第一個(gè)階段 init 回調(diào)是當(dāng)構(gòu)造一個(gè)可能發(fā)出異步事件的類時(shí)會(huì)調(diào)用,要注意由于使用 console.log()  輸出日志到控制臺(tái)是一個(gè)異步操作,在 AsyncHooks 回調(diào)函數(shù)中使用類似的異步操作將會(huì)再次觸發(fā) init 回調(diào)函數(shù),進(jìn)而導(dǎo)致無限遞歸出現(xiàn)  RangeError: Maximum call stack size exceeded 錯(cuò)誤,也就是 “ 棧溢出”。

調(diào)試時(shí),一個(gè)簡(jiǎn)單的記錄日志的方式是使用 fs.writeFileSync() 以同步的方式寫入日志,這將不會(huì)觸發(fā) AsyncHooks 的 init  回調(diào)函數(shù)。

const syncLog = (...args) => fs.writeFileSync('log.txt', `${util.format(...args)}\n`, { flag: 'a' }); const hooks = asyncHooks.createHook({   init(asyncId, type, triggerAsyncId, resource) {     syncLog('init: ', asyncId, type, triggerAsyncId)   } }); hooks.enable();  fs.open('hello.txt', (err, res) => {   syncLog(`fs.open asyncId: ${asyncId()}, fs.open triggerAsyncId: ${triggerAsyncId()}`); });

輸出以下內(nèi)容,init 回調(diào)只會(huì)被調(diào)用一次,因?yàn)?fs.writeFileSync 是同步的是不會(huì)觸發(fā) hooks 回調(diào)的。

init:  2 FSREQCALLBACK 1 fs.open asyncId: 2, fs.open triggerAsyncId: 1

異步之間共享上下文

Node.js v13.10.0 增加了 async_hooks 模塊的 AsyncLocalStorage  類,可用于在一系列異步調(diào)用中共享數(shù)據(jù)。

如下例所示,asyncLocalStorage.run() 函數(shù)第一個(gè)參數(shù)是存儲(chǔ)我們?cè)诋惒秸{(diào)用中所需要訪問的共享數(shù)據(jù),第二個(gè)參數(shù)是一個(gè)異步函數(shù),我們?cè)? setTimeout() 的回調(diào)函數(shù)里又調(diào)用了 test2 函數(shù),這一系列的異步操作都不影響我們?cè)谛枰牡胤饺カ@取  asyncLocalStorage.run() 函數(shù)中存儲(chǔ)的共享數(shù)據(jù)。

const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); asyncLocalStorage.run({ traceId: 1 }, test1); async function test1() {   setTimeout(() => test2(), 2000); } async function test2() {   console.log(asyncLocalStorage.getStore().traceId); }

AsyncLocalStorage 用途很多,例如在服務(wù)端必不可少的日志分析,一個(gè) HTTP 從請(qǐng)求到響應(yīng)整個(gè)系統(tǒng)交互的日志輸出如果能通過一個(gè)  traceId 來關(guān)聯(lián),在分析日志時(shí)也就能夠清晰的看到整個(gè)調(diào)用鏈路。

下面是一個(gè) HTTP 請(qǐng)求的簡(jiǎn)單示例,模擬了異步處理,并且在日志輸出時(shí)去追蹤存儲(chǔ)的 id

const http = require('http'); const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); function logWithId(msg) {   const id = asyncLocalStorage.getStore();   console.log(`${id !== undefined ? id : '-'}:`, msg); } let idSeq = 0; http.createServer((req, res) => {   asyncLocalStorage.run(idSeq++, () => {     logWithId('start');     setImmediate(() => {       logWithId('processing...');       setTimeout(() => {         logWithId('finish');         res.end();       }, 2000)     });   }); }).listen(8080);

下面是運(yùn)行結(jié)果,我在第一次調(diào)用之后直接調(diào)用了第二次,可以看到我們存儲(chǔ)的 id 信息與我們的日志一起成功的打印了出來。

如何使用Node.js的Async Hooks模塊追蹤異步資源

看完了這篇文章,相信你對(duì)“如何使用Node.js的Async Hooks模塊追蹤異步資源”有了一定的了解,如果想了解更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI