学习使用 NodeJs 中 async-hooks 模块

KittyThomas 发布于1年前

学习使用 NodeJs 中 async-hooks 模块

Async Hooks 是 Node8 新出来的特性,提供了一些 API 用于跟踪 NodeJs 中的异步资源的生命周期,属于 Node 内置模块,可以直接引用:

let asycnHooks = require('async_hooks');

之所以会引入 async_hooks 模块,是因为在异步调用中我们很难正确的追踪异步调用的处理逻辑及关系。而 async_hooks 模块友好的解决了上述问题,主要提供以下功能和特性:

  • 每一个函数都会提供一个上下文,我们称之为 async scope;
  • 每一个 async scope 中都有一个 asyncId, 是当前 async scope 的标志,同一个的 async scope 中 asyncId 必然相同,最外层的 asyncId 是 1,每个异步资源在创建时 asyncId 全量递增的;
  • 每一个 async scope 中都有一个 triggerAsyncId 表示当前函数是由那个 async scope 触发生成的;
  • 通过 asyncId 和 triggerAsyncId 我们可以很方便的追踪整个异步的调用关系及链路;
  • 我们可以通过 async_hooks.createHook 函数来注册关于每个异步资源在生命周期中发生的 init/before/after/destory/promiseResolve 等相关事件的监听函数;
  • 同一个 async scope 可能会被调用及执行多次,不管执行多少次,其 asyncId 必然相同,通过监听函数,我们很方便追踪其执行的次数及时间及上线文关系;

executionAsyncId 和 triggerAsyncId

async_hooks 模块提供了 executionAsyncId 函数和 triggerAsyncId 函数来获取当前上下文的 asyncId 和 triggerAsyncId:

const async_hooks = require('async_hooks');
const fs = require('fs');
console.log('global.asyncId:', async_hooks.executionAsyncId());  // global.asyncId: 1
console.log('global.triggerAsyncId:', async_hooks.triggerAsyncId()); // global.triggerAsyncId: 0
fs.open('./app.js', 'r', (err, fd) => {
    console.log('fs.open.asyncId:', async_hooks.executionAsyncId()); // fs.open.asyncId: 7
    console.log('fs.open.triggerAsyncId:', async_hooks.triggerAsyncId()); // fs.open.triggerAsyncId: 1
});

通过上述打印结果我们可以看到全局的 asyncId 为 1,而 fs.open 回调函数的 asyncId 为 7,triggerAsyncId为 1。可以看出 fs.open 这个异步资源是由全局进来调用触发的。

async_hooks.createHook(callbacks)

我们可以使用 async_hooks.createHook 来创建一个异步资源的钩子,注册一些关于异步资源生命周期中可能发生事件的回调函数作为 async_hooks.createHook 的输入。每当异步资源被创建/执行/销毁时这些钩子函数会被触发:

const async_hooks = require('async_hooks');
const asyncHook = async_hooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) { },
  destroy(asyncId) { }
});
asyncHook.enable();   //通过 enable 函数开启钩子功能

目前 createHook 函数可以接受五类 Hook Callbacks 如下:

init(asyncId, type, triggerAsyncId, resource)

init 回调函数一般在异步资源初始化的时候被触发,其参数解释如下:

  • asyncId: 每一个异步资源都会生成一个唯一性标志
  • type: 异步资源的类型,一般都是资源的构造函数的名字,想知道都有哪些可以参考async_hooks官方文档
  • triggerAsyncId: 表示触发当前异步资源被创建的对应的 async scope 的 asyncId
  • resource: 代表被初始化的异步资源对象

before(asyncId)

before 回调函数一般在 asyncId 对应的异步资源操作完成后准备执行回调前被调用,before 回调函数可能被执行多次,由其被回调的次数决定。

after(asyncId)

after 回调函数一般在异步资源执行完回调函数后会立即被调用,如果在执行回调函数的过程中发生未捕获的异常,after 事件会在触发 “uncaughtException” 事件后被调用。

destroy(asyncId)

当 asyncId 对应的异步资源被销毁时调用,有些异步资源的销毁要依赖垃圾回收机制,所以有些情况下由于内存泄漏的原因,destory 事件可能永远不会被触发。

promiseResolve(asyncId)

当 Promise 构造器中的 resovle 函数被执行时,promiseResolve 事件被触发。有些情况下,有些 resolve 函数是被隐式执行的,比如 .then 函数会返回一个新的 Promise,这个时候也会被调用。

以下表达式会触发两次 promiseResolve 事件,第一次是 new Promise() 时被显式执行的 resolve 函数, 第二次是.then函数的回调中被隐式执行的 resolve 函数

new Promise((resolve) => resolve(true)).then((a) => {});

Promise 的执行追踪

由于 V8 的 promise introspection API 对于获取 asyncId 的执行成本比较高,所以默认情况下,我们是不给 Promise 分配新的 asyncId。也就是说默认情况下,我们使用 promises 或者 async/await 时是获取不到当前上下文正确的 asyncId 和 triggerId 。不过没关系,我们可以通过执行 async_hooks.createHook(callbacks).enable() 函数强制开启对 Promise 分配 asyncId:

const ah = require('async_hooks');
ah.createHook({ init() {} }).enable(); // PromiseHooks 会被强制开启
Promise.resolve(1729).then(() => {
  console.log(`asyncId ${ah.executionAsyncId()} triggerId ${ah.triggerAsyncId()}`);
});

另外需要注意的是, beforeafter 事件的钩子函数只会在 Promise 的链式调用时被触发,也就是只有在 .then/.catch 函数中生成的 Promise 时会被触发,其它地方只会触发 init 和 promiseResolve 钩子事件函数。

所以当我们执行如何操作时:

new Promise((resolve) => resolve(true)).then((a) => {});

分别被触发的事件流程如下(假设两次 Promise 上下文对应的 asyncId 分别为 5, 6):

  1. asyncId = 5 的 init 事件函数
  2. asyncId = 5 的 promiseResolve 事件函数
  3. asyncId = 6 的 init 事件函数
  4. asyncId = 6 的 before 事件函数
  5. asyncId = 6 的 promiseResovle 事件函数
  6. asyncId = 6 的 after 事件函数

异常处理

如果一个 AsyncHook 回调函数中发生异常,那么服务将打印错误日志并立即退出,同时所有 ‘uncaughtException’ 监听器将被移除,同时会触发 ‘exit’ 事件。之所以会立即退出进程,是因为如果这些 AsyncHook 函数运行不稳定,下一个相同事件被触发时很可能又抛出异常,这些函数主要就是为了监听异步事件的,如果不稳定应该及时发现并进行更正。

日志打印

由于 console.log 函数也是一个异步调用,如果我们在 asyncHook 函数中再调用 console.log 那么将再次触发相应的 hook 事件,造成死循环调用,所以我们在 asyncHook 函数中必须使用同步打印日志方式来跟踪,可以使用 fs.writeSync 函数:

async_hooks.createHook({
  init(asyncId, type, triggerAsyncId) {
    const eid = async_hooks.executionAsyncId();
    fs.writeSync(
      1, `${type}(${asyncId}): trigger: ${triggerAsyncId} execution: ${eid}\n`);
  }
}).enable();

参考文献

 

查看原文: 学习使用 NodeJs 中 async-hooks 模块

  • ticklishpeacock
  • reddog
  • orangekoala
  • purpleostrich
  • crazybutterfly
  • crazytiger
  • beautifullion
  • yellowlion
  • bigpanda
  • crazyleopard
  • greenkoala
  • brownladybug
  • beautifulpeacock
  • ticklishtiger
  • whitegoose
  • whitekoala