Vấn đề thực tế: Điểm nghẽn vô hình tại mốc 10.000 kết nối đồng thời
Biểu đồ giám sát của bạn trông hoàn toàn bình thường. Mức sử dụng CPU duy trì ổn định ở 35%, RAM chỉ tiêu tốn khoảng 4GB trên tổng số 16GB, và độ trễ ổ đĩa NVMe luôn dưới 1ms. Mặc dù tài nguyên hệ thống còn rất dư dả, người dùng lại liên tục phàn nàn về tốc độ phản hồi chậm chạp, trong khi Nginx thỉnh thoảng lại nhả ra các mã lỗi 502 Bad Gateway và 504 Gateway Timeout trong những đợt truy cập tăng đột biến.
Kiểm tra nhanh qua dmesg hoặc /var/log/syslog sẽ chỉ ra ngay thủ phạm thực sự:
TCP: request_sock_TCP: Possible SYN flooding on port 443. Dropping request.
TCP: net_ratelimit: 893 callbacks suppressed
Chạy lệnh netstat -s xác nhận rằng network stack của bạn đang quá tải dưới áp lực lớn:
$ netstat -s | grep -i listen
14201 times the listen queue of a socket overflowed
18450 SYNs to LISTEN sockets dropped
Mã nguồn ứng dụng của bạn không phải là nguyên nhân gây nghẽn ở đây. Chính Linux kernel đã tự động loại bỏ các bắt tay TCP hợp lệ gửi đến trước khi Nginx hoặc runtime kịp nhận được socket descriptor.
Phân tích nguyên nhân gốc rễ: Vì sao cấu hình mặc định của Linux lại nghẽn khi tải cao
Các thông số mặc định của kernel được thiết kế nhằm mục đích tương thích rộng rãi trên mọi thiết bị, từ các bo mạch nhúng nhỏ gọn cho đến các máy ảo cấu hình thấp. Chúng giúp bảo vệ các hệ thống cấu hình yếu không bị crash do hết bộ nhớ (OOM). Đáng tiếc là, những ngưỡng an toàn quá mức này lại trở thành rào cản bóp nghẹt các dịch vụ web thông lượng cao vốn phải xử lý hàng nghìn request mỗi giây.
1. Hàng đợi Connection Backlog quá nhỏ
Mỗi kết nối TCP đều bắt đầu bằng quy trình bắt tay ba bước (three-way handshake): SYN → SYN-ACK → ACK. Kernel chia quy trình tiếp nhận này thành hai hàng đợi nội bộ riêng biệt:
- Hàng đợi SYN Backlog: Theo dõi các kết nối bán mở (half-open connections), nơi client đã gửi gói
SYNvà đang chờ xác nhận. Kernel mặc định thường giới hạnnet.ipv4.tcp_max_syn_backlogở mức 128 hoặc 256 mục. - Hàng đợi Listen Accept: Lưu trữ các kết nối đã hoàn tất bắt tay và đang chờ một tiến trình ứng dụng (chẳng hạn như Nginx, Node.js hoặc Go) gọi hàm
accept(). Được quản lý bởinet.core.somaxconn, hàng đợi này thường có giá trị mặc định là 128 hoặc 4096.
Một đợt flash sale hoặc lượng truy cập tăng vọt bất ngờ có thể làm tràn hàng đợi 128 slot này chỉ trong vài mili-giây. Một khi đã đầy, kernel sẽ âm thầm loại bỏ các gói SYN gửi đến. Phía client sẽ bị treo, phải chờ vài giây cho đến khi hết thời gian chờ gửi lại TCP (retransmission timeout), và thường sẽ bỏ ngang request.
2. Cạn kiệt Ephemeral Port và vòng đời kết nối biến động nhanh
Reverse proxy và microservice thường xuyên mở lượng lớn kết nối outbound tới database cùng các dịch vụ nội bộ. Khi một phiên TCP kết thúc bình thường, socket phía client sẽ chuyển sang trạng thái TIME_WAIT trong thời gian cố định 60 giây để hứng các gói tin đến trễ. Ngoài ra, các socket mồ côi có thể bị kẹt ở trạng thái FIN-WAIT-2 nếu bên nhận phía xa không gửi gói tin FIN đóng kết nối.
Linux mặc định dải port tạm thời (ephemeral port – net.ipv4.ip_local_port_range) từ 32768–60999. Khoảng này chỉ cung cấp xấp xỉ 28.231 port khả dụng cho mỗi địa chỉ IP đích. Nếu proxy của bạn xử lý liên tục 500 request/giây tới một endpoint backend duy nhất, số lượng port này sẽ cạn kiệt chỉ trong chưa đầy một phút. Các nỗ lực kết nối tiếp theo sẽ thất bại ngay lập tức với lỗi EADDRNOTAVAIL (Cannot assign requested address).
3. Kích thước TCP Window Buffer quá nhỏ
Thông lượng dữ liệu truyền qua các mạng tốc độ cao hoặc diện rộng phụ thuộc vào BDP (Bandwidth-Delay Product – Tích số Băng thông và Độ trễ):
BDP (Bytes) = Link Speed (Bytes/giây) × Round Trip Time (Giây)
Xét một đường truyền 10 Gbps kết nối các server giữa Frankfurt và Ashburn với thời gian khứ hồi (RTT) là 75ms. Để tận dụng tối đa đường truyền này, lượng buffer in-flight cần thiết là:
(10,000,000,000 / 8) × 0.075 = 93,750,000 bytes (~89.4 MB)
Buffer nhận mặc định của Linux thường dừng lại ở mức 4MB đến 6MB. Nếu TCP window của bạn không thể chứa trọn vẹn giá trị BDP, phía gửi bắt buộc phải dừng truyền dữ liệu và chờ các gói ACK. Kết quả là đường truyền 10 Gbps đắt đỏ của bạn hoạt động lãng phí dưới 15% năng lực thực tế.
4. Thuật toán kiểm soát nghẽn dựa trên mất mát gói tin (Cubic)
Hầu hết các bản phân phối Linux đều sử dụng Cubic làm mặc định. Cubic giả định rằng bất kỳ gói tin nào bị rớt đều là dấu hiệu của việc nghẽn buffer vật lý trên các thiết bị chuyển mạch (switch). Do đó, nó lập tức giảm kích thước cửa sổ nghẽn (congestion window) xuống một nửa.
Giả định này không còn hoàn toàn chính xác trên hạ tầng mạng hiện đại. Trên các kết nối di động chập chờn, mạng Wi-Fi hoặc mạng đám mây dùng chung (multi-tenant) đông đúc, các gói tin thường xuyên bị mất do nhiễu tạm thời chứ không hẳn vì tràn buffer. Cubic vô tình bóp nghẹt tốc độ truyền dữ liệu không cần thiết, gây ra các đợt tăng vọt độ trễ thất thường.
Đánh giá các giải pháp thực tế: Sai lầm phổ biến vs. Tinh chỉnh chuẩn Production
Các nhóm kỹ sư khi đối mặt với những giới hạn này thường chọn một trong ba hướng đi sau:
- Giải quyết bằng cách nâng cấp phần cứng: Tăng gấp đôi vCPU và gắn thêm RAM cũng không thể khắc phục tình trạng cạn kiệt hàng đợi ở tầng kernel. Một máy chủ vật lý với 128 core và 512GB RAM vẫn sẽ tiếp tục làm rơi kết nối nếu
net.core.somaxconnvẫn bị ghim cứng ở mức 128. - Copy-paste cấu hình vô tội vạ: Việc sử dụng lại các đoạn mã lỗi thời từ các diễn đàn dễ để lại những tác dụng phụ nghiêm trọng. Ví dụ, việc bật
net.ipv4.tcp_tw_recycleđòi hỏi TCP timestamp phải chính xác đến từng micro-giây. Bất kỳ client nào nằm sau router NAT dùng chung trong doanh nghiệp đều sẽ thất bại khi kiểm tra bắt tay, khiến toàn bộ văn phòng bị chặn truy cập. Linux kernel đã loại bỏ hoàn toàn thiết lập này từ phiên bản 4.12. - Tinh chỉnh Kernel bài bản và có hệ thống: Định kích thước hàng đợi dựa trên lưu lượng đột biến dự kiến, điều chỉnh các vector bộ nhớ TCP phù hợp với lượng RAM vật lý, và chuyển sang thuật toán kiểm soát nghẽn dựa trên điểm nghẽn thực tế (bottleneck-based).
Hướng dẫn từng bước tinh chỉnh hệ thống Production
Hãy làm theo các bước dưới đây để cấu hình các máy chủ Linux production của bạn đạt thông lượng ổn định ngay cả trong những đợt bùng nổ truy cập lớn.
Bước 1: Kiểm tra các giới hạn Kernel hiện tại
Kiểm tra cấu hình hiện tại của hệ thống trước khi bắt đầu thay đổi các giá trị:
# Kiểm tra giới hạn hàng đợi kết nối
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
# Kiểm tra các module kiểm soát nghẽn khả dụng và đang hoạt động
sysctl net.ipv4.tcp_available_congestion_control
sysctl net.ipv4.tcp_congestion_control
Bước 2: Mở rộng Socket Queue và Network Ring Buffer
Mở rộng cả hai hàng đợi backlog để các đợt tải đột biến được đệm an toàn trong bộ nhớ thay vì bị drop ngay lập tức:
# Mở rộng hàng đợi listen accept cho các socket đã thiết lập
sudo sysctl -w net.core.somaxconn=65535
# Mở rộng hàng đợi SYN backlog cho các bắt tay bán mở
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535
# Tăng hàng đợi gói tin đầu vào trên network interface driver
sudo sysctl -w net.core.netdev_max_backlog=65535
Bước 3: Ngăn ngừa cạn kiệt Port và thu hồi các Socket cũ
Mở rộng dải ephemeral port và cho phép tái sử dụng socket an toàn đối với các kết nối outbound:
# Mở toàn bộ dải ephemeral port (cung cấp hơn 55.000 port cục bộ)
sudo sysctl -w net.ipv4.ip_local_port_range="10240 65535"
# Tái sử dụng socket TIME_WAIT an toàn cho kết nối đi khi timestamp giao thức khớp nhau
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# Thu hồi các kết nối FIN-WAIT-2 mồ côi sau 15 giây thay vì 60 giây
sudo sysctl -w net.ipv4.tcp_fin_timeout=15
Bước 4: Mở rộng kích thước TCP Window và Memory Vector
Linux quản lý buffer socket thông qua các vector gồm ba giá trị: [min, default, max] tính bằng byte. Hệ thống sẽ tự động cấp phát không gian buffer dựa trên nhu cầu thực tế nhưng luôn nằm trong giới hạn này.
Đối với hệ thống có RAM từ 8GB đến 16GB, hãy cấp phát tối đa 16MB hoặc 32MB cho mỗi socket đối với các kết nối yêu cầu thông lượng cao:
# Đặt giới hạn tối đa cho buffer nhận và gửi của hệ điều hành (32MB)
sudo sysctl -w net.core.rmem_max=33554432
sudo sysctl -w net.core.wmem_max=33554432
# Đặt buffer mặc định ở mức 256KB
sudo sysctl -w net.core.rmem_default=262144
sudo sysctl -w net.core.wmem_default=262144
# Bộ nhớ đọc TCP: [tối thiểu 4KB, mặc định 128KB, tối đa 32MB]
sudo sysctl -w net.ipv4.tcp_rmem="4096 131072 33554432"
# Bộ nhớ ghi TCP: [tối thiểu 4KB, mặc định 128KB, tối đa 32MB]
sudo sysctl -w net.ipv4.tcp_wmem="4096 131072 33554432"
Bước 5: Kích hoạt thuật toán kiểm soát nghẽn Google BBR
Google đã phát triển BBR (Bottleneck Bandwidth and Round-trip propagation time) nhằm vượt qua các hạn chế của thuật toán kiểm soát nghẽn dựa trên mất mát gói tin. Thay vì coi mất gói là dấu hiệu tắc nghẽn, BBR liên tục đo đạc băng thông thực tế của đường truyền và độ trễ khứ hồi. Nó truyền tải đúng bằng dung lượng vật lý của đường truyền, giúp giữ sạch hàng đợi switch và giảm thiểu độ trễ đệm.
BBR yêu cầu packet pacing scheduler dạng Fair Queueing (fq), được hỗ trợ sẵn từ Linux kernel 4.9 trở lên:
# Kích hoạt Fair Queueing scheduler
sudo sysctl -w net.core.default_qdisc=fq
# Chuyển thuật toán kiểm soát nghẽn TCP sang BBR
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
Kiểm tra lại để đảm bảo kernel đã nạp module BBR thành công:
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = bbr
$ lsmod | grep bbr
tcp_bbr 20480 16
Bước 6: Lưu cấu hình vĩnh viễn qua các lần reboot
Thực thi lệnh sysctl -w chỉ thay đổi cấu hình tạm thời trên RAM. Để áp dụng vĩnh viễn, hãy gom chúng vào một file cấu hình riêng biệt trong thư mục /etc/sysctl.d/:
cat << 'EOF' | sudo tee /etc/sysctl.d/99-network-tuning.conf
# Hàng đợi kết nối
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
# Ephemeral Port & Tái sử dụng Socket
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# Tự động điều chỉnh TCP Buffer (Phù hợp cho RAM 8GB-16GB)
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.ipv4.tcp_rmem = 4096 131072 33554432
net.ipv4.tcp_wmem = 4096 131072 33554432
# Pacing & Kiểm soát nghẽn BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
# Áp dụng cấu hình ngay lập tức từ ổ đĩa
sudo sysctl --system
Bước 7: Đồng bộ Backlog của Web Server và Ứng dụng
Hàng đợi accept của kernel luôn bị giới hạn bởi giá trị nhỏ hơn giữa: net.core.somaxconn hoặc tham số backlog truyền vào system call listen() của ứng dụng. Nếu Nginx cấu hình nhận 511 kết nối trong khi kernel cho phép 65.535, thì giới hạn hiệu lực vẫn chỉ dừng lại ở con số 511.
Hãy cập nhật cấu hình virtual host Nginx của bạn trong /etc/nginx/nginx.conf để tận dụng triệt để dung lượng hàng đợi mới:
server {
listen 80 backlog=65535;
listen 443 ssl http2 backlog=65535;
server_name api.example.com;
# ...
}
Reload lại Nginx để áp dụng cấu hình mới mà không làm gián đoạn các phiên kết nối đang hoạt động:
sudo nginx -t && sudo systemctl reload nginx
Kiểm tra và giám sát trạng thái theo thời gian thực
Sử dụng công cụ thống kê socket (ss) để xác nhận dịch vụ của bạn đang chạy với không gian hàng đợi đã được mở rộng:
# Kiểm tra các listening TCP socket cho HTTP và HTTPS
ss -lnt '( sport = :http or sport = :https )'
Hãy chú ý kỹ các cột kết quả. Đối với các socket đang ở trạng thái LISTEN, Send-Q biểu thị kích thước backlog tối đa, trong khi Recv-Q hiển thị số lượng kết nối đang xếp hàng chờ ứng dụng gọi hàm accept():
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 65535 0.0.0.0:443 0.0.0.0:*
LISTEN 0 65535 0.0.0.0:80 0.0.0.0:*
Khi Send-Q đã hiển thị giá trị 65535, máy chủ của bạn đã sẵn sàng hấp thụ các đợt lưu lượng kết nối đồng thời cực lớn một cách mượt mà, không còn lo rơi rớt gói tin hay xuất hiện các lỗi gateway chập chờn.
