Node.js 20 中 AsyncLocalStorage 的用法与异步上下文跟踪
AsyncLocalStorage 是 Node.js 内置的异步上下文跟踪机制,在 20 版本中已经稳定。它的核心作用是让不同异步操作之间共享同一个上下文对象,比如在 HTTP 请求处理中传递请求 ID 或用户身份。下面我会从基本用法、判断方式、操作建议以及常见问题这几个方面展开,尽量贴近实际排查场景。
基本用法:从 run() 开始
在 Node.js 20 中使用 AsyncLocalStorage 非常简单,首先从 'async_hooks' 模块引入该类。创建一个 AsyncLocalStorage 实例后,通过 run() 方法传入一个上下文对象和一个回调函数,在回调函数内及其所有异步操作中,都可以通过 getStore() 获取该上下文。需要注意的是,run() 的回调必须是同步函数或者返回 Promise,否则上下文可能无法正确传播。
这段描述已经概括了核心用法。实际操作时,我通常这样写:
const { AsyncLocalStorage } = require('async_hooks');
const als = new AsyncLocalStorage();
als.run({ requestId: '123' }, () => {
// 这里以及所有异步回调里都能取到上下文
console.log(als.getStore()); // { requestId: '123' }
});关键是 run() 的回调必须返回 Promise 或同步执行,否则后续异步步骤可能丢失上下文。比如回调里直接用 setTimeout 但没返回 Promise,就会导致 setTimeout 内的 getStore() 返回 undefined。
判断上下文是否生效
验证 AsyncLocalStorage 是否生效,可以在不同异步阶段调用 getStore() 并检查返回值。例如,在一个 setTimeout 或 Promise.then 中调用 getStore(),如果返回之前设置的上下文对象,则说明跟踪成功。如果返回 undefined,常见原因是未在 run() 的作用域内调用,或者 run() 的回调中创建了新的异步上下文但没有嵌套。
我排查时会在关键异步操作前后加日志:
console.log('Before async:', als.getStore());
setTimeout(() => {
console.log('Inside async:', als.getStore());
}, 10);如果第二个日志是 undefined,就说明上下文没传过来。这时检查两点:一是 run() 的回调是否同步或返回 Promise;二是 setTimeout 是不是在 run() 的作用域内调用的。如果回调里用了 async 函数,要确保 run() 返回的是那个 async 函数的 Promise。
操作建议:为每个请求独立上下文
当需要为每个 HTTP 请求传递独立上下文时,建议在中间件的最外层调用 AsyncLocalStorage.run(),将请求 ID 或用户信息存入存储中。注意不要将 AsyncLocalStorage 实例用作全局单例之外的用途,每个作用域应复用同一个实例,否则会导致上下文丢失。避免在多个 run() 调用之间共享可变对象,以免造成状态污染。
以 Express 为例,我会在第一个中间件里创建 run:
app.use((req, res, next) => {
als.run({ requestId: req.headers['x-request-id'] || uuid() }, () => {
next();
});
});这样后续所有的中间件、路由处理、数据库调用只要在同一个回调链里,都能拿到 requestId。但注意不要在多个 run() 之间共享同一个对象,比如把 { requestId: ... } 对象引用传递到下一个 run(),那样会导致状态污染。建议每次都创建新的上下文对象。
风险边界:跨进程与事件监听器
AsyncLocalStorage 不能自动跟踪跨进程的异步操作,比如 Worker 线程或子进程。如果需要在 Worker 线程中传递上下文,必须手动通过 postMessage 传递数据。另外,对于未被 Promise 或 async/await 包装的回调(如事件监听器),上下文可能不会按预期传播,应优先使用 Promise 或 async 函数。
我做过一次尝试:在 Worker 线程里调用 getStore(),结果始终是 undefined。解决办法是在主线程把上下文传给 Worker:
const worker = new Worker('./worker.js');
worker.postMessage({ context: als.getStore() });然后在 worker 里手动保存。对于事件监听器,比如 process.on('unhandledRejection'),如果回调不是 async 函数,上下文可能丢失。所以尽量把事件监听改成 async 或者用 Promise 包裹。
常见坑:run() 外部调用 getStore() 与忘记返回 Promise
一个常见的错误是在 run() 外部调用 getStore() 获取到 undefined 后认为 AsyncLocalStorage 未生效,实际上是因为调用位置不在任何 run() 的作用域内。另一个坑是忘记给 run() 的回调返回 Promise,导致异步操作中的上下文丢失。此外,如果多个异步操作并发执行,应确保每个操作都在独立的 run() 中运行,否则它们会共享同一上下文。
举个例子,一个并发请求处理中有多个异步任务,如果所有任务都放在同一个 run() 里,它们会共享同一个存储对象,可能互相覆盖。正确的做法是对每个请求单独 run(),然后用 Promise.all 等并发控制。
排查上下文丢失的方法
要排查异步上下文的丢失问题,可以在关键异步操作前后添加日志,打印 getStore() 的结果。如果发现某个异步步骤中上下文变为 undefined,可以检查该步骤是否位于 run() 的异步回调链中。还可以利用 async_hooks 提供的 createHook 监听 init 和 destroy 事件,手动跟踪异步资源的创建和销毁,以定位上下文传播断裂的位置。
createHook 的方法比较底层,但可以作为最后手段。我通常先用日志定位到具体异步阶段,如果确认是 run() 回调没返回 Promise,就改 async/await。对于 Promise.all 之类的场景,确保每个任务都用 async 函数包裹。
简而言之,AsyncLocalStorage 在 Node.js 20 中已经足够稳定,只要遵循“每个请求单独 run()”、“回调返回 Promise”、“避免跨进程”这几个原则,就能完美跟踪异步上下文。