Async Hook 的出现简单来说有两个目的,一是提供了一个处理异步任务机制的抽象;二是暴露了方便追踪 handle objects 生命周期的 Hook。本文主要从以下几个方面来讨论:
- Hook 的起因
- Overview
- Handle Objects
- 一些意外
Hook 的起因
Node.js 是因异步的特性而流行,然而异步的特性本身却有一定的缺陷。从笔者的角度看来简单来说有三个:1、思路差异;2、复杂的场景控制困难;3、难以调试/监控。其中的 1、2 在这几年层出不穷的轮子以及最终 async/await 特性的发布下已经日趋稳定。而第三个问题则是 Node.js 最近两个大版本中努力的方向。Async Hook 的出现在笔者看来一定程度上完善了异步的监控机制。
早期 domain 出现的时候就有同学想要按照 domain 的方式设计一个监听异步行为的 hook (参见 issue: implement domain-like hooks (asynclistener) for userland),基于这个出现了一个叫做 async-listener 的模块。该模块在 process 对象上 pollyfill 了几个监控异步行为的接口。而 8.0 中出现的 Async Hook 则可以说是 Node.js 对 async-listener 的官方支持。
const async_hooks = require('async_hooks');
setTimeout(() => {
console.log('first setTimeout id', async_hooks.currentId()); // 2
});
setTimeout(() => {
console.log('second setTimeout id', async_hooks.currentId()); // 4
});
Async hook 对每一个函数(不论异步还是同步)提供了一个 Async scope,你可以通过 async_hooks.currentId() 获取当前函数的 Async ID。
const async_hooks = require('async_hooks');
console.log('default Async Id', async_hooks.currentId()); // 1
process.nextTick(() => {
console.log('nextTick Async Id', async_hooks.currentId()); // 5
test();
});
function test () {
console.log('nextTick Async Id', async_hooks.currentId()); // 5
}
在同一个 Async scope 中,你会拿到相同的 Async ID。
Overview
const async_hooks = require('async_hooks');
// 获取当前执行上下文的异步的 Async ID
const cid = async_hooks.currentId();
// 获取调用当前异步的异步的 Async ID
const tid = async_hooks.triggerId();
// 创建一个新的 AsyncHook 实例. 所有回调都是可选项
const asyncHook = async_hooks.createHook({ init, before, after, destroy });
// 允许该实例中异步函数启用 hook 不会自动生效需要手动启用。
asyncHook.enable();
// 关闭监听异步事件
asyncHook.disable();
//
// 以下是可以监控的几个事件的 callback
//
// 对象构造时会触发 init 事件(此时资源可能还没初始化完 )
// 因此 asyncId 引用的资源 (resource) 的所有字段都可能还未填充
function init(asyncId, type, triggerId, resource) { }
// before is called just before the resource's callback is called. It can be
// called 0-N times for handles (e.g. TCPWrap), and will be called exactly 1
// time for requests (e.g. FSReqWrap).
function before(asyncId) { }
// after is called just after the resource's callback has finished.
function after(asyncId) { }
// destroy is called when an AsyncWrap instance is destroyed.
function destroy(asyncId) { }
Handle Objects
Async Hooks 可以触发事件来通知我们关于 handle object 的变化。所以要了解 Async Hook 同时也需要了解 handle objects。
Node.js 的核心 API 大部分是用 JavaScript 定义的,然而 ECMAScript 标准却并没有规范 JavaScript 要如何使用 TCP socket、读取文件等等的一些列操作。这些操作是通过 C++ 调用 libuv 和 V8 来实现的,而 node 内部与这些 C++ 层交互的对象被称之为 handle objects。
8.0 之后使用 Async hook 可以对 handle object 的生命周期进行追踪:
const fs = require('fs');
const async_hooks = require('async_hooks');
let indent = 0;
async_hooks.createHook({
init(asyncId, type, triggerId) {
const cId = async_hooks.currentId();
print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`);
},
before(asyncId) {
print(`${getIndent(indent)}before: ${asyncId}`);
indent += 2;
},
after(asyncId) {
indent -= 2;
print(`${getIndent(indent)}after: ${asyncId}`);
},
destroy(asyncId) {
print(`${getIndent(indent)}destroy: ${asyncId}`);
},
}).enable();
let server = require('net').createServer((sock) => {
sock.end('hello world\n');
server.close();
});
server.listen(8080, () => print('server started'));
function print(str) {
fs.writeSync(1, str + '\n');
}
function getIndent(n) {
return ' '.repeat(n);
}
使用 nc localhost 8080 来调用。可以看到 server 端的输出:
TCPWRAP(2): trigger: 1 scope: 1
TickObject(3): trigger: 2 scope: 1
before: 3
server started
after: 3
destroy: 3
TCPWRAP(4): trigger: 2 scope: 0
before: 2
TickObject(5): trigger: 2 scope: 2
after: 2
before: 5
SHUTDOWNWRAP(6): trigger: 4 scope: 5
after: 5
destroy: 5
destroy: 2
before: 6
after: 6
destroy: 6
before: 4
TTYWRAP(7): trigger: 4 scope: 4
SIGNALWRAP(8): trigger: 4 scope: 4
TickObject(9): trigger: 4 scope: 4
TickObject(10): trigger: 4 scope: 4
after: 4
before: 9
after: 9
before: 10
after: 10
before: 4
after: 4
destroy: 9
destroy: 10
destroy: 4
其中形如 TCPWRAP 的描述则是 handle object,而 scope 即当前异步操作的 Async ID,如果在当前异步中又触发了异步,那么新的异步的 Trigger ID 即当前的 Async ID。当某个异步即将被执行 before hook 会被触发,当某个异步执行完成 after hook 会被触发,当异步流程结束则会触发 destroy hook。
在 Node.js 中一些如 socket 之类的 handle objects 原本并不能准确的确认其释放,而现在则可以通过 destroy hook 来确认其释放了。
一些意外
虽然看起来是有前景,然而这个特性还处于试验中。可能会出现意外或者一些神奇的情况。
比如在 Async Hook 监控的异步中,console.log 也是基于异步实现的,所以如果在 Hook 中使用 console.log 来打印信息就会出现死循环。只能使用非常原始的方式在 Hook 中输出信息,即类似上文中大家看到的直接 fs.writeSync 的方式:
const fs = require('fs');
const util = require('util');
function print(...args) {
// use a function like this one when debugging inside an AsyncHooks callback
fs.writeSync(1, `${util.format(...args)}\n`);
}
Async Hook 是对目前的异步进行 Hook,如果 Hook 中的代码如果出现异常, node 的进程会打出 stack trace 然后退出进程。所以不建议在这些 hook 中加入逻辑,如果加入 try/catch 可能会增加正常代码的维护成本,各位也最好谨慎使用这个试验性质的黑魔法。
最后附上一些引用链接: