Logo
Published on

[NestJS] OpenTelemetry: tối ưu 50% hiệu suất

Authors

Tò mò cũng thú vị 👻

Hi 👋, hôm nay tớ lại mò mẫm nghịch ngợm và phát hiện ra một cách cải thiện hiệu suất cực kỳ đơn giản mà hiệu quả trong một dự án NestJS sử dụng OpenTelemetry. Đoán xem, chỉ cần thay đổi một vài dòng code mà hiệu suất cải thiện tận 50% luôn đó!

Chuyện bắt đầu từ một ngày rảnh rỗi...

Công ty tớ đang triển khai OpenTelemetry cho các service để theo dõi hiệu suất và phát hiện vấn đề. Tuy tớ không trực tiếp tham gia vào dự án này, nhưng vì tò mò muốn tìm hiểu thêm về công nghệ mới, nên một ngày đẹp trời tớ quyết định mò vào xem code triển khai OpenTelemetry trong project như thế nào.

Curious developer

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 1Tạo SpanGửi SpanTiếp tục xử lý
Request 2Tạo SpanGửi SpanTiếp tục xử lý
Request 3Tạo SpanGửi SpanTiếp tục xử lý
...

BatchSpanProcessor

Request 1Tạo SpanLưu vào buffer → Tiếp tục xử lý
Request 2Tạo SpanLưu vào buffer → Tiếp tục xử lý
Request 3Tạo SpanLư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.


Tớ đã chia sẻ phát hiện này với team và chúng tớ đã triển khai BatchSpanProcessor cho tất cả các service 🤓

Các bạn nghĩ sao về cải tiến này, hãy cùng comment nhé ^^