告别请求追踪噩梦:NestJS 集成 AsyncLocalStorage,打造跨框架复用模块
什么是 AsyncLocalStorage?
AsyncLocalStorage 的核心 API
为什么要在 NestJS 中使用 AsyncLocalStorage?
如何在 NestJS 中集成 AsyncLocalStorage?
1. 安装必要的依赖
2. 创建一个 AsyncLocalStorage 实例
3. 创建一个 NestJS 中间件
4. 在 AppModule 中注册中间件
5. 在服务中使用 AsyncLocalStorage
6. 跨框架复用
总结
“喂,小王啊,你那个接口又报 500 了,赶紧看看日志,查查是哪个用户,干了啥操作导致的!”
“啊?张哥,我这接口一天几万次调用,日志都几百兆了,这咋查啊?大海捞针啊!”
“我不管,反正你得给我查出来!这可是影响线上业务的!”
相信很多后端开发的小伙伴都遇到过类似的场景。在微服务架构下,一个请求往往会跨越多个服务,每个服务又可能有多个实例,想要追踪一个请求的完整链路,简直比登天还难。传统的日志记录方式,只能记录每个服务内部的操作,无法将整个请求链路串联起来。一旦出现问题,排查起来就非常痛苦,往往需要花费大量的时间和精力。
别慌!今天就给大家介绍一个神器——AsyncLocalStorage
,它可以帮助你轻松实现请求追踪,告别大海捞针式的排查方式!
什么是 AsyncLocalStorage?
AsyncLocalStorage
是 Node.js 内置的一个模块(Node.js v12.17.0+ 或 v13.10.0+),它可以让你在异步操作中存储和访问特定于请求的上下文数据。简单来说,就是给每个请求创建一个“专属存储空间”,在这个空间里你可以存放任何与该请求相关的数据,例如用户 ID、请求 ID、trace ID 等等。无论这个请求经过多少个异步函数,你都可以随时随地访问这些数据。
AsyncLocalStorage 的核心 API
new AsyncLocalStorage()
:创建一个新的AsyncLocalStorage
实例。run(store, callback, ...args)
:运行一个回调函数,并将store
作为该请求的上下文数据。callback
中的所有异步操作都可以访问store
。getStore()
:在callback
内部获取当前请求的上下文数据(即store
)。enterWith(store)
: 类似于run
,但不执行回调,只是设置上下文。exit(callback)
: 退出当前的异步上下文.
为什么要在 NestJS 中使用 AsyncLocalStorage?
NestJS 是一个流行的 Node.js 框架,它提供了很多强大的功能,例如依赖注入、模块化、中间件等等。但是,NestJS 本身并没有提供请求追踪的功能。如果我们想在 NestJS 中实现请求追踪,就需要自己手动处理请求上下文数据的传递。这不仅繁琐,而且容易出错。
AsyncLocalStorage
的出现,完美地解决了这个问题。它可以与 NestJS 无缝集成,让我们能够轻松地实现请求追踪,而无需编写大量的 boilerplate 代码。
如何在 NestJS 中集成 AsyncLocalStorage?
1. 安装必要的依赖
由于AsyncLocalStorage
是Node.js内置模块, 无需额外安装。
2. 创建一个 AsyncLocalStorage 实例
// src/common/async-local-storage.ts import { AsyncLocalStorage } from 'async_hooks'; export const requestContext = new AsyncLocalStorage<Map<string, any>>();
这里我们创建了一个 AsyncLocalStorage
实例,并将其命名为 requestContext
。这个实例将用于存储所有请求的上下文数据。store
的类型设置为Map<string, any>
, 方便存储多种类型的数据。
3. 创建一个 NestJS 中间件
// src/common/request-context.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { requestContext } from './async-local-storage'; @Injectable() export class RequestContextMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const store = new Map(); store.set('requestId', req.headers['x-request-id'] || Math.random().toString(36).slice(2)); // 假设请求头中有 x-request-id requestContext.run(store, () => { next(); }); } }
这个中间件会在每个请求进入时,创建一个新的 Map
对象作为该请求的上下文数据,并将其存储到 requestContext
中。这里我们假设请求头中有一个 x-request-id
字段,用于标识请求。如果没有这个字段,我们就生成一个随机的请求 ID。然后,我们调用 requestContext.run()
方法,将 store
和 next()
函数作为参数传入。next()
函数是 Express 框架中的一个函数,用于将请求传递给下一个中间件或路由处理函数。
4. 在 AppModule 中注册中间件
// src/app.module.ts import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { RequestContextMiddleware } from './common/request-context.middleware'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(RequestContextMiddleware).forRoutes('*'); } }
我们在 AppModule
中注册了 RequestContextMiddleware
,并将其应用于所有路由 (forRoutes('*')
)。这样,每个请求都会经过这个中间件,从而实现请求上下文数据的存储和传递。
5. 在服务中使用 AsyncLocalStorage
// src/app.service.ts import { Injectable } from '@nestjs/common'; import { requestContext } from './common/async-local-storage'; @Injectable() export class AppService { getHello(): string { const store = requestContext.getStore(); const requestId = store.get('requestId'); console.log(`[AppService] Request ID: ${requestId}`); return 'Hello World!'; } async someAsyncOperation(): Promise<string> { const store = requestContext.getStore(); const requestId = store ? store.get('requestId') : 'undefined'; console.log(`[AppService - Async] Request ID: ${requestId}`); return new Promise((resolve) => { setTimeout(() => { const store = requestContext.getStore(); const requestId = store ? store.get('requestId') : 'undefined'; console.log(`[AppService - Async - Timeout] Request ID: ${requestId}`); resolve('Async operation complete!'); }, 1000); }); } }
在服务中,我们可以通过 requestContext.getStore()
方法获取当前请求的上下文数据。然后,我们可以从中获取请求 ID,并将其用于日志记录、错误追踪等操作。注意,即使在异步函数中(如someAsyncOperation
中的setTimeout
),getStore()
也能正确获取到当前请求的上下文。
6. 跨框架复用
AsyncLocalStorage
的强大之处在于,它不仅可以在 NestJS 中使用,还可以在其他 Node.js 框架中使用,例如 Express、Koa 等等。这意味着,你可以编写一个通用的请求追踪模块,然后在不同的框架中使用它,从而实现代码的复用。
例如,假设你有一个 Express 应用,你可以这样使用 requestContext
:
// express-app.ts import express from 'express'; import { requestContext } from './src/common/async-local-storage'; // 假设这是你的 AsyncLocalStorage 实例 const app = express(); app.use((req, res, next) => { const store = new Map(); store.set('requestId', req.headers['x-request-id'] || Math.random().toString(36).slice(2)); requestContext.run(store, () => { next(); }); }); app.get('/', (req, res) => { const store = requestContext.getStore(); const requestId = store.get('requestId'); console.log(`[Express App] Request ID: ${requestId}`); res.send('Hello from Express!'); }); app.listen(3001, () => { console.log('Express app listening on port 3001'); });
可以看到,在 Express 应用中使用 AsyncLocalStorage
的方式与在 NestJS 中几乎完全相同。你只需要在请求进入时,创建一个新的 Map
对象作为该请求的上下文数据,并将其存储到 requestContext
中即可。
总结
AsyncLocalStorage
是一个非常强大的工具,它可以帮助你轻松实现请求追踪,告别大海捞针式的排查方式。通过与 NestJS 等框架的集成,你可以编写出更加健壮、可维护的应用程序。同时,AsyncLocalStorage
的跨框架复用特性,也能够大大提高你的开发效率。
希望本文能够帮助你更好地理解和使用 AsyncLocalStorage
。如果你有任何问题或建议,欢迎留言讨论!
一些进阶用法和思考:
- 错误处理: 在
run
的回调函数中发生的未捕获异常, 会导致应用崩溃。 可以考虑使用try...catch
或者domain
模块进行错误处理。 - 更复杂的存储结构:
Map
只是一个简单的示例,你可以根据实际需求使用更复杂的存储结构,例如类实例或者嵌套的对象。 - 与其他库集成:
AsyncLocalStorage
可以与许多第三方库集成,例如日志库 (如 Winston、Pino)、APM 工具 (如 New Relic、Datadog) 等,以实现更强大的功能。 - 性能考量: 虽然
AsyncLocalStorage
提供了便利性, 但是在极高并发场景下, 过度使用可能会有性能损耗. 需要根据实际情况进行压测和评估. enterWith
的使用场景: 在一些不需要立即执行回调的场景,例如在HTTP请求开始时设置上下文,然后在后续的中间件或控制器中获取,可以使用enterWith
。 之后可以使用exit
退出上下文。
通过以上方法,你可以将 AsyncLocalStorage
应用到更广泛的场景,构建出更强大的应用。