
Đạt được con số 1 triệu người dùng đồng thời (Concurrent Users – CCU) không chỉ là bài toán về việc “cắm thêm server”. Đó là cuộc chiến về CPU, bộ nhớ và độ trễ (latency). Tại Mezon, chúng tôi đã thực hiện một cuộc cách mạng về kiến trúc API để vượt qua những giới hạn vật lý của mô hình truyền thống.
Dưới đây là 6 bước tiến hóa quan trọng trong kiến trình tối ưu hóa đó.
1. Khởi đầu: gRPC Gateway & gRPC Server (Mô hình tiêu chuẩn)
Ban đầu, chúng tôi sử dụng mô hình phổ biến: gRPC Gateway làm lớp chuyển đổi từ HTTP/JSON sang gRPC để giao tiếp với các service nội bộ.
- Hạn chế: Quá trình chuyển đổi (marshalling) dữ liệu từ JSON sang Protobuf tiêu tốn rất nhiều tài nguyên CPU khi lưu lượng tăng cao.
- Vấn đề:
encoding/jsontiêu chuẩn của Go dựa trên cơ chế reflection, vốn rất chậm và gây áp lực lên Garbage Collector (GC).
2. Tối ưu hóa Marshalling: Chuyển sang VT Marshal
Nhận thấy JSON truyền thống là nút thắt cổ chai, chúng tôi đã chuyển sang sử dụng vtprotobuf (Vitess Protobuf).
- Cải tiến: Thay vì dùng cơ chế reflection, VT Marshal sinh code (code generation) để thực hiện serialization.
- Kết quả: Giảm đáng kể số lượng cấp phát bộ nhớ (allocations) và tăng tốc độ xử lý dữ liệu lên gấp nhiều lần.
3. Tách biệt API và Realtime (Microservices Isolation)
Để phục vụ 1M CCU, việc để chung logic API (Request-Response) và Realtime (WebSocket/Streaming) là một rủi ro lớn.
- Chiến lược: Chúng tôi tách riêng cụm server xử lý API thuần túy và cụm chuyên trách Realtime.
- Lợi ích: Tránh tình trạng các kết nối WebSocket giữ (hold) quá nhiều file descriptor và RAM, gây ảnh hưởng đến hiệu suất của các endpoint API quan trọng.
4. Thay thế Gateway bằng Connect-Go + FastHTTP
Giai đoạn tiếp theo là thay thế gRPC Gateway truyền thống. Chúng tôi chuyển sang sử dụng:
- Connect-Go: Một framework nhẹ hơn, hỗ trợ cả gRPC và HTTP/JSON một cách mượt mà mà không cần qua lớp proxy phức tạp.
- FastHTTP: Thay thế cho
net/httpmặc định của Go. FastHTTP sử dụng cơ chế Worker Pool và Zero-allocation giúp xử lý hàng trăm nghìn request/giây với lượng RAM cực thấp.
5. Loại bỏ Connect-Go: Tối giản hóa tối đa

Dù Connect-Go rất tốt, nhưng để chạm ngưỡng 1M CCU, mọi “layer” trung gian đều là một chi phí. Chúng tôi quyết định loại bỏ Connect-Go để viết trực tiếp các handler trên nền FastHTTP.
- Mục tiêu: Giảm tối đa các bước trung gian (middlewares) không cần thiết.
- Hiệu quả: Đường đi của dữ liệu từ Network Card đến Logic xử lý là ngắn nhất có thể.
6. Cú hích cuối cùng: Unix Domain Socket (UDS)
Thông thường, Nginx giao tiếp với Backend qua TCP/IP (localhost:8080). Tuy nhiên, TCP stack của OS có những giới hạn về port và overhead của quá trình handshake.
- Giải pháp: Thay thế kết nối Nginx <-> FastHTTP bằng Unix Domain Socket.
- Tại sao lại nhanh hơn? UDS bỏ qua toàn bộ các bước kiểm tra checksum, sequence number của giao thức TCP. Dữ liệu được truyền trực tiếp trong bộ nhớ kernel giữa các process.
- Kết quả: Giảm độ trễ cực sâu và loại bỏ hoàn toàn lỗi “Ephemeral Port Exhaustion” khi traffic cực đại.
7. Tối ưu giao tiếp liên dịch vụ (Inter-service) qua NATS: Sức mạnh của O(1)
Khi quy mô hệ thống đạt đến 1M CCU, việc các service nói chuyện với nhau qua JSON hay String truyền thống sẽ gây ra thảm họa về hiệu năng. Chúng tôi đã chuyển sang dùng NATS Messaging với một Protocol tùy chỉnh dựa trên Byte Array.
Cấu trúc gói tin: Header | Key | Value
Thay vì gửi một bản tin vô định hình, mỗi message truyền qua NATS được đóng gói chặt chẽ:
- Header: Chứa metadata (loại message, version, độ dài).
- Key: Định danh hành động hoặc tài nguyên.
- Value: Payload thực tế (đã được VT Marshal hóa).
Tại sao lại là O(1)?
Thông thường, khi dùng String, hệ thống phải quét qua từng ký tự để tìm điểm bắt đầu và kết thúc (scanning/parsing), độ phức tạp tỷ lệ thuận với độ dài chuỗi O(n).
Với cấu trúc Byte Array cố định:
- Đọc Header: Biết ngay vị trí (offset) chính xác của Key và Value.
- Truy cập trực tiếp: Hệ thống chỉ cần nhảy đến đúng địa chỉ bộ nhớ (Pointer Arithmetic) để lấy dữ liệu mà không cần parse lại toàn bộ gói tin.
- Zero-copy: Dữ liệu được đọc trực tiếp từ buffer, giúp tốc độ decode đạt mức O(1) – tức là thời gian xử lý không đổi bất kể dữ liệu lớn hay nhỏ.
Kết quả: Việc loại bỏ các bước
string splithayregextrong khâu trung gian giúp CPU “thảnh thơi” hơn, tập trung toàn bộ tài nguyên cho logic nghiệp vụ thay vì xử lý chuỗi.
Tổng kết kết quả

Nhờ sự kiên trì trong việc tối ưu hóa từng byte dữ liệu và từng bước nhảy của gói tin, Mezon API hiện tại đã có thể:
- Chịu tải 1M+ CCU một cách ổn định.
- Giảm 40-60% chi phí hạ tầng so với mô hình ban đầu.
- Độ trễ (P99) luôn duy trì ở mức cực thấp ngay cả trong giờ cao điểm.
Bài học rút ra: Đừng ngại đập đi xây lại những thứ “tiêu chuẩn” nếu chúng trở thành rào cản cho sự tăng trưởng.
