Get in touch
or send us a question?
CONTACT

Hack hiệu suất NestJS: Giảm 50% tải CPU với OpenTelemetry BatchSpan

Khám phá code hiện tại 🧐

Khi xem qua đoạn code cấu hình OpenTelemetry, tớ nhận thấy team đang sử dụng SimpleSpanProcessor:

OpenTelemetryModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => {
    const serviceName = configService.get('SERVICE_NAME');
    return {
      serviceName,
      spanProcessor: new SimpleSpanProcessor(
        new OTLPTraceExporter({
          url: configService.get('OPEN_TELEMETRY_TRACE_URL'),
        }),
      )
    };
  },
  inject: [ConfigService],
}),

Thoạt nhìn, đoạn code trên hoàn toàn bình thường và đúng chuẩn. Nhưng vì tính tò mò nên tớ quyết định tìm hiểu thêm về OpenTelemetry và xem liệu có gì có thể cải thiện không.

Khi đọc tài liệu mở ra chân trời mới ✨

Sau một hồi đọc tài liệu, tớ phát hiện ra OpenTelemetry cung cấp hai loại SpanProcessor chính:

  1. SimpleSpanProcessor: Xử lý và gửi từng span riêng lẻ ngay khi chúng được tạo ra
  2. BatchSpanProcessor: Gom nhóm các span lại và gửi theo lô, giảm thiểu số lượng request

Tài liệu còn nhấn mạnh rằng:

“BatchSpanProcessor được khuyến nghị sử dụng trong môi trường production vì nó hiệu quả hơn về tài nguyên. SimpleSpanProcessor nên chỉ được sử dụng trong môi trường phát triển hoặc debug.”

Nhưng tớ vẫn chưa biết dùng BatchSpanProcessor, theo lý thuyết có vẻ hay đó, nhưng test lại xem sao há 😆

Đặt giả thuyết và bắt đầu thử nghiệm 🧪

Với thông tin này, tớ đặt giả thuyết: “Nếu chuyển từ SimpleSpanProcessor sang BatchSpanProcessor, hiệu suất hệ thống sẽ cải thiện đáng kể.”

Để kiểm chứng, tớ quyết định dùng k6 – một công cụ load testing mạnh mẽ, để so sánh hiệu suất giữa hai cấu hình.

Cấu hình test:

  • 100 virtual users
  • Thời gian test: 300 giây
  • API endpoint: /card – api để lấy thông tin thẻ

Kịch bản 1: Sử dụng SimpleSpanProcessor

Khi chạy ứng dụng với SimpleSpanProcessor, Grafana hiển thị mức tải CPU rất cao:

  • CPU sử dụng: Pod 1 ~95%, Pod 2 ~70%

Kịch bản 2: Chuyển sang BatchSpanProcessor

Tớ thay đổi cấu hình thành:

OpenTelemetryModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => {
    const serviceName = configService.get('SERVICE_NAME');
    return {
      serviceName,
      spanProcessor: new BatchSpanProcessor(
        new OTLPTraceExporter({
          url: configService.get('OPEN_TELEMETRY_TRACE_URL'),
        }),
        {
          maxQueueSize: 4096,
          scheduledDelayMillis: 500,
          maxExportBatchSize: 512,
        },
      )
    };
  },
  inject: [ConfigService],
}),

Kết quả sau khi chuyển đổi thật đáng kinh ngạc:

  • CPU sử dụng giảm mạnh: Pod 1 ~50%, Pod 2 ~35% (giảm khoảng 47-50%)
CPU Usage Comparison

Biểu đồ CPU Usage: Đường màu xanh (trái) là SimpleSpanProcessor, đường màu cam và vàng (phải) là BatchSpanProcessor

Quả thật sử dụng BatchSpanProcessor đã cải thiện đáng kể hiệu suất CPU(phần này mình quên check RAM và Network @@)

Hiểu rõ hơn về sự khác biệt 🔍

Để dễ hiểu sự khác biệt, có thể tưởng tượng như thế này:

SimpleSpanProcessor

Request 1 → Tạo Span → Gửi Span → Tiếp tục xử lý
Request 2 → Tạo Span → Gửi Span → Tiếp tục xử lý
Request 3 → Tạo Span → Gửi Span → Tiếp tục xử lý
...

BatchSpanProcessor

Request 1 → Tạo Span → Lưu vào buffer → Tiếp tục xử lý
Request 2 → Tạo Span → Lưu vào buffer → Tiếp tục xử lý
Request 3 → Tạo Span → Lưu vào buffer → Tiếp tục xử lý
...
(Sau 500ms hoặc khi đủ 512 spans)
→ Gửi tất cả spans trong một request

Xem qua 2 cách hoạt động dễ dàng thấy được BatchSpanProcessor hoạt động một cách tối ưu hơn, không phải gửi liên tục Span, nhưng khi sử dụng BatchSpanProcessor bạn cần hiểu rõ các thông số cấu hình để hệ thống hoạt động trơn tru hơn

Hiểu rõ hơn về BatchSpanProcessor

Trong cấu hình của BatchSpanProcessor, tớ đã thiết lập 3 tham số quan trọng:

  1. maxQueueSize (4096): Số lượng spans tối đa có thể được lưu trong bộ đệm. Nếu bộ đệm đầy, spans mới sẽ bị loại bỏ.
  2. scheduledDelayMillis (500): Khoảng thời gian (tính bằng mili giây) giữa các lần gửi batch. Trong trường hợp này, hệ thống sẽ gửi batch spans mỗi 500ms.
  3. maxExportBatchSize (512): Số lượng spans tối đa trong mỗi batch được gửi đi. Nếu trong queue có nhiều hơn 512 spans, hệ thống sẽ chia thành nhiều batches để gửi.

Các thông số này tuỳ thuộc vào hệ thống của bạn mà cân nhắc tuỳ chỉnh cho phù hợp nha

Nguon: viblo