<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          在 Node.js 中使用 Async Hooks 處理 HTTP 請求上下文實現(xiàn)鏈路追蹤

          共 8067字,需瀏覽 17分鐘

           ·

          2021-02-02 09:26

          作者簡介:五月君,Software Designer,公眾號「Nodejs技術(shù)?!棺髡?。

          Async Hooks 一個實際的使用場景是存儲請求上下文,在異步調(diào)用之間共享數(shù)據(jù)。上節(jié)對基礎(chǔ)使用做了介紹,還沒看的參見之前的分享 使用 Node.js 的 Async Hooks 模塊追蹤異步資源。

          本節(jié)將會介紹如何基于 Async hooks 提供的 API 從零開始實現(xiàn)一個 AsyncLocalStorage 類(異步本地存儲)及在 HTTP 請求中關(guān)聯(lián)日志的 traceId 實現(xiàn)鏈路追蹤,這也是 Async Hooks 的一個實際應(yīng)用場景了。

          何為異步本地存儲?

          我們所說的異步本地存儲類似于多線程編程語言中的線程本地存儲。拿之前筆者寫過的 Java 做個舉例,例如 Java 中的 ThreadLocal 類,可以為使用相同變量的不同線程創(chuàng)建一個各自的副本,避免共享資源產(chǎn)生的沖突,在一個線程請求之內(nèi)通過 get()/set() 方法獲取或設(shè)置這個變量在當(dāng)前線程中對應(yīng)的副本值,在多線程并發(fā)訪問時線程之間各自創(chuàng)建的副本互不影響。

          在 Node.js 中我們的業(yè)務(wù)通常都工作在主線程(使用 work_threads 除外),是沒有 ThreadLocal 類的。并且以事件驅(qū)動的方式來處理所有的 HTTP 請求,每個請求過來之后又都是異步的,異步之間還很難去追蹤上下文信息,我們想做的是在這個異步事件開始,例如從接收 HTTP 請求到響應(yīng),能夠有一種機可以讓我們隨時隨地去獲取在這期間的一些共享數(shù)據(jù),也就是我們本節(jié)所提的異步本地存儲技術(shù)。

          在接下來我會講解實現(xiàn) AsyncLocalStorage 的四種方式,從最開始的手動實現(xiàn)到官方 v14.x 支持的 AsyncLocalStorage 類,你也可以從中學(xué)習(xí)到其實現(xiàn)原理。

          現(xiàn)有業(yè)務(wù)問題

          假設(shè),現(xiàn)在有一個需求對現(xiàn)有日志系統(tǒng)做改造,所有記錄日志的地方增加 traceId 實現(xiàn)全鏈路日志追蹤。

          一種情況是假設(shè)你使用一些類似 Egg.js 這樣的企業(yè)級框架,可以依賴于框架提供的中間件能力在請求上掛載 traceId,可以看看之前的一篇文章 基于 Egg.js 框架的日志鏈路追蹤實踐 也是可以實現(xiàn)的,不過當(dāng)時是基于 egg 的一個插件自己做了繼承實現(xiàn),現(xiàn)在已經(jīng)不需要這么麻煩,可以通過配置自定義日志格式來實現(xiàn) https://eggjs.org/zh-cn/core/logger.html#自定義日志格式。

          另一種情況假設(shè)你是用的 Express、Koa 這些基礎(chǔ)框架,所有業(yè)務(wù)都是模塊加載函數(shù)式調(diào)用,如果每次把請求的 traceId 手動在 Controller -> Service -> Model 之間傳遞,這樣對業(yè)務(wù)代碼的侵入太大了,日志與業(yè)務(wù)的耦合度就太高了。

          如下代碼,是我精簡后的一個例子,現(xiàn)在有一個需求,在不更改業(yè)務(wù)代碼的情況下每次日志打印都輸出當(dāng)前 HTTP 請求處理 Headers 中攜帶的 traceId 字段,如果是你會怎么做呢?

          //?logger.js
          const?logger?=?{
          ??info:?(...args)?=>?{
          ????console.log(...args);
          ??}
          }
          module.exports?=?{?logger?}

          //?app.js
          const?express?=?require('express');
          const?app?=?express();
          const?PORT?=?3000;
          const?{?logger?}?=?require('./logger');
          global.logger?=?contextLogger;

          app.use((req,?res,?next)?=>?contextLogger.run(req,?next));

          app.get('/logger',?async?(req,?res,?next)?=>?{
          ??try?{
          ???const?users?=?await?getUsersController();
          ???res.json({?code:?'SUCCESS',?message:?'',?data:?users?});
          ??}?catch?(error)?{
          ????res.json({?code:?'ERROR',?message:?error.message?})
          ??}
          });

          app.listen(PORT,?()?=>?console.log(`server?is?listening?on?${PORT}`));

          async?function?getUsersController()?{
          ??logger.info('Get?user?list?at?controller?layer.');
          ??return?getUsersService();
          }

          async?function?getUsersService()?{
          ??logger.info('Get?user?list?at?service?layer.');
          ??setTimeout(function()?{?logger.info('setTimeout?2s?at?service?layer.')?},?3000);
          ??return?getUsersModel();
          }

          async?function?getUsersModel()?{
          ??logger.info('Get?user?list?at?model?layer.');
          ??return?[];
          }

          方式一:動手實現(xiàn)異步本地存儲

          解決方案是實現(xiàn)請求上下文本地存儲,在當(dāng)前作用域代碼中能夠獲取上下文信息,待處理完畢清除保存的上下文信息,這些需求可以通過 Async Hooks 提供的 API 實現(xiàn)。

          創(chuàng)建 AsyncLocalStorage 類

          • 行 {1} 創(chuàng)建一個 Map 集合存儲上下文信息。
          • 行 {2} 里面的 init 回調(diào)是重點,當(dāng)一個異步事件被觸發(fā)前會先收到 init 回調(diào),其中 triggerAsyncId 是當(dāng)前異步資源的觸發(fā)者,我們則可以在這里獲取上個異步資源的信息存儲至當(dāng)前異步資源中。當(dāng) asyncId 對應(yīng)的異步資源被銷毀時會收到 destroy 回調(diào),所以最后要記得在 destroy 回調(diào)里清除當(dāng)前 asyncId 里存儲的信息。
          • 行 {3} 拿到當(dāng)前請求上下文的 asyncId 做為 Map 集合的 Key 存入傳入的上下文信息。
          • 行 {4} 拿到 asyncId 獲取當(dāng)前代碼的上下文信息。
          //?AsyncLocalStorage.js
          const?asyncHooks?=?require('async_hooks');
          const?{?executionAsyncId?}?=?asyncHooks;
          class?AsyncLocalStorage?{
          ??constructor()?{
          ????this.storeMap?=?new?Map();?//?{1}
          ????this.createHook();?//?{2}
          ??}
          ??createHook()?{
          ????const?ctx?=?this;
          ????const?hooks?=?asyncHooks.createHook({
          ??????init(asyncId,?type,?triggerAsyncId)?{
          ????????if?(ctx.storeMap.has(triggerAsyncId))?{
          ??????????ctx.storeMap.set(asyncId,?ctx.storeMap.get(triggerAsyncId));
          ????????}
          ??????},
          ??????destroy(asyncId)?{
          ????????ctx.storeMap.delete(asyncId);
          ??????}
          ????});
          ????hooks.enable();
          ??}
          ??run(store,?callback)?{?//?{3}
          ????this.storeMap.set(executionAsyncId(),?store);
          ????callback();
          ??}
          ??getStore()?{?//?{4}
          ????return?this.storeMap.get(executionAsyncId());
          ??}
          }
          module.exports?=?AsyncLocalStorage;

          注意,在我們定義的 createHook() 方法里有 hooks.enable(); 這樣一段代碼,這是因為 Promise 默認是沒有開啟的,通過顯示的調(diào)用可以開啟 Promise 的異步追蹤。

          改造 logger.js 文件

          在我們需要打印日志的地方拿到當(dāng)前代碼所對應(yīng)的上下文信息,取出我們存儲的 traceId, 這種方式只需要改造我們?nèi)罩局虚g即可,不需要去更改我們的業(yè)務(wù)代碼。

          const?{?v4:?uuidV4?}?=?require('uuid');
          const?AsyncLocalStorage?=?require('./AsyncLocalStorage');
          const?asyncLocalStorage?=?new?AsyncLocalStorage();

          const?logger?=?{
          ??info:?(...args)?=>?{
          ????const?traceId?=?asyncLocalStorage.getStore();
          ????console.log(traceId,?...args);
          ??},
          ??run:?(req,?callback)?=>?{
          ????asyncLocalStorage.run(req.headers.requestId?||?uuidV4(),?callback);
          ??}
          }

          module.exports?=?{
          ??logger,
          }

          改造 app.js 文件

          注冊一個中間件,傳遞請求信息。

          app.use((req,?res,?next)?=>?logger.run(req,?next));

          運行后輸出結(jié)果

          e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a?Get?user?list?at?router?layer.
          e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a?Get?user?list?at?controller?layer.
          e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a?Get?user?list?at?service?layer.
          e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a?Get?user?list?at?model?layer.
          e82d1a1f-5038-4ac9-a9c8-2aa5abb0f96a?setTimeout?2s?at?service?layer.

          這種方式就是完全基于 Async Hooks 提供的 API 來實現(xiàn),不理解其實現(xiàn)原理的可以在動手實踐下,這種方式需要我們額外維護維護一個 Map 對象,還要處理銷毀操作。

          方式二:executionAsyncResource() 返回當(dāng)前執(zhí)行的異步資源

          executionAsyncResource() 返回當(dāng)前執(zhí)行的異步資源,這對于實現(xiàn)連續(xù)的本地存儲很有幫助,無需像 “方式一” 再創(chuàng)建一個 Map 對象來存儲元數(shù)據(jù)。

          const?asyncHooks?=?require('async_hooks');
          const?{?executionAsyncId,?executionAsyncResource?}?=?asyncHooks;

          class?AsyncLocalStorage?{
          ??constructor()?{
          ????this.createHook();
          ??}
          ??createHook()?{
          ????const?hooks?=?asyncHooks.createHook({
          ??????init(asyncId,?type,?triggerAsyncId,?resource)?{
          ????????const?cr?=?executionAsyncResource();
          ????????if?(cr)?{
          ??????????resource[asyncId]?=?cr[triggerAsyncId];
          ????????}
          ??????}
          ????});
          ????hooks.enable();
          ??}
          ??run(store,?callback)?{
          ????executionAsyncResource()[executionAsyncId()]?=?store;
          ????callback();
          ??}
          ??getStore()?{
          ????return?executionAsyncResource()[executionAsyncId()];
          ??}
          }

          module.exports?=?AsyncLocalStorage;

          方式三:基于 ResourceAsync 創(chuàng)建 AsyncLocalStorage 類

          ResourceAysnc 可以用來自定義異步資源,此處的介紹也是參考 Node.js 源碼對 AsyncLocalStorage 的實現(xiàn)。

          一個顯著的改變是 run() 方法,每一次的調(diào)用都會創(chuàng)建一個資源,調(diào)用其 runInAsyncScope() 方法,這樣在這個資源的異步作用域下,所執(zhí)行的代碼(傳入的 callback)都是可追蹤我們設(shè)置的 store。

          const?asyncHooks?=?require('async_hooks');
          const?{?executionAsyncResource,?AsyncResource?}?=?asyncHooks;

          class?AsyncLocalStorage?{
          ??constructor()?{
          ????this.kResourceStore?=?Symbol('kResourceStore');
          ????this.enabled?=?false;
          ????const?ctx?=?this;
          ????this.hooks?=?asyncHooks.createHook({
          ??????init(asyncId,?type,?triggerAsyncId,?resource)?{
          ????????const?currentResource?=?executionAsyncResource();
          ????????ctx._propagate(resource,?currentResource)
          ??????}
          ????});
          ??}

          ??//?Propagate?the?context?from?a?parent?resource?to?a?child?one
          ??_propagate(resource,?triggerResource)?{
          ????const?store?=?triggerResource[this.kResourceStore];
          ????if?(store)?{
          ??????resource[this.kResourceStore]?=?store;
          ????}
          ??}

          ??_enable()?{
          ????if?(!this.enabled)?{
          ??????this.enabled?=?true;
          ??????this.hooks.enable();
          ????}
          ??}

          ??enterWith(store)?{
          ????this._enable();
          ????const?resource?=?executionAsyncResource();
          ????resource[this.kResourceStore]?=?store;
          ??}

          ??run(store,?callback)?{
          ????const?resource?=?new?AsyncResource('AsyncLocalStorage',?{
          ??????requireManualDestroy:?true,
          ????});
          ????return?resource.emitDestroy().runInAsyncScope(()?=>?{
          ??????this.enterWith(store);
          ??????return?callback();
          ????});
          ??}

          ??getStore()?{
          ????return?executionAsyncResource()[this.kResourceStore];
          ??}
          }

          module.exports?=?AsyncLocalStorage;

          方式四:AsyncLocalStorage 類

          Node.js v13.10.0 async_hooks 模塊新加入了 AsyncLocalStorage 類,實例化一個對象調(diào)用 run() 方法實現(xiàn)本地存儲,也是推薦的方式,不需要自己去再額外維護一個 AsyncLocalStorage 類。

          AsyncLocalStorage 類的實現(xiàn)也就是上面講解的方式三,所以也不需要我們在外部顯示的調(diào)用 hooks.enable() 來啟用 Promise 異步追蹤,因為其內(nèi)部已經(jīng)實現(xiàn)了。

          const?{?AsyncLocalStorage?}?=?require('async_hooks');

          Async Hooks 的性能開銷

          這一點是大家最關(guān)心的問題,如果開啟了 Async Hooks(Promise 需要調(diào)用 Async Hooks 實例的 enable() 方法開啟)每一次異步操作或 Promise 類型的操作,包括 console 只要是異步的都會觸發(fā) hooks,也必然是有性能開銷的。

          參考 Kuzzle 的性能基準測試,使用了 AsyncLocalStorage 與未使用之間相差 ~8%。

          ----Log with AsyncLocalStorageLog classicdifference
          req/s26132842?8%

          當(dāng)然不同的業(yè)務(wù)也有著不同的差異,如果你擔(dān)心會有很大的性能開銷,可以基于自己的業(yè)務(wù)做一些基準測試。

          Reference

          [1]

          nodejs.org/api/async_hooks.html: https://nodejs.org/api/async_hooks.html

          [2]

          Node.js 14 & AsyncLocalStorage: Share a context between asynchronous calls: https://blog.kuzzle.io/nodejs-14-asynclocalstorage-asynchronous-calls

          [3]

          在 Node 中通過 Async Hooks 實現(xiàn)請求作用域: https://mp.weixin.qq.com/s/I22TvmTqCKFClsp0YLDoZw

          [4]

          Async Hooks 性能影響: https://github.com/nodejs/benchmarking/issues/181

          [5]

          kuzzle 基準測試: https://github.com/kuzzleio/kuzzle/pull/1604



          最后


          • 歡迎加我微信(winty230),拉你進技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認真學(xué)前端,做個專業(yè)的技術(shù)人...

          點個在看支持我吧
          瀏覽 72
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天日天天射天天操 | 人成无码| 国产A91精品 | 日韩无码第二页 | 美国A级毛片 |