☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Microservices

Kafka

Bản dịch từ docs.nestjs.com/microservices/kafka

Kafka là một nền tảng streaming phân tán, mã nguồn mở, có ba khả năng chính:

  • Publish và subscribe các stream record, tương tự như message queue hoặc hệ thống enterprise messaging.
  • Lưu trữ các stream record theo cách bền vững, chịu lỗi (fault-tolerant).
  • Xử lý các stream record ngay khi chúng xảy ra.

Dự án Kafka nhằm cung cấp một nền tảng thống nhất, throughput cao, độ trễ thấp để xử lý luồng dữ liệu thời gian thực. Nó tích hợp rất tốt với Apache Storm và Spark để phân tích dữ liệu streaming thời gian thực.

Ghi chú người dịch Nếu bạn từng dùng RabbitMQ hoặc các message queue truyền thống, điểm khác biệt lớn nhất là Kafka không xóa message sau khi consumer đã đọc — message được lưu lại trên topic theo policy retention riêng, và nhiều consumer group độc lập có thể đọc lại cùng một message theo tốc độ của riêng mình. Đây là lý do Kafka phù hợp với event sourcing và replay dữ liệu, còn queue truyền thống thì không.

Cài đặt

Để bắt đầu xây dựng microservice dựa trên Kafka, trước tiên hãy cài package cần thiết:

$ npm i --save kafkajs

Tổng quan

Giống như các cách triển khai transport layer microservice khác của Nest, bạn chọn cơ chế transporter Kafka bằng thuộc tính transport của đối tượng options truyền vào phương thức createMicroservice(), cùng với một thuộc tính options tùy chọn, như dưới đây:

const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.KAFKA,
  options: {
    client: {
      brokers: ['localhost:9092'],
    }
  }
});
const app = await NestFactory.createMicroservice(AppModule, {
  transport: Transport.KAFKA,
  options: {
    client: {
      brokers: ['localhost:9092'],
    }
  }
});

Gợi ý Enum Transport được import từ package @nestjs/microservices.

Options

Thuộc tính options đặc thù theo transporter được chọn. Transporter Kafka expose các thuộc tính được mô tả dưới đây.

client Tùy chọn cấu hình client (đọc thêm tại đây)
consumer Tùy chọn cấu hình consumer (đọc thêm tại đây)
run Tùy chọn cấu hình run (đọc thêm tại đây)
subscribe Tùy chọn cấu hình subscribe (đọc thêm tại đây)
producer Tùy chọn cấu hình producer (đọc thêm tại đây)
send Tùy chọn cấu hình send (đọc thêm tại đây)
producerOnlyMode Feature flag để bỏ qua việc đăng ký consumer group và chỉ hoạt động như producer (boolean)
postfixId Đổi hậu tố (suffix) của giá trị clientId (string)

Client

Có một khác biệt nhỏ ở Kafka so với các transporter microservice khác. Thay vì dùng class ClientProxy, ta dùng class ClientKafkaProxy.

Giống các transporter microservice khác, bạn có nhiều cách để tạo instance ClientKafkaProxy.

Một cách để tạo instance là dùng ClientsModule. Để tạo client instance với ClientsModule, import nó và dùng phương thức register() để truyền một đối tượng options với các thuộc tính giống như trong phương thức createMicroservice() ở trên, cùng với thuộc tính name dùng làm injection token. Đọc thêm về ClientsModule tại đây.

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'HERO_SERVICE',
        transport: Transport.KAFKA,
        options: {
          client: {
            clientId: 'hero',
            brokers: ['localhost:9092'],
          },
          consumer: {
            groupId: 'hero-consumer'
          }
        }
      },
    ]),
  ]
  ...
})

Các cách khác để tạo client (ClientProxyFactory hoặc @Client()) cũng có thể dùng được. Bạn có thể đọc thêm về chúng tại đây.

Dùng decorator @Client() như sau:

@Client({
  transport: Transport.KAFKA,
  options: {
    client: {
      clientId: 'hero',
      brokers: ['localhost:9092'],
    },
    consumer: {
      groupId: 'hero-consumer'
    }
  }
})
client: ClientKafkaProxy;

Message pattern

Message pattern của Kafka microservice sử dụng hai topic cho kênh request và kênh reply. Phương thức ClientKafkaProxy.send() gửi message kèm return address bằng cách gắn một correlation id, reply topic, và reply partition vào request message. Điều này yêu cầu instance ClientKafkaProxy phải subscribe vào reply topic và được gán vào ít nhất một partition trước khi gửi message.

Do đó, bạn cần có ít nhất một reply topic partition cho mỗi Nest application đang chạy. Ví dụ, nếu bạn đang chạy 4 Nest application nhưng reply topic chỉ có 3 partition, thì 1 trong 4 Nest application đó sẽ bị lỗi khi cố gửi message.

Khi các instance ClientKafkaProxy mới được khởi chạy, chúng sẽ join vào consumer group và subscribe vào các topic tương ứng. Quá trình này kích hoạt một lần rebalance các partition của topic được gán cho các consumer trong consumer group.

Thông thường, partition của topic được gán bằng round robin partitioner, gán partition cho tập hợp consumer được sắp xếp theo tên consumer (tên này được đặt ngẫu nhiên khi application khởi chạy). Tuy nhiên, khi một consumer mới join vào consumer group, consumer mới đó có thể được đặt ở bất kỳ vị trí nào trong tập hợp consumer. Điều này tạo ra tình trạng các consumer đã tồn tại từ trước có thể bị gán partition khác nếu consumer đó nằm sau consumer mới trong thứ tự sắp xếp. Kết quả là các consumer bị gán partition khác sẽ mất các response message của các request đã gửi trước khi rebalance xảy ra.

Để ngăn các consumer ClientKafkaProxy mất response message, Nest sử dụng một custom partitioner tích hợp sẵn, đặc thù cho Nest. Custom partitioner này gán partition cho tập hợp consumer được sắp xếp theo timestamp độ phân giải cao (process.hrtime()) được đặt khi application khởi chạy.

Ghi chú người dịch Cơ chế rebalance này giống hệt hành vi consumer group gốc của Kafka (không riêng gì Nest) — khi số lượng consumer trong group thay đổi, Kafka sẽ phân lại toàn bộ partition cho các consumer, kể cả những consumer không hề thay đổi. Cái mà Nest thêm vào chỉ là cách sắp xếp thứ tự consumer (theo process.hrtime() thay vì tên ngẫu nhiên) để giảm khả năng một consumer đang chờ response bị đổi partition giữa đường và “lạc mất” reply của chính nó.

Pattern dạng biểu thức chính quy (regular expression)

KafkaJS hỗ trợ subscribe topic bằng biểu thức chính quy (regular expression), và từ NestJS v12 bạn có thể truyền trực tiếp một RegExp vào @MessagePattern() hoặc @EventPattern(). Nest giữ nguyên pattern, chuyển tiếp nó tới lệnh gọi subscribe() của KafkaJS, và dùng cơ chế khớp biểu thức chính quy khi phân giải handler cho một topic gửi đến.

@EventPattern(/^hero\..+$/)
handleHeroEvents(@Payload() data: any, @Ctx() context: KafkaContext) {
  console.log(`Topic: ${context.getTopic()}`);
}
@Bind(Payload(), Ctx())
@EventPattern(/^hero\..+$/)
handleHeroEvents(data, context) {
  console.log(`Topic: ${context.getTopic()}`);
}

Điều này subscribe handler vào mọi topic khớp với biểu thức trên - hero.kill.dragon, hero.rescue.villager, v.v. - mà không cần đăng ký từng topic một cách rõ ràng. Dùng context.getTopic() để biết topic nào thực sự đã gửi message.

Gợi ý Pattern dạng biểu thức chính quy là khả năng đặc thù của Kafka; các transporter khác vẫn khớp pattern theo giá trị chính xác. Nest reset lastIndex trước khi khớp, nên các biểu thức global (/g) và sticky (/y) không gây ra lỗi khớp do giữ trạng thái (stateful).

Đăng ký nhận response message

Cảnh báo Phần này chỉ liên quan nếu bạn dùng kiểu message request-response (với decorator @MessagePattern và phương thức ClientKafkaProxy.send). Việc subscribe response topic là không cần thiết cho giao tiếp dựa trên event (decorator @EventPattern và phương thức ClientKafkaProxy.emit).

Class ClientKafkaProxy cung cấp phương thức subscribeToResponseOf(). Phương thức subscribeToResponseOf() nhận tên topic của một request làm tham số và thêm tên reply topic tương ứng vào một tập hợp reply topic. Phương thức này là bắt buộc khi triển khai message pattern.

onModuleInit() {
  this.client.subscribeToResponseOf('hero.kill.dragon');
}

Nếu instance ClientKafkaProxy được tạo bất đồng bộ (asynchronously), phương thức subscribeToResponseOf() phải được gọi trước khi gọi phương thức connect().

async onModuleInit() {
  this.client.subscribeToResponseOf('hero.kill.dragon');
  await this.client.connect();
}

Incoming (message đến)

Nest nhận các Kafka message đến dưới dạng một object có các thuộc tính key, value, và headers với giá trị kiểu Buffer. Sau đó Nest parse các giá trị này bằng cách chuyển buffer thành string. Nếu string đó “giống object” (object-like), Nest sẽ thử parse string đó dưới dạng JSON. Giá trị value sau đó được truyền tới handler tương ứng.

Outgoing (message gửi đi)

Nest gửi các Kafka message đi sau một quá trình tuần tự hóa (serialization) khi publish event hoặc gửi message. Quá trình này diễn ra trên các tham số truyền vào phương thức emit() và send() của ClientKafkaProxy, hoặc trên giá trị trả về từ phương thức được gắn @MessagePattern. Quá trình serialization này “chuyển thành string” các object không phải string hoặc buffer bằng cách dùng JSON.stringify() hoặc phương thức toString() trên prototype.

@Controller()
export class HeroesController {
  @MessagePattern('hero.kill.dragon')
  killDragon(@Payload() message: KillDragonMessage): any {
    const dragonId = message.dragonId;
    const items = [
      { id: 1, name: 'Mythical Sword' },
      { id: 2, name: 'Key to Dungeon' },
    ];
    return items;
  }
}

Gợi ý @Payload() được import từ package @nestjs/microservices.

Message gửi đi cũng có thể được gắn key bằng cách truyền một object có thuộc tính key và value. Việc gắn key cho message quan trọng để đáp ứng yêu cầu co-partitioning.

@Controller()
export class HeroesController {
  @MessagePattern('hero.kill.dragon')
  killDragon(@Payload() message: KillDragonMessage): any {
    const realm = 'Nest';
    const heroId = message.heroId;
    const dragonId = message.dragonId;

    const items = [
      { id: 1, name: 'Mythical Sword' },
      { id: 2, name: 'Key to Dungeon' },
    ];

    return {
      headers: {
        realm
      },
      key: heroId,
      value: items
    }
  }
}

Ngoài ra, message truyền theo định dạng này còn có thể chứa header tùy chỉnh được đặt trong thuộc tính hash headers. Giá trị của thuộc tính hash header phải thuộc kiểu string hoặc kiểu Buffer.

@Controller()
export class HeroesController {
  @MessagePattern('hero.kill.dragon')
  killDragon(@Payload() message: KillDragonMessage): any {
    const realm = 'Nest';
    const heroId = message.heroId;
    const dragonId = message.dragonId;

    const items = [
      { id: 1, name: 'Mythical Sword' },
      { id: 2, name: 'Key to Dungeon' },
    ];

    return {
      headers: {
        kafka_nestRealm: realm
      },
      key: heroId,
      value: items
    }
  }
}

Dựa trên event (event-based)

Trong khi phương thức request-response phù hợp để trao đổi message giữa các service, nó lại kém phù hợp hơn khi kiểu message của bạn là event-based (kiểu này ngược lại lại rất phù hợp với Kafka) - khi bạn chỉ muốn publish event mà không cần chờ response. Trong trường hợp đó, bạn không muốn chịu overhead mà request-response yêu cầu để duy trì hai topic.

Xem hai phần sau để tìm hiểu thêm về điều này: Tổng quan: Dựa trên event và Tổng quan: Publish event.

Context

Trong các tình huống phức tạp hơn, bạn có thể cần truy cập thêm thông tin về request đến. Khi dùng transporter Kafka, bạn có thể truy cập object KafkaContext.

@MessagePattern('hero.kill.dragon')
killDragon(@Payload() message: KillDragonMessage, @Ctx() context: KafkaContext) {
  console.log(`Topic: ${context.getTopic()}`);
}
@Bind(Payload(), Ctx())
@MessagePattern('hero.kill.dragon')
killDragon(message, context) {
  console.log(`Topic: ${context.getTopic()}`);
}

Gợi ý @Payload(), @Ctx() và KafkaContext được import từ package @nestjs/microservices.

Để truy cập object IncomingMessage gốc của Kafka, dùng phương thức getMessage() của object KafkaContext, như sau:

@MessagePattern('hero.kill.dragon')
killDragon(@Payload() message: KillDragonMessage, @Ctx() context: KafkaContext) {
  const originalMessage = context.getMessage();
  const partition = context.getPartition();
  const { headers, timestamp } = originalMessage;
}
@Bind(Payload(), Ctx())
@MessagePattern('hero.kill.dragon')
killDragon(message, context) {
  const originalMessage = context.getMessage();
  const partition = context.getPartition();
  const { headers, timestamp } = originalMessage;
}

Trong đó IncomingMessage tuân theo interface sau:

interface IncomingMessage {
  topic: string;
  partition: number;
  timestamp: string;
  size: number;
  attributes: number;
  offset: string;
  key: any;
  value: any;
  headers: Record<string, any>;
}

Nếu handler của bạn có thời gian xử lý chậm cho mỗi message nhận được, bạn nên xem xét dùng callback heartbeat. Để lấy hàm heartbeat, dùng phương thức getHeartbeat() của KafkaContext, như sau:

@MessagePattern('hero.kill.dragon')
async killDragon(@Payload() message: KillDragonMessage, @Ctx() context: KafkaContext) {
  const heartbeat = context.getHeartbeat();

  // Thực hiện một phần xử lý chậm
  await doWorkPart1();

  // Gửi heartbeat để không vượt quá sessionTimeout
  await heartbeat();

  // Thực hiện phần xử lý chậm tiếp theo
  await doWorkPart2();
}

Quy ước đặt tên

Các thành phần Kafka microservice thêm mô tả vai trò tương ứng của chúng vào cuối các tùy chọn client.clientId và consumer.groupId để tránh xung đột giữa thành phần client và server của Nest microservice. Theo mặc định, các thành phần ClientKafkaProxy thêm -client và các thành phần ServerKafka thêm -server vào cả hai tùy chọn này. Hãy chú ý cách các giá trị được cung cấp dưới đây bị biến đổi theo cách đó (như trong comment).

const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.KAFKA,
  options: {
    client: {
      clientId: 'hero', // hero-server
      brokers: ['localhost:9092'],
    },
    consumer: {
      groupId: 'hero-consumer' // hero-consumer-server
    },
  }
});

Và đối với client:

@Client({
  transport: Transport.KAFKA,
  options: {
    client: {
      clientId: 'hero', // hero-client
      brokers: ['localhost:9092'],
    },
    consumer: {
      groupId: 'hero-consumer' // hero-consumer-client
    }
  }
})
client: ClientKafkaProxy;

Gợi ý Quy ước đặt tên client và consumer của Kafka có thể tùy chỉnh bằng cách extend ClientKafkaProxy và KafkaServer trong provider tùy chỉnh của riêng bạn và override constructor.

Vì message pattern của Kafka microservice sử dụng hai topic cho kênh request và kênh reply, một reply pattern cần được suy ra từ request topic. Theo mặc định, tên của reply topic là ghép giữa tên request topic với hậu tố .reply.

onModuleInit() {
  this.client.subscribeToResponseOf('hero.get'); // hero.get.reply
}

Gợi ý Quy ước đặt tên reply topic của Kafka có thể tùy chỉnh bằng cách extend ClientKafkaProxy trong provider tùy chỉnh của riêng bạn và override phương thức getResponsePatternName.

Exception có thể retry

Tương tự các transporter khác, tất cả exception không được xử lý (unhandled) đều tự động được bọc vào một RpcException và chuyển đổi sang định dạng “thân thiện với người dùng”. Tuy nhiên, có những trường hợp đặc biệt (edge-case) khi bạn muốn bỏ qua cơ chế này và để exception được driver kafkajs xử lý thay thế. Ném (throw) một exception khi đang xử lý một message sẽ chỉ định kafkajs retry (gửi lại) message đó, nghĩa là mặc dù handler của message (hoặc event) đã được kích hoạt, offset sẽ không được commit vào Kafka.

Cảnh báo Đối với event handler (giao tiếp dựa trên event), tất cả exception không được xử lý được coi là exception có thể retry theo mặc định.

Để làm điều này, bạn có thể dùng một class riêng gọi là KafkaRetriableException, như sau:

throw new KafkaRetriableException('...');

Gợi ý Class KafkaRetriableException được export từ package @nestjs/microservices.

Xử lý exception tùy chỉnh

Bên cạnh các cơ chế xử lý lỗi mặc định, bạn có thể tạo một Exception Filter tùy chỉnh cho các event Kafka để quản lý logic retry. Ví dụ dưới đây minh họa cách bỏ qua một event gặp lỗi sau một số lần retry có thể cấu hình:

import { Catch, ArgumentsHost, Logger } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { KafkaContext } from '@nestjs/microservices';
import { Producer } from 'kafkajs';

@Catch()
export class KafkaMaxRetryExceptionFilter extends BaseExceptionFilter {
  private readonly logger = new Logger(KafkaMaxRetryExceptionFilter.name);

  constructor(
    private readonly producer: Producer,
    private readonly maxRetries: number,
    // Hàm tùy chọn được thực thi khi vượt quá số lần retry tối đa
    private readonly skipHandler?: (message: any) => Promise<void>,
  ) {
    super();
  }

  async catch(exception: unknown, host: ArgumentsHost) {
    const kafkaContext = host.switchToRpc().getContext<KafkaContext>();
    const message = kafkaContext.getMessage();
    const currentRetryCount = this.getRetryCountFromContext(kafkaContext);

    if (currentRetryCount >= this.maxRetries) {
      this.logger.warn(
        `Max retries (${
          this.maxRetries
        }) exceeded for message: ${JSON.stringify(message)}`,
      );

      if (this.skipHandler) {
        try {
          await this.skipHandler(message);
        } catch (err) {
          this.logger.error('Error in skipHandler:', err);
        }
      }

      try {
        await this.commitOffset(kafkaContext);
      } catch (commitError) {
        this.logger.error('Failed to commit offset:', commitError);
      }
      return; // Dừng việc lan truyền exception
    }

    // Republish message vào cùng topic với retry count đã tăng thêm
    try {
      await this.republishWithRetry(kafkaContext, currentRetryCount + 1);
      await this.commitOffset(kafkaContext);
    } catch (republishError) {
      this.logger.error(
        'Failed to republish message for retry:',
        republishError,
      );
      // Fall back về cơ chế xử lý exception mặc định
      super.catch(exception, host);
    }
  }

  private getRetryCountFromContext(context: KafkaContext): number {
    const headers = context.getMessage().headers || {};
    const retryHeader = headers['retry-count'];
    if (!retryHeader) {
      return 0;
    }
    // Giá trị header là Buffer, nên phải chuyển thành string trước
    const value = Buffer.isBuffer(retryHeader)
      ? retryHeader.toString()
      : String(retryHeader);
    return parseInt(value, 10) || 0;
  }

  private async republishWithRetry(
    context: KafkaContext,
    retryCount: number,
  ): Promise<void> {
    const topic = context.getTopic();
    const message = context.getMessage();

    await this.producer.send({
      topic,
      messages: [
        {
          key: message.key,
          value: message.value,
          headers: {
            ...message.headers,
            'retry-count': retryCount.toString(),
          },
        },
      ],
    });
  }

  private async commitOffset(context: KafkaContext): Promise<void> {
    const consumer = context.getConsumer();
    if (!consumer) {
      throw new Error('Consumer instance is not available from KafkaContext.');
    }

    const topic = context.getTopic();
    const partition = context.getPartition();
    const message = context.getMessage();
    const offset = message.offset;

    if (!topic || partition === undefined || offset === undefined) {
      throw new Error(
        'Incomplete Kafka message context for committing offset.',
      );
    }

    await consumer.commitOffsets([
      {
        topic,
        partition,
        // Khi commit offset, commit số tiếp theo (tức là offset hiện tại + 1)
        offset: (Number(offset) + 1).toString(),
      },
    ]);
  }
}

Filter này cung cấp một cách để retry xử lý một Kafka event tối đa một số lần có thể cấu hình. Khi xảy ra exception, nó republish message vào cùng topic với header retry-count đã tăng thêm, sau đó commit offset hiện tại. Khi đạt số lần retry tối đa, nó kích hoạt một skipHandler tùy chỉnh (nếu có) và commit offset, qua đó bỏ qua event gặp lỗi một cách hiệu quả. Điều này giúp các event tiếp theo được xử lý mà không bị gián đoạn.

Bạn có thể tích hợp filter này bằng cách đăng ký nó ở phạm vi global hoặc ở cấp controller. Lưu ý rằng bạn cần cung cấp một instance Kafka producer:

import { Inject, Injectable } from '@nestjs/common';
import { Producer } from 'kafkajs';

@Injectable()
export class AppKafkaRetryFilter extends KafkaMaxRetryExceptionFilter {
  constructor(@Inject('KAFKA_PRODUCER') producer: Producer) {
    super(producer, 5); // maxRetries = 5
  }
}
import { Inject, Injectable } from '@nestjs/common';

@Injectable()
export class AppKafkaRetryFilter extends KafkaMaxRetryExceptionFilter {
  constructor(@Inject('KAFKA_PRODUCER') producer) {
    super(producer, 5); // maxRetries = 5
  }
}
@Controller()
@UseFilters(AppKafkaRetryFilter)
export class MyEventHandler {
  @EventPattern('your-topic')
  async handleEvent(@Payload() data: any, @Ctx() context: KafkaContext) {
    // Logic xử lý event của bạn...
  }
}
@Controller()
@UseFilters(AppKafkaRetryFilter)
export class MyEventHandler {
  @Bind(Payload(), Ctx())
  @EventPattern('your-topic')
  async handleEvent(data, context) {
    // Logic xử lý event của bạn...
  }
}

Đảm bảo cung cấp Kafka producer trong module của bạn:

import { Kafka } from 'kafkajs';

@Module({
  providers: [
    AppKafkaRetryFilter,
    {
      provide: 'KAFKA_PRODUCER',
      useFactory: async () => {
        const kafka = new Kafka({ brokers: ['localhost:9092'] });
        const producer = kafka.producer();
        await producer.connect();
        return producer;
      },
    },
  ],
})
export class AppModule {}

Commit offset

Việc commit offset là điều cốt yếu khi làm việc với Kafka. Theo mặc định, message sẽ được tự động commit sau một khoảng thời gian nhất định. Để biết thêm thông tin, xem tài liệu KafkaJS. KafkaContext cung cấp cách để truy cập consumer đang hoạt động nhằm commit offset thủ công. Consumer chính là consumer của KafkaJS và hoạt động như cách triển khai gốc của KafkaJS.

@EventPattern('user.created')
async handleUserCreated(@Payload() data: IncomingMessage, @Ctx() context: KafkaContext) {
  // logic nghiệp vụ

  const { offset } = context.getMessage();
  const partition = context.getPartition();
  const topic = context.getTopic();
  const consumer = context.getConsumer();
  await consumer.commitOffsets([{ topic, partition, offset }])
}
@Bind(Payload(), Ctx())
@EventPattern('user.created')
async handleUserCreated(data, context) {
  // logic nghiệp vụ

  const { offset } = context.getMessage();
  const partition = context.getPartition();
  const topic = context.getTopic();
  const consumer = context.getConsumer();
  await consumer.commitOffsets([{ topic, partition, offset }])
}

Để tắt tính năng tự động commit message, đặt autoCommit: false trong cấu hình run, như sau:

const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
  transport: Transport.KAFKA,
  options: {
    client: {
      brokers: ['localhost:9092'],
    },
    run: {
      autoCommit: false
    }
  }
});
const app = await NestFactory.createMicroservice(AppModule, {
  transport: Transport.KAFKA,
  options: {
    client: {
      brokers: ['localhost:9092'],
    },
    run: {
      autoCommit: false
    }
  }
});

Ghi chú người dịch Nếu bạn quen với việc Kafka “auto-commit” nghe có vẻ an toàn, hãy cẩn thận: auto-commit chạy theo chu kỳ thời gian, không đồng bộ với việc handler của bạn xử lý xong hay chưa. Nếu app crash giữa lúc xử lý nhưng sau khi Kafka đã tự commit offset, message đó coi như “biến mất” — bạn sẽ không nhận lại nó nữa. Đây là lý do các hệ thống cần đảm bảo xử lý đúng một lần (at-least-once) thường tắt autoCommit và tự commit thủ công sau khi business logic chạy xong, giống hệt cách bạn tự tay ack message trong RabbitMQ.

Cập nhật trạng thái instance

Để nhận cập nhật thời gian thực về kết nối và trạng thái của driver instance bên dưới, bạn có thể subscribe vào stream status. Stream này cung cấp các cập nhật trạng thái đặc thù cho driver đã chọn. Với driver Kafka, stream status phát ra các event connected, disconnected, rebalancing, crashed, và stopped.

this.client.status.subscribe((status: KafkaStatus) => {
  console.log(status);
});

Gợi ý Type KafkaStatus được import từ package @nestjs/microservices.

Tương tự, bạn có thể subscribe vào stream status của server để nhận thông báo về trạng thái của server.

const server = app.connectMicroservice<MicroserviceOptions>(...);
server.status.subscribe((status: KafkaStatus) => {
  console.log(status);
});

Truy cập producer và consumer bên dưới

Đối với các use case phức tạp hơn, bạn có thể cần truy cập trực tiếp instance producer và consumer bên dưới. Điều này có thể hữu ích cho các tình huống như tự đóng kết nối thủ công hoặc dùng các phương thức đặc thù của driver. Tuy nhiên, hãy nhớ rằng trong hầu hết trường hợp, bạn không nên cần truy cập trực tiếp driver.

Để làm điều này, bạn có thể dùng getter producer và consumer được expose bởi instance ClientKafkaProxy.

const producer = this.client.producer;
const consumer = this.client.consumer;

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.