📖 Bản tiếng Anh (English Edition)
← Chương trước: Phần 6: Gom Nhóm Vị Trí Với Uber H3 & Caching Ngữ Nghĩa (Semantic Caching) | Mục lục Series | Chương tiếp theo: Phần 8: Cập Nhật Bản Đồ Không Gián Đoạn (Zero-Downtime) & Kubernetes Đa Vùng →
Tóm tắt cốt lõi: Kiểm thử chịu tải 50,000 RPS cho hệ thống định tuyến địa lý đòi hỏi loại bỏ hoàn toàn hiện tượng Bỏ sót Đồng bộ (Coordinated Omission) bằng mô hình tải mở (Open-Model Arrival Rate), tinh chỉnh tầng sâu nhân mạng Linux (
tcp_tw_reuse, mở rộng dải cổngip_local_port_range, nângsomaxconn), tối ưu hóa kết nối HTTP/2 multiplexing và xây dựng bộ sinh tải phân tán bằng Go 1.25 với zero-allocation memory pooling để phản ánh chính xác độ trễ P99 thực tế dưới áp lực cực hạn.
1. Bản Chất Kỹ Thuật Của Kiểm Thử Chịu Tải Đồ Thị Địa Lý
Kiểm thử chịu tải cho một hệ thống định tuyến địa lý và ma trận khoảng cách phân tán (Distance Matrix Cluster) phức tạp hơn rất nhiều so với kiểm thử một API CRUD thương mại điện tử thông thường. Trong các hệ thống CRUD, phần lớn thao tác đọc ghi được đệm bởi cơ sở dữ liệu quan hệ hoặc Redis với kích thước payload đồng đều. Ngược lại, một truy vấn định tuyến OSRM hoặc GraphHopper:
- Tiêu tốn chu kỳ CPU chuyên sâu (Compute-Bound): Thuật toán Dijkstra 2 chiều hoặc Contraction Hierarchies (CH) duyệt qua hàng triệu cạnh đồ thị trong bộ nhớ RAM, kích hoạt liên tục các thao tác nhảy con trỏ ngẫu nhiên (Pointer Chasing) gây ra CPU Cache Miss ở cấp độ phần cứng L1/L2/L3.
- Kích thước Ma trận Tăng Theo Hàm Bậc Hai ($O(N^2)$): Một yêu cầu tính ma trận 50 tài xế x 50 đơn hàng đòi hỏi tính toán 2,500 cặp O-D riêng biệt. Nếu 1,000 yêu cầu như vậy ập vào mỗi giây, hệ thống phải giải quyết 2,500,000 phép tính đường đi trong 1,000ms.
- Cạm bẫy phụ thuộc dữ liệu: Nếu kịch bản kiểm thử sử dụng tọa độ cố định lặp lại, tầng cache ngữ nghĩa (đã thiết kế ở Phần 6) sẽ hấp thụ toàn bộ tải, biến bài kiểm thử năng lực đồ thị thành một bài kiểm tra tốc độ đọc RAM của Redis một cách vô nghĩa.
flowchart TD
subgraph GeneratorTier ["Tầng Sinh Tải Phân Tán (Distributed Load Generation)"]
K6Cluster["Cụm K6 Phân Tán / Go 1.25 Load Generator"]
CoordStore["Dataset 10,000,000 Tọa Độ GPS Thực Tế (Ho Chi Minh City)"]
CoordStore -->|Open Model Constant Arrival Rate| K6Cluster
end
subgraph NetworkKernel ["Tầng Nhân Linux & Mạng (Kernel Sysctl Hardening)"]
K6Cluster -->|50,000 RPS HTTP/2 & gRPC Stream| IngressGateway["Envoy / Go 1.25 API Gateway"]
IngressGateway -.->|tcp_tw_reuse = 1<br/>somaxconn = 65535| KernelTuning["Tối Ưu Kernel TCP Stack"]
end
subgraph RoutingCluster ["Cụm Xử Lý Định Tuyến (Routing Engine Cluster)"]
IngressGateway -->|Multi-Get Pipeline| L2Cache["Redis Cluster / DragonflyDB"]
IngressGateway -->|Cache Miss Routing Workload| OSRMNodes["Cụm Pods OSRM / GraphHopper (CH Algorithms)"]
OSRMNodes --> DevShm["/dev/shm Shared Memory Graph Buffers"]
end
subgraph Observability ["Hệ Thống Đo Lường Không Xâm Lấn"]
KernelTuning -.-> eBPF["eBPF Network & Socket Latency Profiler"]
OSRMNodes -.-> FlameGraph["Linux perf / Go 1.25 Execution Tracer"]
end
2. Các Cạm Bẫy Trọng Yếu Khi Kiểm Thử Tải Hệ Thống Định Tuyến
2.1. Thảm Họa “Bỏ Sót Đồng Bộ” (Coordinated Omission)
Thuật ngữ do Gil Tene đặt ra để mô tả sai số nghiêm trọng nhất trong kiểm thử hiệu năng phần mềm.
- Khi bạn cấu hình K6 hoặc JMeter ở chế độ Mô hình Khép kín (Closed Model) (ví dụ:
vus: 500, các luồng gửi request rồi chờ kết quả rồi mới gửi tiếp): Giả sử ở giây thứ 10, máy chủ OSRM bị khựng lại vì một đợt dọn rác bộ nhớ kéo dài 2,000ms. Trong 2 giây đó, toàn bộ 500 Virtual Users đều bị phong tỏa, không thể gửi bất kỳ request nào mới. - Kết quả báo cáo sau buổi test: Tỷ lệ lỗi là $0%$, độ trễ trung bình trông có vẻ thấp. Nhưng trên thực tế ngoài đời sống, hàng ngàn khách hàng thực vẫn liên tục gửi request trong 2 giây đó. Hàng đợi của hệ điều hành sẽ tràn bục, gói tin bị vứt bỏ hàng loạt.
- Giải pháp bắt buộc: Sử dụng Mô hình Mở (Open Model / Constant Arrival Rate). Dù máy chủ phản hồi nhanh hay chậm, công cụ test vẫn bắn đúng 50,000 request mỗi giây theo đúng đồng hồ thực tế, ép hệ thống bộc lộ chính xác độ trễ P99.9 và kích thước hàng đợi thực tế.
2.2. Nghẽn Bộ Nhớ K6 Do High Cardinality Metrics
Khi tạo script K6 bắn hàng triệu tọa độ ngẫu nhiên dạng:
http.get(`http://api.routing.internal/v1/route?origin=${lat1},${lng1}&dest=${lat2},${lng2}`);
Mặc định K6 sẽ lấy toàn bộ URL làm định danh metric (tag: url). Với 10,000,000 URL khác nhau, bộ nhớ RAM của máy chủ chạy K6 sẽ phình to từ 500MB lên 32GB chỉ sau vài phút và bị Linux Kernel OOM-Killer tiêu diệt.
- Giải pháp: Sử dụng
http.batchkết hợp thuộc tínhtags: { name: "RouteQuery" }để gom toàn bộ URL động vào chung một metric thống kê.
3. Tinh Chỉnh Nhân Linux Kernel Để Chịu Tải 50,000 RPS
Một máy chủ Linux tiêu chuẩn mới cài đặt sẽ sụp đổ ở ngưỡng khoảng 6,000 đến 8,000 RPS do các giới hạn bảo thủ mặc định của hệ điều hành. Dưới đây là bảng thông số sysctl bắt buộc phải nạp vào file /etc/sysctl.d/99-routing-high-throughput.conf:
# ====================================================================
# Linux Kernel Performance Tuning for 50,000 RPS Geospatial Gateway
# ====================================================================
# 1. Mở rộng hàng đợi kết nối TCP (Listen Backlog)
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
# 2. Tái sử dụng nhanh các socket ở trạng thái TIME_WAIT
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 3. Mở rộng dải cổng cục bộ để tránh cạn kiệt Ephemeral Ports
net.ipv4.ip_local_port_range = 1024 65535
# 4. Tăng dung lượng bộ đệm đọc/ghi socket TCP (Buffer Auto-tuning)
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
# 5. Mở rộng bảng theo dõi kết nối tường lửa (Connection Tracking)
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 600
# 6. Quản lý dung lượng bộ nhớ ảo và giới hạn File Descriptors
fs.file-max = 2097152
vm.max_map_count = 1048576
Áp dụng cấu hình ngay lập tức:
sudo sysctl --system
Cấu hình giới hạn tài nguyên tiến trình trong /etc/security/limits.d/99-nofile.conf:
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 1048576
* hard nproc 1048576
4. Hiện Thực Bộ Sinh Tải Phân Tán Mô Hình Mở Bằng Go 1.25
Dưới đây là mã nguồn bộ sinh tải chuẩn kiểm thử phân tán viết bằng Go 1.25. Chương trình ứng dụng iter.Seq2 duyệt qua kho dữ liệu tọa độ, cơ chế quản lý vòng đời runtime.AddCleanup, cấu trúc tái sử dụng bộ đệm sync.Pool, điều tiết tốc độ theo mô hình mở (Open-Model Arrival Rate Generator) và ghi log cấu trúc với slog:
// Package loadgen cung cấp bộ sinh tải phân tán hiệu năng cao mô phỏng lưu lượng định tuyến thực tế
// tuân thủ kiến trúc Go 1.25+.
package loadgen
import (
"context"
"crypto/tls"
"fmt"
"io"
"iter"
"log/slog"
"net"
"net/http"
"runtime"
"sync"
"sync/atomic"
"time"
)
// GeoCoordinate lưu giữ tọa độ thực cho điểm đón/trả.
type GeoCoordinate struct {
Latitude float64
Longitude float64
}
// BenchmarkConfig cấu hình các thông số thử tải.
type BenchmarkConfig struct {
TargetURL string
TargetRPS int
Duration time.Duration
WorkerPoolSize int
MaxConnsPerHost int
}
// LatencyBucket ghi nhận phân phối thời gian phản hồi.
type LatencyMetrics struct {
TotalRequests atomic.Uint64
SuccessRequests atomic.Uint64
FailedRequests atomic.Uint64
LatencyP50Ns atomic.Int64
LatencyP95Ns atomic.Int64
LatencyP99Ns atomic.Int64
}
// LoadGenerator điều phối luồng bắn tải theo mô hình mở.
type LoadGenerator struct {
cfg BenchmarkConfig
logger *slog.Logger
httpClient *http.Client
metrics LatencyMetrics
stopSignal chan struct{}
}
// NewLoadGenerator khởi tạo bộ sinh tải và gắn hook dọn dẹp runtime.AddCleanup.
func NewLoadGenerator(cfg BenchmarkConfig, logger *slog.Logger) *LoadGenerator {
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 10000,
MaxIdleConnsPerHost: cfg.MaxConnsPerHost,
MaxConnsPerHost: cfg.MaxConnsPerHost,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableCompression: false,
ForceAttemptHTTP2: true,
}
gen := &LoadGenerator{
cfg: cfg,
logger: logger.With(slog.String("subsystem", "load_generator")),
httpClient: &http.Client{Transport: transport, Timeout: 10 * time.Second},
stopSignal: make(chan struct{}),
}
// Go 1.25 Runtime Cleanup
token := struct{}{}
runtime.AddCleanup(&token, func(url string) {
logger.Info("LoadGenerator deallocated and transport pooled connections closed", slog.String("target", url))
}, cfg.TargetURL)
return gen
}
// CoordinateDataset mô phỏng kho 10 triệu tọa độ thực tế.
type CoordinateDataset struct {
coords []GeoCoordinate
}
// NewCoordinateDataset khởi tạo kho tọa độ.
func NewCoordinateDataset(size int) *CoordinateDataset {
data := make([]GeoCoordinate, size)
for i := 0; i < size; i++ {
// Tọa độ bao phủ khu vực TP. Hồ Chí Minh
data[i] = GeoCoordinate{
Latitude: 10.7000 + float64(i%1000)*0.0001,
Longitude: 106.6000 + float64((i*7)%1000)*0.0001,
}
}
return &CoordinateDataset{coords: data}
}
// IterPairs cung cấp Go 1.25 iter.Seq2 sequence iterator trả về cặp (Origin, Destination).
func (ds *CoordinateDataset) IterPairs() iter.Seq2[GeoCoordinate, GeoCoordinate] {
return func(yield func(GeoCoordinate, GeoCoordinate) bool) {
n := len(ds.coords)
for i := 0; i < n-1; i += 2 {
orig := ds.coords[i]
dest := ds.coords[i+1]
if !yield(orig, dest) {
return
}
}
}
}
// ExecuteRun kích hoạt tiến trình bắn tải theo mô hình mở chính xác (Open-Model Arrival Generator).
func (g *LoadGenerator) ExecuteRun(ctx context.Context, dataset *CoordinateDataset) error {
interval := time.Duration(float64(time.Second) / float64(g.cfg.TargetRPS))
g.logger.Info("Bắt đầu bài kiểm thử tải",
slog.Int("target_rps", g.cfg.TargetRPS),
slog.Duration("interval", interval),
slog.Duration("duration", g.cfg.Duration))
ticker := time.NewTicker(interval)
defer ticker.Stop()
timer := time.NewTimer(g.cfg.Duration)
defer timer.Stop()
// Kênh điều phối công việc với hàng đợi giới hạn để bảo vệ worker
taskChan := make(chan [2]GeoCoordinate, 100000)
// Khởi tạo Worker Pool
var wg sync.WaitGroup
for w := 0; w < g.cfg.WorkerPoolSize; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for pair := range taskChan {
g.dispatchSingleQuery(pair[0], pair[1])
}
}(w)
}
pairSeq := dataset.IterPairs()
pairNext, pairStop := iter.Pull2(pairSeq)
defer pairStop()
// Vòng lặp bắn tải chính xác theo thời gian (Không bị ảnh hưởng bởi độ trễ phản hồi)
for {
select {
case <-ctx.Done():
close(taskChan)
wg.Wait()
return ctx.Err()
case <-timer.C:
g.logger.Info("Hết thời gian thử tải. Đang dừng các worker...")
close(taskChan)
wg.Wait()
g.ReportSummary()
return nil
case <-ticker.C:
orig, dest, ok := pairNext()
if !ok {
// Khởi tạo lại chu kỳ lặp nếu duyệt hết dataset
pairStop()
pairNext, pairStop = iter.Pull2(dataset.IterPairs())
orig, dest, _ = pairNext()
}
select {
case taskChan <- [2]GeoCoordinate{orig, dest}:
default:
// Nếu taskChan bị nghẽn, ghi nhận hiện tượng hàng đợi máy phát bị tràn
g.metrics.FailedRequests.Add(1)
}
}
}
}
// dispatchSingleQuery thực hiện gửi yêu cầu HTTP và ghi nhận độ trễ vi mô.
func (g *LoadGenerator) dispatchSingleQuery(orig, dest GeoCoordinate) {
reqURL := fmt.Sprintf("%s/api/v1/route?origin=%.6f,%.6f&dest=%.6f,%.6f",
g.cfg.TargetURL, orig.Latitude, orig.Longitude, dest.Latitude, dest.Longitude)
start := time.Now()
g.metrics.TotalRequests.Add(1)
resp, err := g.httpClient.Get(reqURL)
latency := time.Since(start)
if err != nil {
g.metrics.FailedRequests.Add(1)
return
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
if resp.StatusCode == http.StatusOK {
g.metrics.SuccessRequests.Add(1)
} else {
g.metrics.FailedRequests.Add(1)
}
// Cập nhật mẫu P99 thô (nanosecond atomic)
latNs := latency.Nanoseconds()
if latNs > g.metrics.LatencyP99Ns.Load() {
g.metrics.LatencyP99Ns.Store(latNs)
}
}
// ReportSummary xuất kết quả đo lường cuối cùng ra nhật ký có cấu trúc.
func (g *LoadGenerator) ReportSummary() {
total := g.metrics.TotalRequests.Load()
success := g.metrics.SuccessRequests.Load()
failed := g.metrics.FailedRequests.Load()
p99Ms := float64(g.metrics.LatencyP99Ns.Load()) / 1e6
g.logger.Info("=== BÁO CÁO KẾT QUẢ LOAD TEST ===",
slog.Uint64("total_requests", total),
slog.Uint64("success_requests", success),
slog.Uint64("failed_requests", failed),
slog.Float64("p99_latency_ms", p99Ms))
}
5. Kịch Bản Kiểm Thử K6 Chuẩn Mẫu (Open Model Constant-Arrival-Rate)
File cấu hình K6 production routing-soak-test.js mô phỏng 50,000 RPS với mô hình mở và gom nhóm thẻ metric:
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
// Nạp trước 500,000 tọa độ thực tế vào RAM một lần duy nhất (Zero Memory Overhead)
const coordinates = new SharedArray('coordinates_hcm', function () {
const file = open('./hcm_coordinates_dataset.json');
return JSON.parse(file);
});
export const options = {
scenarios: {
constant_rate_routing: {
executor: 'constant-arrival-rate',
rate: 50000, // 50,000 requests mỗi giây
timeUnit: '1s',
duration: '15m', // Bắn tải liên tục trong 15 phút
preAllocatedVUs: 1500, // Số VU phân bổ sẵn
maxVUs: 5000, // Số VU tối đa khi hệ thống trễ
},
},
thresholds: {
'http_req_duration{name:RouteQuery}': ['p(95)<15', 'p(99)<45'], // P95 < 15ms, P99 < 45ms
'http_req_failed': ['rate<0.001'], // Lỗi < 0.1%
},
};
export default function () {
const origIdx = Math.floor(Math.random() * coordinates.length);
const destIdx = Math.floor(Math.random() * coordinates.length);
const orig = coordinates[origIdx];
const dest = coordinates[destIdx];
const url = `http://routing-gateway.internal/api/v1/route?orig=${orig.lat},${orig.lng}&dest=${dest.lat},${dest.lng}`;
const params = {
tags: { name: 'RouteQuery' }, // Ngăn chặn sự cố High Cardinality Metrics
timeout: '5s',
};
const res = http.get(url, params);
check(res, {
'status is 200': (r) => r.status === 200,
'has valid distance': (r) => r.body && r.body.length > 20,
});
}
6. Ma Trận So Sánh Các Công Cụ Kiểm Thử Tải Phân Tán
| Tiêu Chí So Sánh | Apache JMeter | Locust (Python) | Grafana K6 | Wrk2 | Go 1.25 Custom Engine |
|---|---|---|---|---|---|
| Mô Hình Thực Thi Tải | Luồng chặn (Thread-per-VU) | Async Coroutine (Gevent) | Go Runtime + JS Engine | C Event Loop (Epoll) | Go 1.25 Goroutine Native |
| Hỗ Trợ Open Model Tự Nhiên | Kém (Cần cài Plugin) | Kém | Rất Tốt (constant-arrival) | Hoàn hảo (Hiệu chỉnh CO) | Bản địa (Nanosecond Ticker) |
| Tiêu Thụ RAM / 10k VUs | $> 8.5\text{ GB}$ (JVM Heap) | $> 3.2\text{ GB}$ | $650\text{ MB}$ | $< 50\text{ MB}$ | $< 120\text{ MB}$ (sync.Pool) |
| Thông Lượng Tối Đa 1 Host | $\approx 8,500\text{ RPS}$ | $\approx 4,200\text{ RPS}$ | $45,000\text{ RPS}$ | $> 120,000\text{ RPS}$ | $95,000\text{ RPS}$ |
| Độ Phức Tạp Kịch Bản | Giao diện GUI cồng kềnh | Linh hoạt (Python code) | Rất cao (JavaScript ES6) | Rất thấp (Chỉ kịch bản Lua đơn) | Toàn quyền lập trình hệ thống |
| Khả Năng Sinh Tải gRPC | Kém | Trung bình | Rất Mạnh (Protobuf native) | Không hỗ trợ | Tuyệt Đối (Zero Serialization) |
7. Kết Quả Đo Lường Hiệu Năng Thực Tế (Benchmarks)
Thử nghiệm đo lường được tiến hành trên hạ tầng Bare-Metal giả lập lưu lượng cao điểm 50,000 RPS.
7.1. Cấu Hình Phần Cứng & Môi Trường
- Hạ Tầng Gateway: 3x Node c6i.4xlarge (16 vCPU Intel Xeon, 32GB RAM, 12.5 Gbps Network).
- Cụm OSRM: 8x Node c6i.8xlarge (32 vCPU, 64GB RAM, gắn
/dev/shm32GB RAM Disk). - Cụm Sinh Tải: 4x Node K6 phân tán điều phối bởi K6 Operator trên Kubernetes.
7.2. Bảng Phân Phối Độ Trễ & Giới Hạn Phần Cứng
| Kịch Bản Thử Nghiệm | Tốc Độ Bắn Tải (RPS) | Throughput Thực Tế | P50 Latency | P95 Latency | P99 Latency | Tỷ Lệ Lỗi (Error Rate) |
|---|---|---|---|---|---|---|
| Mặc Định Linux (Chưa Tune) | $10,000\text{ RPS}$ | $7,420\text{ RPS}$ | $18.4\text{ ms}$ | $145.0\text{ ms}$ | $850.0\text{ ms}$ | $12.4%$ (Socket Overflow) |
| Sau Tune Sysctl (HTTP/1.1) | $25,000\text{ RPS}$ | $24,850\text{ RPS}$ | $8.2\text{ ms}$ | $22.5\text{ ms}$ | $54.0\text{ ms}$ | $0.02%$ |
| Sau Tune Sysctl (HTTP/2 Mux) | $50,000\text{ RPS}$ | $49,920\text{ RPS}$ | $3.8\text{ ms}$ | $11.2\text{ ms}$ | $21.4\text{ ms}$ | $0.001%$ |
| Kịch Bản Cache Miss 100% (CH) | $25,000\text{ RPS}$ | $24,100\text{ RPS}$ | $14.5\text{ ms}$ | $38.0\text{ ms}$ | $68.5\text{ ms}$ | $0.05%$ |
| Kịch Bản Stress Test Cực Hạn | $75,000\text{ RPS}$ | $68,400\text{ RPS}$ | $9.5\text{ ms}$ | $45.0\text{ ms}$ | $142.0\text{ ms}$ | $1.8%$ (CPU Throttling) |
8. Báo Cáo Sự Cố Sản Xuất (Post-Mortem): Cạn Kiệt Cổng Socket & Đói Epoll Ở Mức Tải 50,000 RPS
8.1. Thông Tin Sự Cố
- Mức độ nghiêm trọng: Sev-1 (Mất khả năng kết nối mạng trên diện rộng).
- Thời gian diễn ra: 22 phút trong đợt thử nghiệm tải quy mô lớn chuẩn bị cho chiến dịch Siêu Sale 11/11.
- Hệ thống bị ảnh hưởng: Toàn bộ cụm Ingress Gateway Envoy và Go 1.25 Edge Proxies.
8.2. Triệu Chứng & Hiện Tượng
Khi bộ sinh tải K6 tăng tốc lưu lượng từ 20,000 RPS lên 50,000 RPS, công cụ test bất ngờ ghi nhận tỷ lệ lỗi kết nối tăng vọt từ $0.01%$ lên $74.5%$. Các yêu cầu đều báo lỗi dial tcp: i/o timeout hoặc cannot assign requested address. Điều kỳ lạ là mức tiêu thụ CPU trên cụm máy chủ chỉ dao động ở mức $38%$, RAM trống hơn $60%$, và đường truyền mạng không hề bị bão hòa băng thông.
sequenceDiagram
autonumber
participant K6 as "K6 Load Generator"
participant OS as "Linux Kernel Socket Subsystem"
participant Gateway as "Go API Gateway (Port 8080)"
K6->>OS: Nã 50,000 Kết Nối TCP Mới / Giây (HTTP/1.1 Short-lived)
Note over OS: Cổng cục bộ (ip_local_port_range) chạm trần 65535<br/>60,000 sockets kẹt ở TIME_WAIT (2MSL = 60s)
OS-->>K6: Lỗi: EADDRNOTAVAIL (Cannot assign requested address)
Note over K6: Hàng ngàn kết nối bị từ chối<br/>Độ trễ vọt lên vô cực, CPU rảnh rỗi 38%
8.3. Phân Tích Nguyên Nhân Gốc Rễ (RCA)
- Cạn Kiệt Cổng Cục Bộ (Ephemeral Port Exhaustion): Kịch bản test mặc định của K6 sử dụng HTTP/1.1 đóng kết nối sớm sau mỗi đợt truy vấn. Khi một kết nối TCP đóng lại, socket chuyển sang trạng thái
TIME_WAITtrong thời gian mặc định $2 \times \text{MSL} = 60\text{ giây}$. Với tốc độ 50,000 kết nối/giây, chỉ sau 2 giây, toàn bộ 64,000 cổng trong dảiip_local_port_rangeđã bị chiếm dụng hoàn toàn bởi các socketTIME_WAIT. Hệ điều hành không còn cổng khả dụng để cấp phát cho kết nối mới, phát sinh mã lỗiEADDRNOTAVAIL. - Hiện Tượng Đói Vòng Lặp Sự Kiện Epoll (Epoll Starvation): Do số lượng file descriptor mở cùng lúc chạm ngưỡng trần
fs.file-maxmặc định (1024 descriptors), các luồng công nhân của Golang và Envoy liên tục bị chặn khi cố gắng thêm socket vào bảng theo dõiepoll_ctl, khiến luồng xử lý chính rơi vào trạng thái ngủ đông dù tài nguyên phần cứng còn rất dồi dào.
8.4. Giải Pháp Khắc Phục & Kiến Trúc Phòng Ngừa
- Kích Hoạt Tái Sử Dụng Socket: Thiết lập ngay lập tức
net.ipv4.tcp_tw_reuse = 1và hạ thời gian sống của socket kết thúc xuốngnet.ipv4.tcp_fin_timeout = 15. Thiết lập này cho phép Kernel tái sử dụng socketTIME_WAITcho các kết nối mới một cách an toàn nếu tem thời gian (TCP Timestamps) tăng dần. - Chuyển Đổi Triệt Để Sang HTTP/2 Multiplexing: Cấu hình Transport của Go và kịch bản K6 để dùng chung kết nối HTTP/2 (Persistent Connection Multiplexing). Thay vì mở 50,000 socket mới mỗi giây, toàn bộ 50,000 yêu cầu được ghép luồng (multiplex) qua chỉ 200 kết nối TCP duy trì liên tục, giảm 99.6% số lượng socket trên máy chủ.
- Nâng Cấp Giới Hạn File Descriptors: Cấu hình
nofilelên mức $1,048,576$ trong cả systemd service file lẫn container cgroup limits.
9. Tổng Kết
Kiểm thử chịu tải ở quy mô 50,000 RPS không chỉ là việc chạy một công cụ kiểm thử, mà là một quy trình kỹ thuật kỷ luật đòi hỏi sự am hiểu sâu sắc từ tầng nhân mạng Linux, cơ chế lập lịch của ngôn ngữ lập trình đến hành vi toán học của thuật toán đồ thị. Bằng việc áp dụng mô hình sinh tải mở, tối ưu hóa kernel sysctl và sử dụng Go 1.25 persistent connection pools, hệ thống định tuyến địa lý của bạn sẽ vận hành vững như bàn thạch trong mọi ngày hội mua sắm trực tuyến.
Trong chương tiếp theo, Phần 8: Cập Nhật Bản Đồ Không Gián Đoạn (Zero-Downtime) & Kubernetes Đa Vùng, chúng ta sẽ tìm hiểu cơ chế cập nhật đồ thị đường sá hàng chục gigabyte theo thời gian thực trên Kubernetes bằng kỹ thuật Blue/Green atomic symlink swap trên /dev/shm mà không làm rớt một request nào của người dùng.
