有趣的 Async hooks 模塊
在 Node.js 中,Async hooks 是一個非常有意思且強大的模塊(雖然性能上存在一些問題),在 APM 中,我們可以借助這個模塊做很多事情。本文介紹兩個有趣的用法。
AsyncLocalStorage
在 Node.js 中,上下文傳遞一直是一個非常困難的問題,Node.js 通過 AsyncLocalStorage 提供了一種解決方案,今天看到一個庫中實現(xiàn)了類似 AsyncLocalStorage 的能力,還挺有意思的。代碼如下。
class ALS {
constructor() {
this._contexts = new Map();
this._stack = [];
this.hook = createHook({
init: this._init.bind(this),
before: this._before.bind(this),
after: this._after.bind(this),
destroy: this._destroy.bind(this),
});
}
context() {
return this._stack[this._stack.length - 1];
}
run(context, fn, thisArg, ...args) {
this._enterContext(context);
try {
return fn.call(thisArg, ...args);
}
finally {
this._exitContext();
}
}
enable() {
this.hook.enable();
}
disable() {
this.hook.disable();
this._contexts.clear();
this._stack = [];
}
_init(asyncId) {
const context = this._stack[this._stack.length - 1];
if (context !== undefined) {
this._contexts.set(asyncId, context);
}
}
_destroy(asyncId) {
this._contexts.delete(asyncId);
}
_before(asyncId) {
const context = this._contexts.get(asyncId);
this._enterContext(context);
}
_after() {
this._exitContext();
}
_enterContext(context) {
this._stack.push(context);
}
_exitContext() {
this._stack.pop();
}
}
這個方式是基于 Async hooks 實現(xiàn)的,原理是在 init 鉤子中獲取當前的上下文,然后把當前的上下文傳遞到當前創(chuàng)建的異步資源的,接著在執(zhí)行異步資源回調前,Node.js 會執(zhí)行 before 鉤子,before 鉤子中會把當前異步資源(正在執(zhí)行回調的這個資源)的上下文壓入棧中,然后在回調里就可以通過 context 函數(shù)獲取到當前的上下文,實際上獲取的就是剛才壓入棧中的內容,執(zhí)行完回調后再出棧。前面介紹了其工作原理,主要是實現(xiàn)異步資源的上下文傳遞且在執(zhí)行回調時通過棧的方式實現(xiàn)了上下文的管理,那么第一個上下文是如何來的呢?答案是通過 run 函數(shù)(Node.js 中還可以通過 enterWith),run 會把用戶設置的上下文壓入棧中,然后執(zhí)行了一個用戶傳入的函數(shù),如果這個函數(shù)中創(chuàng)建了異步資源,那么用戶傳入的上下文就會傳遞到這個新創(chuàng)建的異步資源中,后面執(zhí)行這個異步資源的回調時,就可以拿到對應的上下文了。接著看一下使用效果。
const als = new ALS();
als.enable();
http.createServer((req, res) => {
als.run(req, () => {
setImmediate(() => {
console.log(als.context().url);
res.end();
});
})
}).listen(9999, () => {
http.get({ port: 9999, host: '127.0.0.1' });
});
執(zhí)行上面代碼會輸出 /。可以看到在 setImmediate 的回調中(setImmediate 會創(chuàng)建一個異步資源)成功拿到了 run 時設置的上下文。
監(jiān)控異步回調的耗時
在 Node.js 中,代碼執(zhí)行耗時是一個非常值得關注的地方,Node.js 也提供了很多手段采集代碼執(zhí)行的耗時信息,下面介紹的是基于 Async hooks 實現(xiàn)的回調函數(shù)耗時監(jiān)控。
const { createHook } = require('async_hooks');
const fs = require('fs');
const map = {};
createHook({
init: (asyncId) => {
map[asyncId] = { stack: new Error().stack };
},
before: (asyncId) => {
if (map[asyncId]) {
map[asyncId].start = Date.now();
}
},
after: (asyncId) => {
if (map[asyncId]) {
fs.writeFileSync(1, `callback cost: ${Date.now() - map[asyncId].start}, stack: ${map[asyncId].stack}`);
}
},
destroy: (asyncId) => {
delete map[asyncId];
}
}).enable();
setTimeout(() => {
for (let i = 0; i < 1000000000; i++) {
}
});
實現(xiàn)原理非常簡單,主要是利用 before 和 after 鉤子實現(xiàn)了回調的耗時統(tǒng)計,就不多介紹,社區(qū)中也有同學實現(xiàn)了這個能力,具體可以參考 https://github.com/naugtur/blocked-at/tree/master。