Hack hiệu suất NestJS: Giảm 50% tải CPU với OpenTelemetry BatchSpan
Cải Thiện Hiệu Suất Dự Án NestJS với OpenTelemetry 🎉
Giới thiệu
Chào các bạn! 👋 Hôm nay mình muốn chia sẻ với các bạn một khám phá thú vị để cải thiện hiệu suất của dự án NestJS mà mình đang làm việc, bằng cách sử dụng OpenTelemetry. Điều đặc biệt là chỉ với một vài thay đổi nhỏ trong mã nguồn, hiệu suất đã cải thiện lên đến 50%.
Một Ngày Tò Mò
Mình đang làm tại một công ty đang triển khai OpenTelemetry để theo dõi hiệu suất và phát hiện các vấn đề trong các dịch vụ của mình. Mặc dù không phải là thành viên chính trong dự án này, nhưng vì yêu thích công nghệ, mình đã quyết định tìm hiểu về cách mà OpenTelemetry được tích hợp trong dự án.

Khám Phá Mã Nguồn Hiện Tại 🧐
Khi xem qua đoạn mã cấu hình OpenTelemetry, mình thấy rằng team đang sử dụng SimpleSpanProcessor
như dưới đây:
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],
}),
Mặc dù mã này có vẻ hoàn toàn bình thường, mình quyết định tìm hiểu thêm liệu có gì cải thiện không.
Tài Liệu Mở Ra Chân Trời Mới ✨
Sau khi nghiên cứu tài liệu, mình nhận ra OpenTelemetry cung cấp hai loại SpanProcessor
chính:
- SimpleSpanProcessor: Xử lý và gửi từng span ngay khi chúng được tạo.
- BatchSpanProcessor: Gom nhóm các span và gửi chúng theo lô, giúp giảm số lượng request.
Tài liệu khuyến nghị BatchSpanProcessor
cho môi trường sản xuất vì nó hiệu quả hơn về tài nguyên.
Đặt Giả Thuyết và Bắt Đầu Thử Nghiệm 🧪
Mình quyết định thử nghiệm xem hiệu suất sẽ cải thiện ra sao nếu chuyển từ SimpleSpanProcessor
sang BatchSpanProcessor
. Để kiểm chứng, mình sử dụng k6 - một công cụ mạnh mẽ để kiểm tra tải.
Cấu Hình Thử Nghiệm:
- 100 người dùng ảo
- Thời gian thử nghiệm: 300 giây
- API endpoint:
/card
- API lấy thông tin thẻ
Kịch Bản 1: Sử Dụng SimpleSpanProcessor
Khi thử nghiệm với SimpleSpanProcessor
, Grafana gợi ý 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
Mình 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
Kết quả thực sự cực kỳ ấn tượng:
- CPU sử dụng giảm đáng kể: Pod 1 ~50%, Pod 2 ~35% (giảm khoảng 47-50%)

Biểu đồ CPU Usage: Đường màu xanh (trái) là SimpleSpanProcessor, đường màu cam và vàng (phải) là BatchSpanProcessor.
Việc sử dụng BatchSpanProcessor
đã cải thiện đáng kể hiệu suất CPU (mình quên kiểm tra RAM và Network 😅).
Hiểu Rõ Hơn Về Sự Khác Biệt 🔍
Để làm nổi bật sự khác biệt, ta có thể tưởng tượng hoạt động của hai processor như sau:
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.
Như vậy, BatchSpanProcessor
hoạt động hiệu quả hơn, không cần phải gửi mỗi span liên tục. Tuy nhiên, 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.
Thông Số Cấu Hình của BatchSpanProcessor
Trong cấu hình của BatchSpanProcessor
, mình đã thiết lập ba tham số quan trọng:
- 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ỏ.
- scheduledDelayMillis (500): Khoảng thời gian (tính bằng mili giây) giữa các lần gửi batch. Hệ thống sẽ gửi batch spans mỗi 500ms.
- maxExportBatchSize (512): Số lượng spans tối đa trong mỗi batch được gửi đi. Nếu 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 có thể được tùy chỉnh dựa theo nhu cầu của hệ thống bạn.
Mình đã chia sẻ phát hiện này với team và chúng mình quyết định triển khai BatchSpanProcessor
cho tất cả các service. 🤓
Để lại ý kiến của bạn về cải tiến này nhé! Chúc các bạn lập trình vui vẻ! 🥳
Comments ()