新聞中心
在 Node.js 中,如何更優(yōu)雅地獲取請求上下文一直是一個問題,看一下下面的例子。

背景
const http = require('http');
function handler1(req, res) {
console.log(req.url);
}
function handler2(req, res) {
console.log(req.url);
}
http.createServer((req, res) => {
handler1(req, res);
handler2(req, res);
res.end();
}).listen();
上面的例子中,每次收到一個請求時都會執(zhí)行 handler1 和 handler2,為了在不同的地方里都能拿到請求上下文,我們只能逐級進行傳遞,如果業(yè)務邏輯很復雜,這個維護性是非常差的,下接下來看看如何使用 AsyncLocalStorage 解決這個問題。
AsyncLocalStorage
AsyncLocalStorage 是基于 Async Hooks 實現(xiàn)的,它通過上下文傳遞實現(xiàn)了異步代碼的上下文共享和隔離。下面看一個例子。
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function logWithId(msg) {
const id = asyncLocalStorage.getStore();
console.log(`${id !== undefined ? id : '-'}:`, msg);
}
asyncLocalStorage.run(1, () => {
logWithId('start');
setImmediate(() => {
logWithId('finish');
});
});
上面的代碼會輸出
1: start
1: finish
從中可以看到兩個 logWithId 共享了同一個上下文,這個上下文是由 run 函數(shù)設(shè)置的 1,那這種技術(shù)如何解決我們剛開始提出的問題呢?看一下下面的例子。
const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
function handler1() {
const { req } = asyncLocalStorage.getStore();
console.log(req.url);
}
function handler2() {
setImmediate(() => {
const { req } = asyncLocalStorage.getStore();
console.log(req.url);
});
}
http.createServer((req, res) => {
asyncLocalStorage.run({ req, res }, () => {
handler1();
handler2();
});
res.end();
}).listen(9999, () => {
http.get({ port: 9999, path: '/test' })
});
執(zhí)行上面代碼輸出如下。
/test
/test
可以看到,我們不需要逐級地傳遞請求上下文并且可以在任意異步代碼中獲取請求上下文。這讓代碼的編寫和維護帶來了非常大的好處,不過缺點就是,因為 AsyncLocalStorage 是基于 Async hooks 的,所以會帶來一些性能損耗,不同的版本可能不一樣,但是 Node.js 也在不斷地優(yōu)化其性能,我印象中,社區(qū)有人提過使用其他技術(shù)實現(xiàn) AsyncLocalStorage。
AsyncLocalStorage 原理
知其然知其所以然,只知道怎么使用是不夠的,理解其原理可以幫助我們更好地使用它。下面來分析一下 AsyncLocalStorage 的原理。先看一下創(chuàng)建 AsyncLocalStorage 的邏輯
class AsyncLocalStorage {
constructor() {
this.kResourceStore = Symbol('kResourceStore');
this.enabled = false;
}
}
創(chuàng)建AsyncLocalStorage的時候很簡單,主要是置狀態(tài)為false,并且設(shè)置kResourceStore的值為Symbol('kResourceStore')。設(shè)置為Symbol('kResourceStore')而不是 'kResourceStore' 很重要,我們后面會看到。繼續(xù)看一下執(zhí)行AsyncLocalStorage.run的邏輯。
const storageList = [];
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
// 傳遞上下文
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource, type);
}
},
});
run(store, callback, ...args) {
// 把當前 AsyncLocalStorage 加入隊列
ArrayPrototypePush(storageList, this);
// 啟動 AsyncHooks
storageHook.enable();
// 獲取當前的異步資源,比如收到的請求
const resource = executionAsyncResource();
// 記錄舊的上下文
const oldStore = resource[this.kResourceStore];
// 修改當前異步資源的上下文
resource[this.kResourceStore] = store;
// 在新的上下文中執(zhí)行傳入的回調(diào)函數(shù)
try {
return ReflectApply(callback, null, args);
} finally {
resource[this.kResourceStore] = oldStore;
}
}
回調(diào)函數(shù)里可以通過 asyncLocalStorage.getStore() 獲得設(shè)置的公共上下文。
getStore() {
const resource = executionAsyncResource();
return resource[this.kResourceStore];
}
getStore的原理很簡單,首先拿到當前的異步資源,然后根據(jù)AsyncLocalStorage的kResourceStore的值從resource中拿到公共上下文,如果是同步執(zhí)行g(shù)etStore(比如 handler1 中),那么executionAsyncResource返回的就是我們請求所對應的異步資源,上下文就是在run時設(shè)置的上下文({req, res}),但是如果是異步getStore那么怎么辦呢?因為這時候executionAsyncResource返回的不再是請求所對應的異步資源,也就拿不到他掛載的公共上下文。為了解決這個問題,Node.js對公共上下文進行了傳遞。
const storageList = []; // AsyncLocalStorage對象數(shù)組
const storageHook = createHook({
init(asyncId, type, triggerAsyncId, resource) {
const currentResource = executionAsyncResource();
for (let i = 0; i < storageList.length; ++i) {
storageList[i]._propagate(resource, currentResource);
}
}
});
_propagate(resource, triggerResource) {
const store = triggerResource[this.kResourceStore];
resource[this.kResourceStore] = store;
}
我們看到在每次資源創(chuàng)建的時候,Node.js會把當前異步資源的上下文掛載到新創(chuàng)建的異步資源中。所以在asyncLocalStorage.getStore() 時即使不是我們在執(zhí)行run時創(chuàng)建的資源對象,也可以獲得具體asyncLocalStorage對象所設(shè)置的資源( handler2 中)。關(guān)系圖如下。
這樣就實現(xiàn)了異步資源上下文的共享和隔離。
總結(jié)
AsyncLocalStorage 有很多用法和用處,我們在 Node.js APM 中也大量用到該技術(shù),通過 AsyncLocalStorage,我們可以無侵入地實現(xiàn)對 Node.js 應用的內(nèi)部進行觀測,時間關(guān)系,本文簡單地介紹了 AsyncLocalStorage 的使用和原理,有興趣的同學可以自行探索。
文章標題:穿針引線之AsyncLocalStorage
URL鏈接:http://www.5511xx.com/article/cdhhiep.html


咨詢
建站咨詢
