有趣的 Async hooks 模塊
在 Node.js 中,Async hooks 是一個(gè)非常有意思且強(qiáng)大的模塊(雖然性能上存在一些問題),在 APM 中,我們可以借助這個(gè)模塊做很多事情。本文介紹兩個(gè)有趣的用法。
AsyncLocalStorage
在 Node.js 中,上下文傳遞一直是一個(gè)非常困難的問題,Node.js 通過 AsyncLocalStorage 提供了一種解決方案,今天看到一個(gè)庫中實(shí)現(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();
}
}
這個(gè)方式是基于 Async hooks 實(shí)現(xiàn)的,原理是在 init 鉤子中獲取當(dāng)前的上下文,然后把當(dāng)前的上下文傳遞到當(dāng)前創(chuàng)建的異步資源的,接著在執(zhí)行異步資源回調(diào)前,Node.js 會(huì)執(zhí)行 before 鉤子,before 鉤子中會(huì)把當(dāng)前異步資源(正在執(zhí)行回調(diào)的這個(gè)資源)的上下文壓入棧中,然后在回調(diào)里就可以通過 context 函數(shù)獲取到當(dāng)前的上下文,實(shí)際上獲取的就是剛才壓入棧中的內(nèi)容,執(zhí)行完回調(diào)后再出棧。前面介紹了其工作原理,主要是實(shí)現(xiàn)異步資源的上下文傳遞且在執(zhí)行回調(diào)時(shí)通過棧的方式實(shí)現(xiàn)了上下文的管理,那么第一個(gè)上下文是如何來的呢?答案是通過 run 函數(shù)(Node.js 中還可以通過 enterWith),run 會(huì)把用戶設(shè)置的上下文壓入棧中,然后執(zhí)行了一個(gè)用戶傳入的函數(shù),如果這個(gè)函數(shù)中創(chuàng)建了異步資源,那么用戶傳入的上下文就會(huì)傳遞到這個(gè)新創(chuàng)建的異步資源中,后面執(zhí)行這個(gè)異步資源的回調(diào)時(shí),就可以拿到對(duì)應(yīng)的上下文了。接著看一下使用效果。
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í)行上面代碼會(huì)輸出 /。可以看到在 setImmediate 的回調(diào)中(setImmediate 會(huì)創(chuàng)建一個(gè)異步資源)成功拿到了 run 時(shí)設(shè)置的上下文。
監(jiān)控異步回調(diào)的耗時(shí)
在 Node.js 中,代碼執(zhí)行耗時(shí)是一個(gè)非常值得關(guān)注的地方,Node.js 也提供了很多手段采集代碼執(zhí)行的耗時(shí)信息,下面介紹的是基于 Async hooks 實(shí)現(xiàn)的回調(diào)函數(shù)耗時(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++) {
}
});
實(shí)現(xiàn)原理非常簡單,主要是利用 before 和 after 鉤子實(shí)現(xiàn)了回調(diào)的耗時(shí)統(tǒng)計(jì),就不多介紹,社區(qū)中也有同學(xué)實(shí)現(xiàn)了這個(gè)能力,具體可以參考 https://github.com/naugtur/blocked-at/tree/master。