Ép tải (Load testing) là trùm cuối (final boss) của Hệ thống Thiết kế (System Design). Một tay kỹ sư non tơ chạy thử một đoạn script, hí hửng vỗ ngực khi thấy “20,000 RPS” đi kèm với 0 lỗi, rồi đinh ninh vỗ đùi cái bép là hệ thống đã sẵn sàng rước khách. Một gã Kỹ sư Principal lão làng thì thừa biết thừa hiểu rằng: trừ phi mày đè cái Kernel Linux ra độ lại (tune), đâm lủng cái bẫy Coordinated Omission, và giả lập một màn hỗn loạn thực tế (realistic chaos), còn không thì cái con số hào nhoáng đó hoàn toàn là đồ dối trá (complete lie).

Answer-first: Kiểm tra ép tải (Load testing) cho một hệ thống định tuyến (routing engine) không chỉ là vác code Go ra khè nhau. Đó là một trận tra tấn (brutal stress test) tàn bạo lên toàn bộ ngăn xếp mạng (network stack) của cái Kernel Linux (từ sockets, tái chế TCP, tới giới hạn SOMAXCONN), bóp nặn cái bộ lập lịch (scheduler) của Golang, và bào mòn cả mức ngốn RAM của chính cái công cụ ép tải nhà bạn.


1. Mấy Lời Nói Dối Xảo Trá Của Trò Ép Tải (Load Tester)

Cái Bẫy “Bỏ Sót Đồng Bộ” (Coordinated Omission Trap)

Nếu bạn lười biếng để con K6 chạy theo cái “Mô hình Khép kín” (Closed Model - cấu hình constant-vus: 1000), đám User Ảo (Virtual Users) sẽ ngồi ngáp vặt ngóng cái server chậm chạp ói ra phản hồi rồi mới dám vã tiếp cái request thứ hai. Nếu cái API của bạn cạn kiệt sinh lực, tụt lùi từ tốc độ 50ms rớt thê thảm xuống 5,000ms, thì cái máy nã đạn ép tải kia nó cũng sẽ tự động nương tay (inherently slows down) để “bảo kê” cho cái server. Cuối ngày, bản báo cáo trả về 0 lỗi tròn trĩnh, nhưng lên production là chết tươi. Thuốc Giải: Bạn BẮT BUỘC phải xài “Mô hình Mở” (Open Model - cấu hình constant-arrival-rate). Cục bùa này sẽ ép con K6 tàn nhẫn xả đạn (ruthlessly blast) vô đầu server đúng 10,000 request mỗi giây theo đúng lịch trình vạch sẵn không suy suyển, vạch trần phơi bày điểm yếu chí mạng thực sự của cái hàng đợi hệ thống (system queue).

Trò Ép Tải Thằng Cache, Khỏi Ép Engine

Nếu cái script K6 của bạn ngây ngô nã mấy tọa độ vĩ độ/kinh độ đóng đinh (hardcoded), thằng Graphhopper chỉ phải làm việc đúng nháy đầu tiên (first request), rồi 19,999 cái request còn sót lại sẽ được bốc dọn sẵn từ mâm Redis (instantly served) dâng tận họng. Chúc mừng, bạn đang mải mê ép tải con Redis chứ éo phải cái routing engine. Thuốc Giải: Móc cái SharedArray của K6 ra để nhồi (inject) nguyên một bộ sưu tập GPS hỗn mang (randomized GPS traces) đời thực, bốc từ file JSON bên ngoài vào. Ép thằng Graphhopper phải hì hục cày tính hàng ngàn lộ trình không đụng hàng (unique paths).

Điểm Nghẽn OOM Do Mức Độ Đa Dạng Của Metric K6 (Metric Cardinality OOM)

Lúc vã thử tải bằng các tọa độ ngẫu nhiên biến thiên (ví dụ: /api/route?lat=X&lng=Y), thằng K6 sẽ ngu ngốc ráng sức nhét từng biến thể URL (URL variation) đó vào RAM dưới dạng một cái metric (nhật ký đo đạc) riêng biệt. Cái sự cuồng dâm dữ liệu này sẽ đẻ ra một vụ nổ “Mức độ Đa dạng Cao” (High Cardinality), đập chết tươi cái tiến trình bắn đạn K6 bằng cái mác lỗi Cạn Kiệt Bộ Nhớ (OOM - Out of Memory). Bạn BẮT BUỘC phải gom chùm (group) mấy cái request đó lại xài cái thẻ name của K6.


2. Rèn Độ Lại Kernel Linux & Bộ Máy Golang (Runtime Tuning)

Đừng mơ cái trò chạm ngưỡng 20,000 RPS mà cứ ôm cái cài đặt mặc định của Hệ điều hành (OS settings). Cái kernel Linux sẽ ngay lập tức tự vệ bằng cách thẳng tay vứt sọt rác (dropping) mấy cái kết nối của bạn.

Khủng Hoảng Cạn Kiệt Socket & somaxconn

Khi con K6 đâm lủng cái API Golang của bạn, bạn khéo sẽ tá hỏa thấy một nùi lỗi Connection Refused (Từ chối kết nối) văng ra, bất chấp cái con CPU của Golang nó đang nằm thư giãn ngáp ruồi ở mức 10%. Hiện tượng này xảy ra bởi vì cái hàng đợi “Ngóng chờ lọt hố” (Listen Backlog queue) của Hệ điều hành nó đầy ứ ự. Mở terminal lên, phang thẳng cái lệnh sysctl -w net.core.somaxconn=65535 để ép thằng OS giãn nở cục hàng đợi cho mấy cú bắt tay TCP (TCP handshakes) còn có lối vào.

Cú Đánh Lén Rớt Gói Tin Ngầm Của nf_conntrack (Silent Packet Drops)

Nếu K6 khóc lóc báo timeout (đứt gánh), CPU nằm yên chơi xơi nước, và somaxconn đã bơm căng đét, lẹ tay soi cái log dmesg tìm ngay dòng nf_conntrack: table full, dropping packet. Cái tường lửa (firewall) của Kernel nó lanh chanh ghi chép lại sinh mệnh mọi kết nối. Dưới trận bom đạn ép tải khốc liệt, cái cuốn sổ tay này vỡ nát (fills up) và lén lút (silently) vứt rớt các gói tin (drops packets) mà không thèm báo cáo. Bạn chỉ có 2 đường: một là bơm to cái net.netfilter.nf_conntrack_max, hai là xài cái bùa iptables -j NOTRACK để tước cờ bãi nhiệm nó luôn.

Mọi thứ đã được lên dây cót độ đẽo (fully optimized) bóng loáng sẵn sàng nghênh chiến dưới áp lực tải khổng lồ, công đoạn cuối cùng là đôn lên production (production deployment). Nhổ neo tớ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 cấu hình chữa lành sinh mạng (self-healing) hạ tầng Kubernetes, lên đời bản đồ không gián đoạn (zero-downtime map updates), và định tuyến đa quốc gia (multi-region).


Hỏi Nhanh Đáp Gọn (FAQ): Các Điểm Nghẽn Hiệu Năng Ở Golang

Con API Golang nhà tôi chạy trên Kubernetes (K8s) tự nhiên bị giật lag banh chành với cái mác cảnh báo 'CPU Throttled' (Bị bóp CPU), trong khi soi cái thanh sử dụng CPU thì tấp tè. Là cớ làm sao?
Bạn vấp trúng cú chênh phỏm GOMAXPROCS. Thằng Go nó lanh chanh đọc đếm tổng số CPU của cái Node chủ (host Node - ví dụ vác tới 64 lõi) thay vì đếm cái định mức (limit) của cục Pod chứa nó (ví dụ chỉ 2 lõi còm) rồi hung hăng đẻ ra 64 luồng (threads). Cái kernel Linux (thằng chia quota CFS) ngứa mắt (aggressively throttles) nhảy vào đạp thắng liên tục, đẻ ra hàng núi độ trễ khổng lồ do trò đổi ca (context-switching latency). Ốp ngay thằng go.uber.org/automaxprocs (hoặc độ lên Go 1.25+) để đồng bộ thằng Go lại cho khớp với định mức K8s.
Lết tới ngưỡng 10,000 RPS, con CPU của con API nhà tôi phình lên bốc khói chỉ ráng làm mỗi cái việc thiết lập kết nối (establishing connections). Chữa kiểu gì bây giờ?
Cái http.Transport mặc định của Golang chốt hạ một cái hố sâu tăm tối: MaxIdleConnsPerHost = 2. Nằm dưới gánh nặng (heavy load), nó hùng hục cắm đầu xây rồi xô sập (violently tears down and rebuilds) 9,998 cái đường TCP mỗi giây, đốt trụi CPU chỉ mải mê làm trò bắt tay TLS (TLS handshakes). Bạn BẮT BUỘC phải ra lệnh ép cái MaxIdleConnsPerHost tăng lên mức 100 hoặc to hơn (higher) để duy trì bể chứa kết nối (connection pool).
Con API nhà tôi đột tử (crashes) ngốn ráo 100% CPU mỗi bận phải vác cục lộ trình GeoJSON nặng 50MB đi trung chuyển (forwarding). Có chiêu nào trị không?
Bệnh này gọi là Điểm nghẽn Soi Gương JSON (JSON Reflection Bottleneck). Nếu bạn ngu ngơ lôi json.Unmarshal ra để nhai cái mớ phản hồi (response), thằng Golang sẽ lạm dụng thuật soi rọi (massive reflection) và phân bổ bộ nhớ chất đống (heap allocations). Bạn BẮT BUỘC phải xài io.Copy hoặc httputil.ReverseProxy để đổ thẳng mớ bytes đó từ đầu Graphhopper trôi tuột vô họng client, coi như đá văng (bypassing) mớ trình đọc lậu JSON (JSON parser) của Golang ra rìa.
Tui vừa bật cái công tắc nén (Gzip compression) để ráng dành dụm băng thông (bandwidth), thế quái nào giờ con CPU nhà tôi lại đội vòm (maxed out) bốc cháy?
Cái thư viện chuẩn compress/gzip rẻ rách của thằng Go nó mồ côi éo có trò tối ưu phần cứng SIMD (hardware SIMD optimization) nên nó đốt sạch sinh lực CPU hòng nén đám JSON khổng lồ (large JSON). Bạn BẮT BUỘC phải nhảy thuyền (switch to) lôi cục github.com/klauspost/compress/gzip về (nó xài mấy tập lệnh hợp ngữ SSE 4.2 bá đạo), hoặc vứt luôn cái của nợ nén nhả đó giao thẳng cho một thằng Proxy đứng cửa (Edge Proxy) cỡ Nginx lo liệu giùm.