NestJS 中 AsyncLocalStorage 实现分布式追踪:实战指南与 Zipkin/Jaeger 集成
1. 为什么需要分布式追踪?
2. 核心概念:Trace ID, Span ID, Context
3. AsyncLocalStorage 闪亮登场
3.1. 为什么选择 AsyncLocalStorage?
3.2. AsyncLocalStorage 的基本用法
4. NestJS 中实现分布式追踪的步骤
4.1. 创建一个全局的 AsyncLocalStorage 实例
4.2. 创建一个中间件,用于生成和传递 Trace ID
4.3. 在 AppModule 中注册中间件
4.4. 在服务中使用 Trace ID
4.5. 在 Controller 中使用 Trace ID
4.6. 测试
5. 与 Zipkin/Jaeger 集成
5.1. 安装依赖
5.2. 创建 Zipkin 配置
5.3. 创建 Zipkin 中间件
5.4. 在 AppModule 中注册 Zipkin 中间件
5.5. 启动 Zipkin 服务
5.6. 测试
6. Jaeger 集成(类似步骤)
6.1. 安装依赖
6.2. 创建 Jaeger 配置
6.3. 创建 Jaeger 中间件
6.4. 在 AppModule 中注册 Jaeger 中间件
6.5. 启动 Jaeger Agent
6.6. 测试
7. 处理跨服务调用
7.1. 在请求中传递 Trace ID
7.2. 在其他服务中接收 Trace ID
8. 进阶技巧和注意事项
9. 总结
10. 常见问题解答
你好,作为一名后端开发者,构建分布式系统是咱们绕不开的课题。随着微服务架构的普及,跨服务调用成为常态,随之而来的问题就是:如何追踪一个请求在各个服务之间的调用链路?这就是分布式追踪要解决的问题。今天,我将带你深入了解如何在 NestJS 应用中使用 AsyncLocalStorage
实现分布式追踪,并将其与 Zipkin 或 Jaeger 等追踪系统集成。准备好了吗?Let's go!
1. 为什么需要分布式追踪?
在单体应用中,咱们可以通过日志、监控等手段来定位问题。但在分布式系统中,一个请求往往需要经过多个服务,每个服务都可能产生日志,这些日志分散在不同的机器上,想要理清请求的调用链路,定位问题就变得非常困难。
分布式追踪系统通过为每个请求生成一个唯一的 Trace ID
,并在请求在各个服务之间传递时,将 Trace ID
和其他上下文信息(比如 Span ID
)一起传递。这样,咱们就可以将分散在不同服务中的日志关联起来,形成一个完整的调用链路。
分布式追踪的好处显而易见:
- 快速定位问题: 快速确定哪个服务、哪个环节出现了问题。
- 性能分析: 分析每个服务的处理时间,找出性能瓶颈。
- 服务依赖关系: 了解服务之间的调用关系,方便系统架构的优化。
- 监控告警: 基于追踪数据,设置告警规则,及时发现异常。
2. 核心概念:Trace ID, Span ID, Context
在深入技术细节之前,咱们先来了解几个核心概念:
- Trace ID: 整个调用链路的唯一标识,一个
Trace ID
代表一个请求的完整调用链路。 - Span ID: 一个
Span
的唯一标识,一个Span
代表一个服务中的一个操作,比如一个 HTTP 请求、一个数据库查询等。Span ID
通常是递增的,用于标识Span
之间的父子关系。 - Context: 包含追踪信息(
Trace ID
,Span ID
)以及其他请求相关的上下文信息,比如用户 ID、请求头等。Context
在服务之间传递,确保追踪信息在整个调用链路中可用。
3. AsyncLocalStorage 闪亮登场
AsyncLocalStorage
是 Node.js 12.17.0 版本引入的一个实验性 API,用于在异步执行上下文中存储数据。它解决了传统 Node.js
中异步编程(比如 Promise
, async/await
)导致的上下文丢失问题。在 NestJS 中,咱们可以使用 AsyncLocalStorage
来存储和传递 Trace ID
和其他上下文信息。
3.1. 为什么选择 AsyncLocalStorage?
- 自动传递上下文:
AsyncLocalStorage
能够自动在异步操作之间传递上下文,无需手动传递。这极大地简化了代码,减少了出错的可能性。 - 与 NestJS 完美契合: NestJS 基于 Node.js,可以无缝集成
AsyncLocalStorage
。 - 性能:
AsyncLocalStorage
的性能开销相对较小。
3.2. AsyncLocalStorage 的基本用法
首先,咱们需要导入 AsyncLocalStorage
:
import { AsyncLocalStorage } from 'async_hooks'; const asyncLocalStorage = new AsyncLocalStorage();
然后,咱们可以使用 asyncLocalStorage.run(store, callback)
方法来设置上下文。store
是一个对象,用于存储上下文信息。callback
是一个函数,在这个函数中,咱们可以访问上下文信息。
asyncLocalStorage.run(new Map(), () => { // 在这里访问上下文信息 const traceId = asyncLocalStorage.getStore()?.get('traceId'); console.log('Trace ID:', traceId); });
在这个例子中,咱们创建了一个新的 Map
对象作为 store
,然后在 callback
函数中访问了 traceId
。需要注意的是,在 callback
函数中,咱们可以通过 asyncLocalStorage.getStore()
方法来获取当前上下文信息。如果没有设置上下文,asyncLocalStorage.getStore()
返回 undefined
。
4. NestJS 中实现分布式追踪的步骤
现在,咱们来一步一步地实现 NestJS 中的分布式追踪。
4.1. 创建一个全局的 AsyncLocalStorage 实例
首先,咱们创建一个全局的 AsyncLocalStorage
实例。通常,咱们可以在 main.ts
文件中创建它,确保它在整个应用中可用。
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { AsyncLocalStorage } from 'async_hooks'; export const asyncLocalStorage = new AsyncLocalStorage(); async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } bootstrap();
4.2. 创建一个中间件,用于生成和传递 Trace ID
接下来,咱们创建一个中间件,用于生成 Trace ID
,并将 Trace ID
存储到 AsyncLocalStorage
中,并将其传递给下游服务。
// trace.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { asyncLocalStorage } from './main'; // 导入 AsyncLocalStorage 实例 import { v4 as uuidv4 } from 'uuid'; // 用于生成 UUID @Injectable() export class TraceMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const traceId = req.headers['x-trace-id'] || uuidv4(); // 从请求头中获取 Trace ID,如果不存在则生成一个 res.setHeader('x-trace-id', traceId); // 将 Trace ID 设置到响应头中 asyncLocalStorage.run(new Map([['traceId', traceId]]), () => { // 将 Trace ID 存储到 AsyncLocalStorage 中 next(); // 调用下一个中间件或路由处理程序 }); } }
这个中间件做了以下几件事:
- 从请求头中获取
x-trace-id
,如果不存在,则生成一个 UUID 作为Trace ID
。 - 将
Trace ID
设置到响应头中,以便下游服务可以获取。 - 使用
asyncLocalStorage.run()
方法,将Trace ID
存储到AsyncLocalStorage
中。 - 调用
next()
函数,将请求传递给下一个中间件或路由处理程序。
4.3. 在 AppModule 中注册中间件
// app.module.ts import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TraceMiddleware } from './trace.middleware'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(TraceMiddleware).forRoutes('*'); // 将中间件应用于所有路由 } }
在这里,咱们将 TraceMiddleware
应用于所有路由。这意味着,每个请求都会经过这个中间件。
4.4. 在服务中使用 Trace ID
现在,咱们可以在服务中使用 Trace ID
了。比如,咱们可以在日志中输出 Trace ID
,以便将日志关联起来。
// app.service.ts import { Injectable } from '@nestjs/common'; import { asyncLocalStorage } from './main'; @Injectable() export class AppService { getHello(): string { const traceId = asyncLocalStorage.getStore()?.get('traceId'); console.log(`[${traceId}] Hello World!`); return 'Hello World!'; } }
在这个例子中,咱们在 getHello()
方法中获取了 Trace ID
,并将其输出到控制台中。这样,咱们就可以在日志中看到每个请求的 Trace ID
了。
4.5. 在 Controller 中使用 Trace ID
// app.controller.ts import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import { asyncLocalStorage } from './main'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { const traceId = asyncLocalStorage.getStore()?.get('traceId'); console.log(`[${traceId}] Controller: Received request`); return this.appService.getHello(); } }
4.6. 测试
启动你的 NestJS 应用,并发送一个请求。你可以看到在控制台中输出了带有 Trace ID
的日志。你可以通过在请求头中设置 x-trace-id
来手动设置 Trace ID
,或者让系统自动生成。
5. 与 Zipkin/Jaeger 集成
上面的步骤已经实现了基本的分布式追踪功能,但咱们还需要将追踪数据发送到追踪系统,比如 Zipkin 或 Jaeger,才能进行可视化分析。接下来,我将演示如何与 Zipkin 集成。
5.1. 安装依赖
首先,咱们需要安装 zipkin
相关的依赖:
npm install zipkin-transport-http zipkin-instrumentation-koa2 zipkin-instrumentation-express
5.2. 创建 Zipkin 配置
// zipkin.config.ts import { Tracer, BatchRecorder, HttpLogger } from 'zipkin'; import { HttpTransport } from 'zipkin-transport-http'; const zipkinEndpoint = process.env.ZIPKIN_ENDPOINT || 'http://localhost:9411/api/v2/spans'; // Zipkin 服务端地址 const httpTransport = new HttpTransport({ endpoint: zipkinEndpoint, headers: { 'Content-Type': 'application/json' }, }); const recorder = new BatchRecorder({ logger: new HttpLogger(), transport: httpTransport, }); export const tracer = new Tracer({ recorder, localServiceName: 'nestjs-app' }); // 替换成你的服务名
在这个配置中,咱们创建了一个 Tracer
实例,用于记录追踪信息。localServiceName
是你的服务名,zipkinEndpoint
是 Zipkin 服务端地址。咱们使用 HttpTransport
将追踪数据发送到 Zipkin 服务端。
5.3. 创建 Zipkin 中间件
// zipkin.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { tracer } from './zipkin.config'; import { asyncLocalStorage } from './main'; import { expressMiddleware } from 'zipkin-instrumentation-express'; @Injectable() export class ZipkinMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { expressMiddleware({ tracer, serviceName: 'nestjs-app', // 替换成你的服务名 remoteServiceName: 'unknown', // 替换成依赖的服务名 recordRequest: (req, res, span) => { // 记录请求信息 span.name(req.method + ' ' + req.path); }, recordResponse: (req, res, span) => { // 记录响应信息 }, })(req, res, next); } }
这个中间件使用了 zipkin-instrumentation-express
库提供的中间件,它会自动创建 Span
,并将追踪数据发送到 Zipkin。
5.4. 在 AppModule 中注册 Zipkin 中间件
// app.module.ts import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TraceMiddleware } from './trace.middleware'; import { ZipkinMiddleware } from './zipkin.middleware'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(TraceMiddleware).forRoutes('*'); consumer.apply(ZipkinMiddleware).forRoutes('*'); // 将中间件应用于所有路由 } }
在这里,咱们将 ZipkinMiddleware
应用于所有路由。
5.5. 启动 Zipkin 服务
确保你已经启动了 Zipkin 服务。你可以使用 Docker 快速启动 Zipkin:
docker run -d -p 9411:9411 openzipkin/zipkin
5.6. 测试
发送请求后,访问 Zipkin UI (http://localhost:9411/),你应该能看到你的服务的追踪信息了。
6. Jaeger 集成(类似步骤)
与 Jaeger 的集成与 Zipkin 类似,主要区别在于依赖和配置。
6.1. 安装依赖
npm install jaeger-client
6.2. 创建 Jaeger 配置
// jaeger.config.ts import { initTracer } from 'jaeger-client'; const config = { serviceName: 'nestjs-app', // 你的服务名 sampler: { type: 'const', param: 1, }, reporter: { logSpans: true, agentHost: 'localhost', // Jaeger Agent 地址 agentPort: 6832, }, }; const options = { logger: { log: (msg: any) => console.log(msg) }, }; export const tracer = initTracer(config, options);
6.3. 创建 Jaeger 中间件
// jaeger.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { tracer } from './jaeger.config'; import { asyncLocalStorage } from './main'; import { Tags, FORMAT_HTTP_HEADERS } from 'opentracing'; @Injectable() export class JaegerMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const parentSpanContext = tracer.extract(FORMAT_HTTP_HEADERS, req.headers); const span = tracer.startSpan(req.method + ' ' + req.path, { childOf: parentSpanContext, startTime: Date.now(), }); span.setTag(Tags.HTTP_METHOD, req.method); span.setTag(Tags.HTTP_URL, req.url); res.on('finish', () => { span.setTag(Tags.HTTP_STATUS_CODE, res.statusCode); span.finish(); }); req.on('close', () => { span.setTag(Tags.HTTP_STATUS_CODE, res.statusCode); span.finish(); }); asyncLocalStorage.run(new Map([['span', span]]), () => { next(); }); } }
6.4. 在 AppModule 中注册 Jaeger 中间件
// app.module.ts import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TraceMiddleware } from './trace.middleware'; import { JaegerMiddleware } from './jaeger.middleware'; @Module({ imports: [], controllers: [AppController], providers: [AppService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(TraceMiddleware).forRoutes('*'); consumer.apply(JaegerMiddleware).forRoutes('*'); // 将中间件应用于所有路由 } }
6.5. 启动 Jaeger Agent
确保你已经启动了 Jaeger Agent。你可以使用 Docker 快速启动 Jaeger Agent:
docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 16686:16686 jaegertracing/all-in-one
6.6. 测试
发送请求后,访问 Jaeger UI (http://localhost:16686/),你应该能看到你的服务的追踪信息了。
7. 处理跨服务调用
当你的 NestJS 服务需要调用其他服务时,你需要在 HTTP 请求中传递 Trace ID
和其他上下文信息。这需要修改你的 HTTP 请求客户端,比如使用 axios
或 node-fetch
。
7.1. 在请求中传递 Trace ID
// app.service.ts import { Injectable } from '@nestjs/common'; import axios from 'axios'; import { asyncLocalStorage } from './main'; @Injectable() export class AppService { async callOtherService(): Promise<string> { const traceId = asyncLocalStorage.getStore()?.get('traceId'); const headers = { 'x-trace-id': traceId, }; const response = await axios.get('http://other-service.com/api/hello', { headers }); return response.data; } }
7.2. 在其他服务中接收 Trace ID
在其他服务中,你需要接收 x-trace-id
请求头,并将其存储到 AsyncLocalStorage
中。这与之前的步骤相同。
8. 进阶技巧和注意事项
- 采样: 为了避免追踪数据的过度膨胀,追踪系统通常会进行采样。你可以配置采样策略,比如只追踪一定比例的请求。
- Span 的自定义: 你可以自定义 Span,记录更详细的信息,比如 SQL 查询、缓存操作等。
- 异常处理: 在 Span 中记录异常信息,方便定位问题。
- 异步任务: 对于异步任务,你需要确保在任务中传递
Trace ID
。可以使用AsyncLocalStorage
或其他上下文传递机制。 - 性能影响: 分布式追踪会带来一定的性能开销。你需要评估开销,并进行优化。
- 安全: 确保追踪数据不包含敏感信息。
9. 总结
今天,我带你了解了如何在 NestJS 应用中使用 AsyncLocalStorage
实现分布式追踪,并将其与 Zipkin 和 Jaeger 集成。通过使用 AsyncLocalStorage
,咱们可以方便地在异步执行上下文中传递追踪信息,从而构建完整的调用链路。希望这些内容对你有所帮助。记住,分布式追踪是一个复杂的话题,需要根据你的实际情况进行调整和优化。祝你在构建分布式系统的路上越走越远!
10. 常见问题解答
- Q: 为什么使用 AsyncLocalStorage 而不是其他上下文传递机制?
- A:
AsyncLocalStorage
能够自动在异步操作之间传递上下文,与 NestJS 框架结合紧密,简化了代码。其他的机制,例如手动传递上下文,容易出错,代码量也比较大。
- A:
- Q: 如何选择 Zipkin 和 Jaeger?
- A: Zipkin 和 Jaeger 都是优秀的分布式追踪系统。Zipkin 比较轻量级,易于部署和使用。Jaeger 提供了更多的功能,比如数据存储、可视化等,但部署相对复杂。你可以根据自己的需求选择合适的系统。
- Q: 如何在 Kubernetes 环境中部署 Zipkin 和 Jaeger?
- A: 可以使用 Helm 或 Kubernetes 的 YAML 文件来部署 Zipkin 和 Jaeger。在部署过程中,需要配置服务的域名、端口等信息。
- Q: 如何处理跨服务调用中的错误?
- A: 在跨服务调用中,需要在 Span 中记录错误信息,例如 HTTP 状态码、错误消息等。这样,在追踪系统中就可以看到错误信息,方便定位问题。
- Q: 如何优化分布式追踪的性能?
- A: 可以使用采样策略,只追踪一部分请求,减少追踪数据的量。可以优化 Span 的数量,避免过度追踪。可以优化数据传输方式,例如使用压缩等。
希望这份指南能帮助你构建更强大的分布式 NestJS 应用。加油!