Node.js 的 Async Hooks 模块追踪异步资源

2023-05-26 0 422

Node.js 的 Async Hooks 模块追踪异步资源

Async Hooks 机能是 Node.js v8.x 版新减少的两个核心理念组件,它提供更多了 API 用以跟踪 Node.js 流程中促发器天然资源的新闻稿周期性,可在数个促发器初始化间共享资源统计数据,责任编辑从最基本上入门篇已经开始自学,后会有在这类情景下具体内容应用领域课堂教学篇如是说。

executionAsyncId 和 triggerAsyncId

async hooks 组件提供更多了 executionAsyncId() 表达式象征现阶段继续执行语句的促发器天然资源 Id,Nenon采用 asyncId 则表示。除了两个 triggerAsyncId() 表达式来象征现阶段继续执行语句被促发的促发器天然资源 Id,也是现阶段促发器天然资源是由别的促发器天然资源建立的。每一促发器天然资源单厢聚合 asyncId,该 id 会呈递减的形式聚合,且在 Node.js 现阶段示例里自上而下惟一。

const asyncHooks = require(async_hooks); const fs = require(fs); const asyncId = () => asyncHooks.executionAsyncId(); consttriggerAsyncId =() => 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 由自上而下促发。

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

默认未开启的 Promise 继续执行跟踪

默认情况下,由于 V8 提供更多的 promise introspection API 相对消耗性能,Promise 的继续执行没有分配 asyncId。这意味着默认情况下,采用了 Promise 或 Async/Await 的流程将不能正确的继续执行和促发 Promise 回调语句的 ID。即得不到现阶段促发器天然资源 asyncId 也得不到现阶段促发器天然资源是由别的促发器天然资源建立的 triggerAsyncId,如下所示:

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

通过 asyncHooks.createHook 建立两个 hooks 对象启用 Promise 促发器跟踪。

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

促发器天然资源的生命周期性

asyncHooks 的 createHook() 方法返回两个用于启用(enable)和禁用(disable)hooks 的示例,该方法接收 init/before/after/destory 四个回调来象征两个促发器天然资源从初始化、回调初始化之前、回调初始化后、销毁整个生命周期性过程。

init(初始化)

当构造两个可能发出促发器事件的类时初始化。

async:促发器天然资源惟一 idtype:促发器天然资源类型,对应于天然资源的构造表达式名称,更多类型参考 async_hooks_typetriggerAsyncId:现阶段促发器天然资源由别的促发器天然资源建立的促发器天然资源 idresource:初始化的促发器天然资源/** * Called when a class is constructed that has the possibility to emit an asynchronous event. *@param asyncId a unique ID for the async resource * @paramtype the type of the async resource *@paramtriggerAsyncId the unique ID of the async resource in whose execution context this async resource was created *@paramresource 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(回调表达式初始化前)

当启动促发器操作(例如 TCP 服务器接收新链接)或完成促发器操作(例如将统计数据写入磁盘)时,系统将初始化回调来通知用户,也是我们写的业务回调表达式。在这之前会先促发 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(回调表达式初始化后)

当回调处理完成后促发 after 回调,如果回调出现未捕获异常,则在促发 uncaughtException 事件或域(domain)处理后促发 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(销毁)

当 asyncId 对应的促发器天然资源被销毁后初始化 destroy 回调。一些天然资源的销毁依赖于垃圾回收,因此如果对传递给 init 回调的天然资源对象有引用,则有可能永远不会初始化 destory 从而导致应用领域流程中出现内存泄漏。如果天然资源不依赖垃圾回收,这将不会有问题。

/** * 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() 表达式继续执行时促发 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;

以下代码会促发两次 promiseResolve() 回调,第一次是我们直接初始化的 resolve() 表达式,第二次是在 .then() 里虽然我们没有显示的初始化,但是它也会返回两个 Promise 所以还会被再次初始化。

consthooks = asyncHooks.createHook({ promiseResolve(asyncId) { syncLog(promiseResolve: , asyncId); } }); new Promise((resolve) => resolve(true)).then((a) => {}); // 输出结果 promiseResolve: 2 promiseResolve: 3

注意 init 回调里写日志造成 “栈溢出” 问题

两个促发器天然资源的生命周期性中第两个阶段 init 回调是当构造两个可能发出促发器事件的类时会初始化,要注意由于采用 console.log() 输出日志到控制台是两个促发器操作,在 AsyncHooks 回调函数中采用类似的促发器操作将会再次促发 init 回调表达式,进而导致无限递归出现 RangeError: Maximum call stack size exceeded 错误,也是 “ 栈溢出”。

调试时,两个简单的记录日志的形式是采用 fs.writeFileSync() 以同步的形式写入日志,这将不会促发 AsyncHooks 的 init 回调表达式。

const syncLog = (…args) => fs.writeFileSync(log.txt, `${util.format(…args)}\n`, { flag: a }); consthooks = 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 回调只会被初始化一次,因为 fs.writeFileSync 是同步的是不会促发 hooks 回调的。

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

促发器间共享资源语句

Node.js v13.10.0 减少了 async_hooks 组件的 AsyncLocalStorage 类,可用于在一系列促发器初始化中共享资源统计数据。

如下例所示,asyncLocalStorage.run() 表达式第两个参数是存储我们在促发器初始化中所需要访问的共享资源统计数据,第二个参数是两个促发器表达式,我们在 setTimeout() 的回调表达式里又初始化了 test2 表达式,这一系列的促发器操作都

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 用途很多,例如在服务端必不可少的日志分析,两个 HTTP 从请求到响应整个系统交互的日志输出如果能通过两个 traceId 来关联,在分析日志时也就能够清晰的看到整个初始化链路。

下面是两个 HTTP 请求的简单示例,模拟了促发器处理,并且在日志输出时去跟踪存储的 id

const http = require(http); const { AsyncLocalStorage } = require(async_hooks); const asyncLocalStorage = newAsyncLocalStorage();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 信息与我们的日志一起成功的打印了出来。

Node.js 的 Async Hooks 模块追踪异步资源

在下一节会详细如是说, 如何在 Node.js 中采用 async hooks 组件的 AsyncLocalStorage 类处理请求语句, 也会详细讲解 AsyncLocalStorage 类是如何实现的本地存储。

Reference

https://nodejs.org/dist/latest-v14.x/docs/api/async_hooks.html

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务