由于 Node.js 应用中遍地都是异步操作,并且 Node.js 的异步操作在 EventLoop 主线程与 Worker 线程中穿插执行,因此想要对整个调用链(比如一个 HTTP 请求从接收到响应的整个过程)进行追踪、故障快速定位等操作将变得异常繁琐,为此 Node.js 提供了 async_hooks
模块来实现对异步操作的追踪;本文将通过异步资源(AsyncResource)、 异步钩子(AsyncHooks)、AsyncLocalStorage 三个方面来为大家详细介绍 async_hooks
模块的使用。
AsyncResource
在详细介绍 async_hooks
模块之前,我们需要对即异步资源进行阐述说明:
-
异步资源是在调用异步操作时创建的,比如下面的
setTimeout
调用:const res = setTimeout(() => {}, 1000); console.log(res); 复制代码
上例中
res
的结构如下所示:Timeout { _idleTimeout: 1000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 28, _onTimeout: [Function (anonymous)], _timerArgs: undefined, _repeat: null, _destroyed: false, [Symbol(refed)]: true, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 2, [Symbol(triggerId)]: 1 } 复制代码
-
异步资源主要用于跟踪异步操作,以便异步操作完成后,EventLoop 能够正确地执行相关回调;
-
等到相关异步操作完成并且其回调被成功触发后,异步资源也会像其它对象一样被系统回收,比如:
const res = setTimeout(() => { console.log(res); }, 1000); console.log(res); setTimeout(() => { console.log(res); }, 1000); 复制代码
执行上面的代码,我们会发现只有在
res
关联的异步操作完成并且其回调被成功触发后,res
的_destroyed
才会被设置为true
:Timeout { _destroyed: false, } Timeout { _destroyed: false, } Timeout { _destroyed: true, } 复制代码
除了 Timeout
这些 Node.js 内置的异步资源外,我们也可以通过 async_hooks
模块下的 AsyncResource
类来创建自己的异步资源,其构造函数的参数如下:
-
type
:资源类型(即名称); -
options
:选项设置,相关属性如下:-
triggerAsyncId
:创建该异步资源的异步资源上下文编号,默认取async_hooks.executionAsyncId()
的值; -
requireManualDestroy
:- 值为
false
时,Node.js 在对资源进行垃圾回收时,会自动触发该资源所关联的destroy
钩子; - 值为
true
时,Node.js 在对资源进行垃圾回收时,不会自动触发该资源所关联的destroy
钩子,需要显示调用AsyncResource
的emitDestroy
方法来触发; - 默认值为
false
。
- 值为
-
AsyncResource
的主要接口如下:
-
AsyncResource.bind
:AsyncResource 的静态方法,把指定的fn
函数绑定在当前所属的异步资源上下文中(即在调用AsyncResource.bind
时所在的异步资源上下文);该方法的参数如下:fn
:要绑定到当前所属的异步资源上下文中的执行函数;type
:异步资源类型,通过该属性与相关的AsyncResource
进行关联,该参数为非必填参数;thisArg
:指定fn
调用时this
的值,该参数为非必填参数。
-
bind
:把指定的fn
函数绑定在当前AsyncResource
实例所属的异步资源上下文中;该方法的参数如下:fn
:要绑定到当前AsyncResource
实例所属的异步资源上下文中的执行函数;thisArg
:指定fn
调用时this
的值,该参数为非必填参数。
-
runInAsyncScope
:在当前AsyncResource
实例所属的异步上下文中执行指定的fn
函数;该方法的参数如下:fn
:在全新的异步资源上下文中执行的函数;thisArg
:指定fn
调用时this
的值,该参数为非必填参数;...args
:指定fn
调用时传递给fn
的参数列表,该参数为非必填参数。
-
emitDestroy
:调用destroy
钩子,该方法只能被调用一次,多次调用将抛出异常; -
asyncId
:获取分配给当前AsyncResource
实例的唯一编号; -
triggerAsyncId
:获取创建当前AsyncResource
实例的异步资源上下文编号。
AsyncHook
上文我们对异步资源进行了介绍,async_hooks
提供了一系列的钩子函数来跟踪这些异步资源的整个生命周期(如下图所示),本节我们就对相关钩子函数的使用做简短介绍。
由图可知,在一个异步资源的生命周期中,主要包含以下几个钩子:
-
init
:对异步资源初始化时触发; -
promiseResolve
:- 对
Promise
对象执行resolve
或reject
操作时触发; - 调用
Promise
对象的then
或catch
方法时触发,此时在触发promiseResolve
的前后会分别触发before
和after
钩子。
我们看下面的例子:
const fs = require('fs'); const { createHook } = require('async_hooks'); const hook = createHook({ promiseResolve (asyncId) { fs.appendFileSync('log.out', `promiseResolve ${asyncId}\n`); }, before(asyncId) { fs.appendFileSync('log.out', `before ${asyncId}\n`); }, after(asyncId) { fs.appendFileSync('log.out', `after ${asyncId}\n`); }, }); hook.enable(); new Promise((resolve) => resolve(true)).then((a) => {}); 复制代码
执行上面的代码,然后查看
log.out
的输出:promiseResolve 2 before 3 promiseResolve 3 after 3 复制代码
通过上面的输出可知,
promiseResolve
在Promise
对象构造函数中调用resolve
时及调用了Promise
对象的then
时各触发了一次,并且第二次同时触发了before
与after
钩子。 - 对
-
before
:执行异步操作的回调函数之前触发; -
after
:执行异步操作的回调函数之后触发; -
destroy
:异步资源被销毁之后触发。
如上所述,async_hooks
提供了 init
、promiseResolve
、before
、after
及 destroy
几个钩子,并通过 async_hooks.createHook
来创建并启用相关钩子,比如:
const { createHook } = require('async_hooks');
const hook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
},
promiseResolve(asyncId) {
},
before(asyncId) {
},
after(asyncId) {
},
destroy(asyncId) {
}
});
hook.enable();
复制代码
上述钩子的参数解释如下:
asyncId
:异步资源的唯一编号;type
:异步资源的类型(即名称),点击查看 Node.js 内置资源类型;triggerAsyncId
:创建异步资源的异步资源上下文编号;resource
:异步资源的引用。
在使用上述钩子时,需要注意以下几点:
- 对于每个异步资源,除了
init
和destroy
会被调用一次外,其余钩子被调用的次数大于等于一; - 在钩子函数的实现体,尽量避免使用异步操作(包括
console
语句),因为这将导致因钩子被频繁调用而陷入死循环; - 在执行异步操作之前,要先调用
hook.enable()
方法来启用钩子,不然所执行的异步操作将不会触发相关钩子。
利用这些钩子我们可以做许多有意思的事,比如在异步调用链中实现一个类似线程局部存储的机制:
const http = require('http');
const { AsyncResource, createHook, executionAsyncId } = require('async_hooks');
const authProfiles = {};
const hook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
if (type === 'HTTP_PARSER') {
if (typeof authProfiles[asyncId] === 'undefined') {
authProfiles[asyncId] = {};
}
}
},
destroy(asyncId) {
authProfiles[asyncId] = null;
}
})
hook.enable();
http.createServer((req, res) => {
const asyncResource = new AsyncResource('HTTP_PARSER');
asyncResource.runInAsyncScope((req, res) => {
const asyncId = executionAsyncId();
authProfiles[asyncId].key = Date.now();
// 其它异步操作....
res.end();
}, null, req, res);
}).listen(3000);
复制代码
上例中:
- 通过
init
和destroy
钩子实现了在异步资源初始化时对相关authProfiles
信息进行初始化,并且在异步资源销毁时移除相关authProfiles
信息; - 在处理用户请求的整个异步调用链中(即
asyncResource.runInAsyncScope
回调内的异步调用链),均可通过变量asyncId
的值操作相关authProfiles
信息。
AsyncLocalStorage
上文我们通过异步钩子实现了类似线程局部存储的机制,用以在某一异步资源上下文中的所有异步操作均能对某一数据进行操作。可能是类似需求的使用频度很高,所以 Node.js 提供了更方便的 AsyncLocalStorage
,接下来我们就用 AsyncLocalStorage
来改造上文 authProfiles
设置的例子:
const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
http.createServer((req, res) => {
asyncLocalStorage.run({
key: Date.now(),
}, (req, res) => {
// 其它异步操作....
res.end();
}, req, res)
}).listen(3000);
复制代码
上例中,我们无需通过钩子来管理请求链路中所共享数据的生命周期,而是交给 AsyncLocalStorage
简单、高效地进行管理。
AsyncLocalStorage
的主要接口如下:
-
run
:创建一个独立的上下文并运行指定的函数,在指定函数中的所有操作均可通过getStore
获得共享数据,但函数外的操作无法获得共享数据;该方法的参数如下:store
:共享数据,可以为任意类型;callback
:要执行的回调函数;...args
:传递给回调函数的参数列表。
-
enterWith
:调用该方法后,之后所有操作均可通过getStore
获得共享数据;该方法的参数如下:store
:共享数据,可以为任意类型。
这里需要注意的是,如果在异步操作中调用了
enterWith
,其影响范围仅限于该异步操作内部,比如下面的例子:const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); asyncLocalStorage.enterWith(1); console.log(asyncLocalStorage.getStore()); // 输出 1 new Promise((resolve) => resolve(true)).then((value) => { asyncLocalStorage.enterWith(value); console.log(asyncLocalStorage.getStore()); // 输出 true setTimeout(() => { console.log(asyncLocalStorage.getStore()); // 输出 true }, 1000); }); setTimeout(() => { console.log(asyncLocalStorage.getStore()); // 输出 1 }, 1000); 复制代码
-
exit
:在指定函数中的所有操作调用getStore
无法获得外部上下文中设置的共享数据,比如下例:const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); asyncLocalStorage.enterWith(1); console.log(asyncLocalStorage.getStore()); // 输出 1 asyncLocalStorage.exit(() => { console.log(asyncLocalStorage.getStore()); // 输出 undefined }); 复制代码
该方法的参数如下:
callback
:要执行的回调函数;...args
:传递给回调函数的参数列表。
-
disable
:调用该方法后,后续调用getStore
将返回undefined
; -
getStore
:获取当前上下文中的共享数据;
总结
本文对 Node.js 中异步操作的高阶内容异步资源(AsyncResource)、 异步钩子(AsyncHooks)、AsyncLocalStorage 进行了介绍,通过这些工具,我们可以对应用中的整个调用链(比如一个 HTTP 请求从接收到响应的整个过程)进行追踪,并以此减少应用故障定位所花费的时间与精力。最后,本文若有纰漏之处,还望大家批评指正,最后祝大家快乐编码每一天。
参考链接
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。