WEBKT

告别请求追踪噩梦:NestJS 集成 AsyncLocalStorage,打造跨框架复用模块

68 0 0 0

什么是 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() 方法,将 storenext() 函数作为参数传入。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 应用到更广泛的场景,构建出更强大的应用。

全栈老铁 NestJSAsyncLocalStorage请求追踪

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7895