• 當前位置:聯(lián)升科技 > 技術(shù)資訊 > 行業(yè)動(dòng)態(tài) >

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

    2021-01-26    作者:五月君編程    來(lái)源:Nodejs技術(shù)棧    閱讀: 次

    作者簡(jiǎn)介:五月君,Software Designer,公眾號「Nodejs技術(shù)?!棺髡?。
    Async Hooks 功能是 Node.js v8.x 版本新增加的一個(gè)核心模塊,它提供了 API 用來(lái)追蹤 Node.js 程序中異步資源的聲明周期,可在多個(gè)異步調用之間共享數據,本文從最基本入門(mén)篇開(kāi)始學(xué)習,之后會(huì )有在某些場(chǎng)景下具體應用實(shí)踐篇介紹。
    executionAsyncId 和 triggerAsyncId
    async hooks 模塊提供了 executionAsyncId() 函數標志當前執行上下文的異步資源 Id,下文使用 asyncId 表示。還有一個(gè) triggerAsyncId() 函數來(lái)標志當前執行上下文被觸發(fā)的異步資源 Id,也就是當前異步資源是由哪個(gè)異步資源創(chuàng )建的。每個(gè)異步資源都會(huì )生成 asyncId,該 id 會(huì )呈遞增的方式生成,且在 Node.js 當前實(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()}`); 
    }); 
    下面是我們運行的結果,全局的 asyncId 為 1,fs.open 回調里打印的 triggerAsyncId 為 1 由全局觸發(fā)。
    Global asyncId: 1, Global triggerAsyncId: 0 
    fs.open asyncId: 5, fs.open triggerAsyncId: 1 
    默認未開(kāi)啟的 Promise 執行跟蹤
    默認情況下,由于 V8 提供的 promise introspection API 相對消耗性能,Promise 的執行沒(méi)有分配 asyncId。這意味著(zhù)默認情況下,使用了 Promise 或 Async/Await 的程序將不能正確的執行和觸發(fā) Promise 回調上下文的 ID。即得不到當前異步資源 asyncId 也得不到當前異步資源是由哪個(gè)異步資源創(chuàng )建的 triggerAsyncId,如下所示:
    Promise.resolve().then(() => { 
      // Promise asyncId: 0. Promise triggerAsyncId: 0 
      console.log(`Promise asyncId: ${asyncId()}. Promise triggerAsyncId: ${triggerAsyncId()}`); 
    }) 
    通過(guò) asyncHooks.createHook 創(chuàng )建一個(gè) hooks 對象啟用 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è)回調來(lái)標志一個(gè)異步資源從初始化、回調調用之前、回調調用之后、銷(xiāo)毀整個(gè)生命周期過(guò)程。
    init(初始化)
    當構造一個(gè)可能發(fā)出異步事件的類(lèi)時(shí)調用。
    async:異步資源唯一 id
    type:異步資源類(lèi)型,對應于資源的構造函數名稱(chēng),更多類(lèi)型參考 async_hooks_type
    triggerAsyncId:當前異步資源由哪個(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(回調函數調用前)
    當啟動(dòng)異步操作(例如 TCP 服務(wù)器接收新鏈接)或完成異步操作(例如將數據寫(xiě)入磁盤(pán))時(shí),系統將調用回調來(lái)通知用戶(hù),也就是我們寫(xiě)的業(yè)務(wù)回調函數。在這之前會(huì )先觸發(fā) before 回調。
    /** 
     * 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(回調函數調用后)
    當回調處理完成之后觸發(fā) after 回調,如果回調出現未捕獲異常,則在觸發(fā) uncaughtException 事件或域(domain)處理之后觸發(fā) after 回調。
    /** 
     * 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(銷(xiāo)毀)
    當 asyncId 對應的異步資源被銷(xiāo)毀后調用 destroy 回調。一些資源的銷(xiāo)毀依賴(lài)于垃圾回收,因此如果對傳遞給 init 回調的資源對象有引用,則有可能永遠不會(huì )調用 destory 從而導致應用程序中出現內存泄漏。如果資源不依賴(lài)垃圾回收,這將不會(huì )有問(wèn)題。
    /** 
     * Called after the resource corresponding to asyncId is destroyed 
     * @param asyncId a unique ID for the async resource 
     */ 
    destroy?(asyncId: number): void; 
    promiseResolve
    當傳遞給 Promise 構造函數的 resolve() 函數執行時(shí)觸發(fā) promiseResolve 回調。
    /** 
      * 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() 回調,第一次是我們直接調用的 resolve() 函數,第二次是在 .then() 里雖然我們沒(méi)有顯示的調用,但是它也會(huì )返回一個(gè) Promise 所以還會(huì )被再次調用。
    const hooks = asyncHooks.createHook({ 
      promiseResolve(asyncId) { 
        syncLog('promiseResolve: ', asyncId); 
      } 
    }); 
    new Promise((resolve) => resolve(true)).then((a) => {}); 
     
    // 輸出結果 
    promiseResolve:  2 
    promiseResolve:  3 
    注意 init 回調里寫(xiě)日志造成 “棧溢出” 問(wèn)題
    一個(gè)異步資源的生命周期中第一個(gè)階段 init 回調是當構造一個(gè)可能發(fā)出異步事件的類(lèi)時(shí)會(huì )調用,要注意由于使用 console.log() 輸出日志到控制臺是一個(gè)異步操作,在 AsyncHooks 回調函數中使用類(lèi)似的異步操作將會(huì )再次觸發(fā) init 回調函數,進(jìn)而導致無(wú)限遞歸出現 RangeError: Maximum call stack size exceeded 錯誤,也就是 “ 棧溢出”。
    調試時(shí),一個(gè)簡(jiǎn)單的記錄日志的方式是使用 fs.writeFileSync() 以同步的方式寫(xiě)入日志,這將不會(huì )觸發(fā) AsyncHooks 的 init 回調函數。
    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()}`); 
    }); 
    輸出以下內容,init 回調只會(huì )被調用一次,因為 fs.writeFileSync 是同步的是不會(huì )觸發(fā) hooks 回調的。
    init:  2 FSREQCALLBACK 1 
    fs.open asyncId: 2, fs.open triggerAsyncId: 1 
    異步之間共享上下文
    Node.js v13.10.0 增加了 async_hooks 模塊的 AsyncLocalStorage 類(lèi),可用于在一系列異步調用中共享數據。
    如下例所示,asyncLocalStorage.run() 函數第一個(gè)參數是存儲我們在異步調用中所需要訪(fǎng)問(wèn)的共享數據,第二個(gè)參數是一個(gè)異步函數,我們在 setTimeout() 的回調函數里又調用了 test2 函數,這一系列的異步操作都不影響我們在需要的地方去獲取 asyncLocalStorage.run() 函數中存儲的共享數據。
    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 從請求到響應整個(gè)系統交互的日志輸出如果能通過(guò)一個(gè) traceId 來(lái)關(guān)聯(lián),在分析日志時(shí)也就能夠清晰的看到整個(gè)調用鏈路。
    下面是一個(gè) HTTP 請求的簡(jiǎn)單示例,模擬了異步處理,并且在日志輸出時(shí)去追蹤存儲的 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); 
    下面是運行結果,我在第一次調用之后直接調用了第二次,可以看到我們存儲的 id 信息與我們的日志一起成功的打印了出來(lái)。
    image.png
    在下一節會(huì )詳細介紹, 如何在 Node.js 中使用 async hooks 模塊的 AsyncLocalStorage 類(lèi)處理請求上下文, 也會(huì )詳細講解 AsyncLocalStorage 類(lèi)是如何實(shí)現的本地存儲。


    相關(guān)文章

    我們很樂(lè )意傾聽(tīng)您的聲音!
    即刻與我們取得聯(lián)絡(luò )
    成為日后肩并肩合作的伙伴。

    行業(yè)資訊

    聯(lián)系我們

    13387904606

    地址:新余市仙女湖區仙女湖大道萬(wàn)商紅A2棟

    手機:13755589003
    QQ:122322500
    微信號:13755589003

    江西新余網(wǎng)站設計_小程序制作_OA系統開(kāi)發(fā)_企業(yè)ERP管理系統_app開(kāi)發(fā)-新余聯(lián)升網(wǎng)絡(luò )科技有限公司 贛ICP備19013599號-1   贛公網(wǎng)安備 36050202000267號   

    微信二維碼
    色噜噜狠狠一区二区三区果冻|欧美亚洲日本国产一区|国产精品无码在线观看|午夜视频在线观看一区|日韩少妇一区二区无码|伊人亚洲日韩欧美一区二区|国产在线码观看清码视频