WEBKT

NestJS 中 AsyncLocalStorage 实现分布式追踪:实战指南与 Zipkin/Jaeger 集成

10 0 0 0

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(); // 调用下一个中间件或路由处理程序
});
}
}

这个中间件做了以下几件事:

  1. 从请求头中获取 x-trace-id,如果不存在,则生成一个 UUID 作为 Trace ID
  2. Trace ID 设置到响应头中,以便下游服务可以获取。
  3. 使用 asyncLocalStorage.run() 方法,将 Trace ID 存储到 AsyncLocalStorage 中。
  4. 调用 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 请求客户端,比如使用 axiosnode-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 框架结合紧密,简化了代码。其他的机制,例如手动传递上下文,容易出错,代码量也比较大。
  • 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 应用。加油!

代码飞侠 NestJS分布式追踪AsyncLocalStorageZipkinJaeger

评论点评

打赏赞助
sponsor

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

分享

QRcode

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