Kiểm thử chịu tải (Load testing) là bước quan trọng cuối cùng trước khi đưa hệ thống phân tán (System Design) vào vận hành. Việc kiểm thử không chỉ đơn thuần là chạy script và ghi nhận kết quả throughput 20,000 RPS với 0% lỗi. Một kỹ sư hệ thống cần đảm bảo tối ưu hóa thông số Kernel Linux, xử lý triệt để hiện tượng Coordinated Omission và mô phỏng chính xác các kịch bản lưu lượng thực tế.

Answer-first: Kiểm thử chịu tải hệ thống định tuyến (routing engine) đòi hỏi tối ưu toàn diện từ tầng mạng Kernel Linux (sockets, TCP reuse, SOMAXCONN), cấu hình bộ lập lịch (scheduler) Golang đến quản lý bộ nhớ của công cụ test. Trong năm 2026, các hệ thống high-performance còn kết hợp eBPF để theo dõi (trace) bottleneck tại kernel level mà không gây overhead.


1. Các Cạm Bẫy Trong Kiểm Thử Tải (Load Testing)

Hiện Tượng “Bỏ Sót Đồng Bộ” (Coordinated Omission)

Khi cấu hình K6 ở “Mô hình Khép kín” (Closed Model - constant-vus: 1000), các User Ảo (Virtual Users) sẽ chờ phản hồi từ server trước khi gửi request tiếp theo. Nếu API bị chậm (ví dụ tăng độ trễ từ 50ms lên 5,000ms), công cụ test sẽ tự động giảm tốc độ gửi request, dẫn đến kết quả phản ánh không chính xác tải thực tế. Giải Pháp: Sử dụng “Mô hình Mở” (Open Model - constant-arrival-rate). Cấu hình này buộc K6 duy trì tốc độ gửi request cố định (ví dụ 10,000 requests/giây) theo đúng lịch trình, giúp phát hiện chính xác giới hạn của hàng đợi hệ thống (system queue).

Thử Tải Tầng Cache Thay Vì Routing Engine

Nếu script K6 sử dụng danh sách tọa độ cố định (hardcoded), Graphhopper chỉ tính toán ở request đầu tiên, các request sau sẽ nhận kết quả trực tiếp từ Redis cache. Điều này làm cho bài kiểm thử phản ánh hiệu năng Redis thay vì routing engine. Giải Pháp: Sử dụng SharedArray của K6 để nạp tập dữ liệu tọa độ GPS ngẫu nhiên từ file bên ngoài, buộc Graphhopper thực hiện tính toán lộ trình thực tế cho các yêu cầu khác nhau.

Điểm Nghẽn Bộ Nhớ K6 Do Metric Cardinality (High Cardinality OOM)

Khi gửi truy vấn với tham số tọa độ ngẫu nhiên (như /api/route?lat=X&lng=Y), K6 có thể ghi nhận mỗi URL thành một metric riêng biệt. Việc này tạo ra hiện tượng High Cardinality, gây cạn kiệt bộ nhớ (OOM - Out of Memory) trên tiến trình K6. Bạn cần gom nhóm các request bằng cách sử dụng thuộc tính name trong K6.


2. Tối Ưu Kernel Linux & Golang Runtime

Để đáp ứng mức tải 20,000 RPS, các thông số mặc định của Hệ điều hành (OS) cần được điều chỉnh để tránh việc Kernel từ chối kết nối.

Cạn Kiệt Socket & Tối Ưu somaxconn

Khi K6 gửi lưu lượng lớn tới API Gateway Golang, hệ thống có thể xuất hiện lỗi Connection Refused dù CPU ở mức thấp. Nguyên nhân do hàng đợi kết nối (Listen Backlog queue) của OS bị đầy. Cần điều chỉnh thông số hệ thống bằng lệnh sysctl -w net.core.somaxconn=65535 để nâng giới hạn hàng đợi cho quá trình bắt tay TCP (TCP handshake).

Hiện Tượng Mất Gói Tin Do nf_conntrack

Khi K6 báo lỗi timeout nhưng CPU chưa quá tải và somaxconn đã tối ưu, bạn cần kiểm tra log dmesg tìm cảnh báo nf_conntrack: table full, dropping packet. Firewall của Kernel theo dõi trạng thái kết nối; khi số lượng kết nối tăng vọt, bảng conntrack bị đầy làm rớt gói tin (drops packets). Giải pháp là tăng giới hạn qua net.netfilter.nf_conntrack_max hoặc cấu hình iptables -j NOTRACK cho các cổng dịch vụ.

Sau khi hệ thống được tối ưu hóa chịu tải, bước tiếp theo là triển khai lên môi trường production. Theo dõi Phần 8: Cập Nhật Bản Đồ Không Gián Đoạn (Zero-Downtime) & Kubernetes Đa Vùng (Multi-Region) để thiết lập hạ tầng Kubernetes tự phục hồi (self-healing), cập nhật dữ liệu bản đồ không gián đoạn và định tuyến đa vùng.


Câu Hỏi Thường Gặp (FAQ): Xử Lý Điểm Nghẽn Hiệu Năng Golang

API Golang chạy trên Kubernetes bị giới hạn CPU (CPU Throttled) dù mức sử dụng thực tế chưa đạt ngưỡng?
Nguyên nhân do cơ chế xác định số CPU (GOMAXPROCS) trong container. Go mặc định đọc số lượng CPU của Node chủ (Host Node - ví dụ 64 cores) thay vì định mức tài nguyên của Pod (cấp phát ví dụ 2 cores), dẫn đến việc khởi tạo 64 threads. Bộ lập lịch Linux CFS sẽ giới hạn tài nguyên (throttling) do xung đột chuyển đổi ngữ cảnh (context-switching). Bạn nên tích hợp thư viện go.uber.org/automaxprocs (hoặc nâng cấp lên Go 1.25+) để tự động đồng bộ GOMAXPROCS với cgroup limit của K8s.
Ở mức 10,000 RPS, CPU của API Golang tiêu tốn phần lớn tài nguyên cho việc khởi tạo kết nối TCP?
Cấu hình http.Transport mặc định trong Golang giới hạn MaxIdleConnsPerHost = 2. Dưới tải cao, client liên tục đóng và khởi tạo lại các kết nối TCP, gây lãng phí tài nguyên cho quá trình bắt tay TLS (TLS handshake). Bạn cần tăng thông số MaxIdleConnsPerHost lên ngưỡng phù hợp (như 100 hoặc cao hơn) để duy trì connection pool hiệu quả.
Ứng dụng bị cạn kiệt CPU khi chuyển tiếp (forwarding) dữ liệu GeoJSON dung lượng lớn?
Hiện tượng này phát sinh do chi phí giải mã JSON bằng cơ chế reflection trong json.Unmarshal. Cần thay thế bằng io.Copy hoặc httputil.ReverseProxy để truyền trực tiếp luồng byte từ Graphhopper tới client mà không cần qua tiến trình parse JSON.
Bật nén Gzip làm tăng đột biến dung lượng sử dụng CPU?
Thư viện compress/gzip tiêu chuẩn của Go chưa tối ưu hóa bằng các tập lệnh SIMD phần cứng khi xử lý dữ liệu lớn. Bạn có thể chuyển sang sử dụng github.com/klauspost/compress/gzip (hỗ trợ tập lệnh SSE 4.2/AVX2) hoặc chuyển giao nhiệm vụ nén dữ liệu cho tầng Reverse Proxy/Edge Gateway (như Nginx hay Envoy).

🔗 Đọc thêm các chuyên đề liên quan: