文章目录
- 跟踪异步回调的原因
- 异步调用链基础
- async_hook
- AsyncResource
- asyncResource.runInAsyncScope
跟踪异步回调的原因
- node 基于事件循环的异步非阻塞 I/O 模型,发起一次异步调用,回调在之后的循环中才被调用,此时已经无法追踪到是谁发起了这个异步调用
- 比如下面谁的 callback 先调用、以及 callback 属实谁的回调?无法从日志中确认调用链
const fs = require('fs')function callback(err, data) {console.log('callback', data)
}// 无改文件
fs.readFile("a.txt", callback)
console.log('after a')
// 无改文件
fs.readFile("b.txt", callback)
console.log('after b')
// after a
// after b
// callback undefined
// callback undefined
异步调用链基础
- async scope:每一个函数(不论异步还是同步)都会提供一个上下文, 异步称之为 async scope
- asyncId:每一个 async scope 中都有一个 asyncId ,它是当前 async scope 的标志,同一个的 async scope 中 asyncId 必然相同,每个异步资源在创建时, asyncId 自动递增,全局唯一
- 同一个 async scope 可能会被调用及执行多次,不管执行多少次,其 asyncId 必然相同,通过监听函数,我们很方便追踪其执行的次数、时间以及上下文关系
- triggerAsyncId:每一个 async scope 中都有一个 triggerAsyncId ,用来表示当前函数是由哪个 async scope 触发生成的
- 通过 asyncId 和 triggerAsyncId 就可以追踪整个异步的调用关系及链路,这个是全链路追踪的核心
async_hook
文档
- async_hook 提供了一系列钩子用于监听当前执行环境中的所有类型异步回调
- type:触发异步回调的类型,自定义类型通过 AsyncResource 创建
FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject
示例:
// 监听 fs.readFile 异步回调const fs = require('fs')
const async_hooks = require('async_hooks');
const { fd } = process.stdout;let indent = 0;
async_hooks.createHook({init(asyncId, type, triggerAsyncId) {const eid = async_hooks.executionAsyncId();const indentStr = ' '.repeat(indent);fs.writeSync(fd,`${indentStr}${type}(${asyncId}):` +` trigger: ${triggerAsyncId} execution: ${eid} \n`);},before(asyncId) {const indentStr = ' '.repeat(indent);fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`);indent += 2;},after(asyncId) {indent -= 2;const indentStr = ' '.repeat(indent);fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`);},destroy(asyncId) {const indentStr = ' '.repeat(indent);fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`);},
}).enable();function callback(err, data) {console.log('callback', data)
}fs.readFile("a.txt", callback)
console.log('after a')
fs.readFile("b.txt", callback)
console.log('after b')
// 打印结果
FSREQCALLBACK(4): trigger: 1 execution: 1 # a
after a
TickObject(5): trigger: 1 execution: 1
FSREQCALLBACK(6): trigger: 1 execution: 1 # b
after b
before: 5
after: 5
before: 4
callback undefinedTickObject(7): trigger: 4 execution: 4 # trigger by a
after: 4
before: 7
after: 7
before: 6
callback undefinedTickObject(8): trigger: 6 execution: 6 # trigger by b
after: 6
before: 8
after: 8
destroy: 5
destroy: 7
destroy: 4
destroy: 8
destroy: 6
AsyncResource
文档
用于监听自定义异步回调,自定义时一般通过继承 AsyncResource 实现自己的逻辑
-
asyncResource.runInAsyncScope
- 当配置好 async_hook后 ,new AsyncResource 会触发 init 钩子
- 通过 runInAsyncScope(fn) 执行异步回调会触发 before-> fn -> after 钩子
- 通过在 runInAsyncScope 方法中包裹要传入的异步调用。可以保证这个资源( fn )的异步行为是可追踪的
追踪自定义异步回调示例:
const async_hooks = require("async_hooks");const fs = require("fs");
const { fd } = process.stdout;let indent = 0;// 配置好 hook
async_hooks.createHook({init(asyncId, type, triggerAsyncId) {const eid = async_hooks.executionAsyncId();const indentStr = " ".repeat(indent);fs.writeSync(fd,`${indentStr}${type}(${asyncId}):` +` trigger: ${triggerAsyncId} execution: ${eid} \n`);},before(asyncId) {const indentStr = " ".repeat(indent);fs.writeSync(fd, `${indentStr}before: ${asyncId}\n`);indent += 2;},after(asyncId) {indent -= 2;const indentStr = " ".repeat(indent);fs.writeSync(fd, `${indentStr}after: ${asyncId}\n`);},destroy(asyncId) {const indentStr = " ".repeat(indent);fs.writeSync(fd, `${indentStr}destroy: ${asyncId}\n`);},}).enable();function callback(err, data) {console.log("callback", data);
}// 继承 AsyncResource 实现自定义逻辑
class MyResource extends async_hooks.AsyncResource {constructor() {super("my-resource");}close() {this.emitDestroy();}
}function p() {return new Promise((resolve) => {setTimeout(() => {resolve();}, 1000);});
}let resource = new MyResource();
// 追踪自定义异步回调
resource.runInAsyncScope(async () => {console.log("hello");await p();
});resource.close();
// 打印结果
my-resource(4): trigger: 1 execution: 1
before: 4PROMISE(5): trigger: 4 execution: 4
helloTickObject(6): trigger: 4 execution: 4 PROMISE(7): trigger: 4 execution: 4 Timeout(8): trigger: 4 execution: 4 PROMISE(9): trigger: 7 execution: 4
after: 4
before: 6
after: 6
destroy: 4
destroy: 6
before: 8
after: 8
before: 9
after: 9
destroy: 8