Khi một hệ thống máy chủ Web, cổng API Gateway, cụm Reverse Proxy Nginx/HAProxy hoặc máy chủ truyền thông thời gian thực (WebSocket / gRPC Streaming) bước vào giai đoạn mở rộng quy mô lớn, đội ngũ kỹ sư hạ tầng (Systems & SRE Engineers) thường chạm trán một bức tường giới hạn kỳ lạ: dù biểu đồ giám sát phần cứng cho thấy mức sử dụng CPU mới chỉ ở ngưỡng 15%, dung lượng bộ nhớ RAM vật lý còn trống hàng chục Gigabyte và băng thông card mạng 10GbE/25GbE chưa dùng hết một phần tư công suất, nhưng khách hàng bên ngoài lại liên tục gặp lỗi Connection Reset by Peer, Connection Timed Out hoặc độ trễ thiết lập kết nối tăng vọt lên hàng giây. Trong nhật ký hệ thống (dmesg và journalctl), hàng loạt cảnh báo cấp độ nhân hệ điều hành xuất hiện dày đặc như too many open files, TCP: request_sock_TCP: Possible SYN flooding on port 443. Sending cookies hay nf_conntrack: table full, dropping packet.
Nguyên nhân gốc rễ của hiện tượng này không nằm ở mã nguồn ứng dụng hay năng lực phần cứng, mà nằm ở các thông số cấu hình mặc định của Nhân hệ điều hành Linux (Linux Kernel). Các bản phân phối Linux máy chủ tiêu chuẩn (Ubuntu Server, Debian, RHEL, Rocky Linux) được thiết lập mặc định theo hướng bảo toàn tài nguyên cho mục đích sử dụng đa dụng thay vì tối ưu hóa cho bài toán C100K / C1M (xử lý đồng thời từ 100.000 đến hơn 1.000.000 kết nối TCP trên một máy chủ vật lý hoặc máy ảo duy nhất). Bài viết kỹ thuật chuyên sâu này giải phẫu toàn diện đường đi của gói tin mạng bên trong không gian nhân Linux (Kernel Space) và cung cấp quy trình tuning Linux Kernel cho máy chủ Web triệu kết nối thông qua việc mở rộng giới hạn File Descriptors (VFS & systemd), tinh chỉnh hàng đợi bắt tay TCP (SYN Queue, Accept Queue, somaxconn), quản lý vòng đời trạng thái TIME_WAIT chuẩn IETF RFC 9293, tối ưu hóa bộ đệm Socket theo tích băng thông - độ trễ (BDP), kích hoạt thuật toán điều khiển tắc nghẽn TCP BBR của Google và kiểm chứng thực tế bằng bộ công cụ ss, nstat cùng cơ chế đa hợp sự kiện epoll.
- 1. Giải phẫu Bài toán C100K/C1M: Chuyện gì Xảy ra trong Linux Kernel khi Có Hàng Triệu Kết nối TCP?
- 1.1 Kiến trúc Mọi thứ đều là Tập tin (Everything is a File) của VFS và Giới hạn Bộ nhớ Mỗi Socket TCP
- 1.2 Sơ đồ Luồng Đi của Gói tin TCP qua Các Hàng đợi Kernel (NIC Ring Buffer, SYN Queue, Accept Queue)
- 1.3 Cơ chế Đa hợp Sự kiện I/O độ phức tạp O(1) với epoll so với select và poll
- 2. Mở rộng Giới hạn File Descriptors Toàn hệ thống (fs.file-max, limits.conf và systemd LimitNOFILE)
- 2.1 Phân biệt 3 Tầng Giới hạn File Descriptor: Toàn hệ thống (fs.file-max), Trần Tiến trình (fs.nr_open) và Phiên Người dùng (PAM limits)
- 2.2 Tại sao Dịch vụ systemd Phớt lờ /etc/security/limits.conf và Cấu hình Chuẩn LimitNOFILE=1048576
- 3. Tinh chỉnh Hàng đợi Kết nối TCP (somaxconn, netdev_max_backlog, tcp_max_syn_backlog và syn_cookies)
- 3.1 Giải quyết Hiện tượng Tràn Hàng đợi SYN (SYN Queue) và Hàng đợi Chấp nhận (Accept Queue / net.core.somaxconn)
- 3.2 Cơ chế Phòng thủ SYN Flood bằng net.ipv4.tcp_syncookies và Mở rộng Hàng đợi Thiết bị Mạng (netdev_max_backlog)
- 4. Tối ưu Vòng đời Socket: Quản lý Trạng thái TIME_WAIT (tcp_tw_reuse), tcp_fin_timeout và Ephemeral Ports
- 4.1 Bản chất Trạng thái TIME_WAIT (2MSL theo RFC 9293) và Rủi ro Cạn kiệt Cổng Tạm thời (Ephemeral Port Exhaustion)
- 4.2 Tinh chỉnh An toàn tcp_tw_reuse, tcp_fin_timeout và Tại sao Linux 4.12 Loại bỏ Hoàn toàn tcp_tw_recycle
- 5. Tinh chỉnh Bộ đệm Socket (TCP Buffer Sizing) và Kích hoạt Thuật toán Chống Tắc nghẽn TCP BBR
- 5.1 Tính toán Tích Băng thông - Độ trễ (Bandwidth-Delay Product - BDP) cho Bộ đệm Đọc/Ghi (tcp_rmem, tcp_wmem)
- 5.2 Chuyển đổi từ Thuật toán Dựa trên Mất gói (CUBIC) sang Thuật toán Dựa trên Mô hình Băng thông (TCP BBR + fq)
- 6. Kiểm tra Thông số bằng ss -s, nstat, sysctl và Mô phỏng Chịu tải với epoll
- 6.1 Kịch bản Chẩn đoán Trạng thái Hàng đợi TCP và Bộ đếm Rớt Gói Kernel (ListenOverflows, ListenDrops)
- 6.2 Xây dựng Trình Kiểm chứng Máy chủ Bất đồng bộ Dựa trên Linux epoll (Python select.epoll) cho Bài toán C100K
- 7. Tổng kết & Tài liệu tham khảo (References)
- 7.1 Bảng Đối chiếu Thông số Kernel Mặc định so với Cấu hình Production C100K/C1M
- 7.2 Tài liệu tham khảo & Nguồn đối chiếu Chính thống
1. Giải phẫu Bài toán C100K/C1M: Chuyện gì Xảy ra trong Linux Kernel khi Có Hàng Triệu Kết nối TCP?
Thuật ngữ C10K (10.000 kết nối đồng thời) từng là thách thức lớn của ngành kỹ thuật hệ thống vào đầu những năm 2000 khi các máy chủ web còn sử dụng mô hình cấp phát một tiến trình hoặc một luồng thực thi riêng biệt cho mỗi kết nối (Thread-per-Connection như Apache prefork). Ngày nay, với sự phổ biến của kiến trúc hướng sự kiện bất đồng bộ (Event-Driven Architecture) trong Nginx, Envoy, Go Runtime hay Node.js/Python asyncio, bài toán thực tế tại các doanh nghiệp công nghệ đã dịch chuyển lên quy mô C100K (100.000 kết nối) và C1M (1.000.000 kết nối đồng thời) trên mỗi nút mạng biên. Để tinh chỉnh chính xác từng biến số sysctl, trước hết chúng ta cần hiểu rõ cách thức nhân Linux cấp phát tài nguyên cho mỗi kết nối TCP từ khi khung tin Ethernet chạm vào cổng mạng vật lý cho đến khi tiến trình ứng dụng đọc được dữ liệu.
1.1 Kiến trúc Mọi thứ đều là Tập tin (Everything is a File) của VFS và Giới hạn Bộ nhớ Mỗi Socket TCP
Trong triết lý thiết kế của hệ điều hành UNIX và Linux, Hệ thống Tập tin Ảo (Virtual File System - VFS) trừu tượng hóa mọi tài nguyên nhập xuất — từ tệp tin trên ổ cứng NVMe, đường ống giao tiếp liên tiến trình (pipe), thiết bị phần cứng cho đến mỗi kết nối mạng TCP/UDP (Network Socket) — dưới một cấu trúc thống nhất gọi là Bộ mô tả Tập tin (File Descriptor - FD).
Mỗi khi một máy khách thiết lập thành công kết nối TCP tới cổng :443 của máy chủ Web (hoặc khi máy chủ Reverse Proxy mở một kết nối TCP chiều đi ra tới dịch vụ Upstream phía sau), nhân Linux phải thực hiện chuỗi cấp phát cấu trúc dữ liệu nội bộ trong vùng bộ nhớ nhân không thể hoán đổi (Non-swappable Kernel Slab Memory):
- Cấp phát cấu trúc
struct filevàstruct inodegiả lập trong VFS (sockfs): Gắn kết nối mạng với một chỉ số nguyên không âm (File Descriptor Index) trong bảng tệp mở của tiến trình (Process File Descriptor Table). - Cấp phát cấu trúc
struct socketvàstruct sock(tcp_sock): Lưu trữ toàn bộ trạng thái máy trạng thái hữu hạn của giao thức TCP (TCP State Machine), số thứ tự gói tin (Sequence Number), cửa sổ trượt (Sliding Window), bộ hẹn giờ truyền lại (Retransmission Timer) và thông tin kiểm soát tắc nghẽn. - Cấp phát Bộ đệm Đọc (
sk_rcvbuf- Receive Buffer) và Bộ đệm Ghi (sk_sndbuf- Send Buffer): Chứa các phân đoạn dữ liệu (struct sk_buff- Socket Buffer) đang chờ ứng dụng đọc lên không gian người dùng (User Space) hoặc đang chờ card mạng truyền đi và nhận gói xác nhậnACK.
Về mặt toán học hệ thống, một kết nối TCP ở trạng thái nhàn rỗi (Idle Keep-Alive hoặc WebSocket đang chờ sự kiện) tiêu tốn tối thiểu khoảng 3.3 KB đến 4.5 KB bộ nhớ cấu trúc hạt nhân. Khi kết nối bắt đầu truyền nhận dữ liệu hoạt động, tổng dung lượng bộ nhớ của nó bằng kích thước cấu trúc hạt nhân cộng với dung lượng hiện hành của sk_rcvbuf và sk_sndbuf. Do đó, nếu chúng ta muốn duy trì 500.000 kết nối đồng thời nhưng lại đặt mức bộ đệm mặc định quá lớn hoặc không tinh chỉnh cơ chế quản lý bộ nhớ hệ thống (xem thêm phân tích chuyên sâu về quản lý trang nhớ hạt nhân và Slab Cache tại bài viết Giám sát tài nguyên hệ thống và tối ưu bộ nhớ RAM/Swap trên Linux), nhân Linux sẽ cạn kiệt vùng nhớ mạng TCP (tcp_mem) hoặc chạm trần giới hạn File Descriptor chỉ sau vài chục nghìn kết nối đầu tiên.
1.2 Sơ đồ Luồng Đi của Gói tin TCP qua Các Hàng đợi Kernel (NIC Ring Buffer, SYN Queue, Accept Queue)
Nhiều kỹ sư thường lầm tưởng rằng khi máy khách gửi gói tin tới máy chủ, ứng dụng Web (như Nginx hay Go HTTP Server) sẽ trực tiếp nhận và xử lý ngay gói tin đó. Trên thực tế, trước khi hàm hệ thống accept4() trong không gian người dùng được phép lấy ra một kết nối mới, gói tin phải vượt qua 4 hàng đợi (Queues) độc lập nằm bên trong phần cứng card mạng và ngăn xếp TCP/IP của nhân Linux. Nếu bất kỳ hàng đợi nào trong 4 mắt xích này bị đầy, nhân Linux sẽ âm thầm vứt bỏ gói tin (Drop Packet) mà ứng dụng ở tầng trên hoàn toàn không hề hay biết.
Sơ đồ kiến trúc dưới đây mô tả chi tiết hành trình của gói tin TCP đi xuyên qua 4 tầng hàng đợi hạt nhân và biến số sysctl kiểm soát trực tiếp từng tầng:
flowchart TD
subgraph Layer1_NIC ["1. Tầng Phần cứng Card Mạng & Ngắt Mềm (NIC & SoftIRQ Layer)"]
PacketIn["Gói tin TCP từ Client (Ethernet Frame)"] --> RingBuf["NIC RX Ring Buffer (ethtool -G rx 4096)"]
RingBuf -- "DMA Copy & NAPI Poll" --> BacklogQ["Per-CPU Backlog Queue (net.core.netdev_max_backlog = 65536)"]
end
subgraph Layer2_TCPHandshake ["2. Tầng Bắt tay 3 Bước TCP trong Nhân Linux (Kernel TCP Stack)"]
BacklogQ -- "Gói tin SYN đầu tiên" --> SynQ["Hàng đợi Bán mở - SYN Queue (net.ipv4.tcp_max_syn_backlog = 65536)"]
SynQ -- "Tràn hàng đợi SYN: Kích hoạt bảo vệ" --> SynCookie["Cơ chế Mã hóa Không trạng thái (net.ipv4.tcp_syncookies = 1)"]
SynQ -- "Nhận gói ACK hoàn tất 3-Way Handshake" --> AcceptQ["Hàng đợi Chấp nhận - Accept Queue: min(listen backlog, net.core.somaxconn = 65535)"]
end
subgraph Layer3_UserSpace ["3. Tầng Đa hợp Sự kiện & Không gian Người dùng (VFS & Worker Process)"]
AcceptQ -- "System Call accept4() cấp phát FD" --> EpollTree["Cây Đỏ-Đen epoll Instance (fs.file-max & systemd LimitNOFILE = 1048576)"]
EpollTree -- "epoll_wait() O(1) Ready List" --> Buffers["Bộ đệm Đọc/Ghi Socket (net.ipv4.tcp_rmem / tcp_wmem + TCP BBR)"]
end
subgraph Layer4_Teardown ["4. Tầng Đóng Kết nối & Thu hồi Tài nguyên (Socket Teardown Lifecycle)"]
Buffers -- "Đóng kết nối 4 bước (FIN / ACK)" --> TimeWait["Trạng thái TIME_WAIT & FIN_WAIT_2 (tcp_tw_reuse = 1, tcp_fin_timeout = 15)"]
TimeWait -- "Giải phóng Cổng Tạm thời" --> PortPool["Dải Cổng Nội bộ (net.ipv4.ip_local_port_range = 10240 65535)"]
end
1.3 Cơ chế Đa hợp Sự kiện I/O độ phức tạp O(1) với epoll so với select và poll
Khi một kết nối TCP đã đi qua Accept Queue và được cấp phát một File Descriptor (FD), làm thế nào để một tiến trình máy chủ Web có thể theo dõi đồng thời 100.000 đến 1.000.000 FD xem kết nối nào vừa có dữ liệu mới gửi tới?
Theo tài liệu chuẩn epoll(7) Linux Programmer's Manual, các giao diện đa hợp I/O thế hệ cũ là select(2) và poll(2) hoạt động với độ phức tạp thuật toán tuyến tính O(N):
- Với
select(2), cấu trúc mặt nạ bitfd_setbị khóa cứng ở giới hạn mặc địnhFD_SETSIZE = 1024mô tả tập tin. - Với cả
select(2)vàpoll(2), ở mỗi vòng lặp sự kiện, tiến trình phải sao chép toàn bộ mảng chứaNmô tả tập tin từ User Space xuống Kernel Space, và nhân Linux phải quét tuần tự qua cả100.000socket chỉ để tìm ra vài chục socket đang thực sự có dữ liệu đến. KhiNvượt quá10.000, CPU của máy chủ bị tiêu tốn hoàn toàn vào việc quét mảng vô ích (System Call Overhead).
Cơ chế epoll (ra mắt từ Linux Kernel 2.5.44) giải quyết triệt để điểm nghẽn này bằng cách tách bạch việc đăng ký theo dõi và việc chờ sự kiện thông qua hai cấu trúc dữ liệu nội bộ trong nhân Linux:
- Cây Đỏ-Đen (
Red-Black Tree- RB-Tree) trong không gian nhân: Khi máy chủ gọiepoll_ctl(epfd, EPOLL_CTL_ADD, fd, event), nhân Linux chèn FD mới vào một Cây Đỏ-Đen tự cân bằng với chi phí tra cứu, thêm và xóa chỉ làO(log N), không cần sao chép lại danh sách FD ở mỗi vòng lặp. - Danh sách Liên kết Kép các Socket Sẵn sàng (
Ready Doubly-Linked List) kết hợp Callback Ngắt Mạng (O(1)): Khi đăng ký FD vàoepoll, nhân Linux gắn một hàm gọi lại (Callback Hook) trực tiếp vào hàng đợi chờ của từng Socket. Ngay khi card mạng nhận được gói tin và trình xử lý ngắt mềm (NET_RX_SOFTIRQ) đẩy dữ liệu vào bộ đệm đọc của một Socket cụ thể, hàm Callback lập tức đẩy tham chiếu của Socket đó vào danh sách sẵn sàng (rdllist). Khi tiến trình ứng dụng gọiepoll_wait(), nhân Linux chỉ việc kiểm tra xemrdllistcó phần tử nào không và trả về đúng số lượng các Socket đang hoạt động với độ phức tạpO(1)(không phụ thuộc vào tổng số kết nối đang mở).
Sơ đồ tuần tự dưới đây minh họa toàn bộ vòng đời tương tác thời gian thực giữa Client, ngăn xếp TCP của nhân Linux và tiến trình Worker chạy epoll từ lúc bắt tay 3 bước cho đến lúc đóng kết nối 4 bước:
sequenceDiagram
autonumber
actor Client as Client / Load Generator
participant NIC as NIC & SoftIRQ (netdev_max_backlog)
participant SYNQ as Kernel SYN Queue (tcp_max_syn_backlog)
participant ACCQ as Kernel Accept Queue (net.core.somaxconn)
participant EPOLL as Worker Process (epoll_wait & LimitNOFILE)
Note over Client,EPOLL: Giai đoạn 1: Bắt tay 3 bước TCP (3-Way Handshake theo RFC 9293)
Client->>NIC: Gửi gói TCP SYN (seq = x)
NIC->>SYNQ: Đưa kết nối vào trạng thái SYN_RECV trong SYN Queue
SYNQ-->>Client: Phản hồi gói TCP SYN+ACK (seq = y, ack = x + 1)
Client->>NIC: Gửi gói TCP ACK (ack = y + 1)
NIC->>SYNQ: Xác thực ACK hợp lệ & Xóa khỏi SYN Queue
SYNQ->>ACCQ: Chuyển Socket sang trạng thái ESTABLISHED trong Accept Queue
Note over Client,EPOLL: Giai đoạn 2: Đa hợp Sự kiện I/O O(1) qua Linux epoll
ACCQ->>EPOLL: Đánh thức epoll_wait() trên Listening Socket -> Gọi accept4() cấp phát FD
EPOLL->>EPOLL: Đăng ký Client FD vào Cây Đỏ-Đen qua epoll_ctl(EPOLL_CTL_ADD, EPOLLIN | EPOLLET)
Client->>NIC: Truyền dữ liệu HTTP Request (PSH, ACK)
NIC->>EPOLL: SoftIRQ Callback đẩy FD vào Ready List -> epoll_wait() đọc từ tcp_rmem & Gửi phản hồi qua tcp_wmem
Note over Client,EPOLL: Giai đoạn 3: Đóng kết nối 4 bước & Quản lý TIME_WAIT
EPOLL-->>Client: Gửi gói TCP FIN+ACK để đóng kết nối chủ động hoặc bị động
Client->>NIC: Gửi gói TCP ACK & FIN+ACK
NIC->>ACCQ: Chuyển Socket qua FIN_WAIT_2 (tcp_fin_timeout = 15s) -> TIME_WAIT (tcp_tw_reuse = 1)
2. Mở rộng Giới hạn File Descriptors Toàn hệ thống (fs.file-max, limits.conf và systemd LimitNOFILE)
Vì mỗi kết nối TCP mở vào hoặc mở ra đều tiêu tốn đúng 1 File Descriptor (FD), nên nếu một máy chủ Reverse Proxy (như Nginx hoặc HAProxy) tiếp nhận 250.000 kết nối đồng thời từ người dùng cuối và chuyển tiếp chúng tới các máy chủ Backend nội bộ qua 250.000 kết nối chiều đi ra, tiến trình Proxy đó cần mở tối thiểu 500.000 File Descriptors (chưa kể các tệp nhật ký, tệp bộ đệm trên đĩa và các thư viện liên kết động .so). Trong khi đó, giới hạn mặc định trên mỗi tiến trình của hầu hết các bản phân phối Linux (ulimit -n) chỉ vỏn vẹn là 1024!
2.1 Phân biệt 3 Tầng Giới hạn File Descriptor: Toàn hệ thống (fs.file-max), Trần Tiến trình (fs.nr_open) và Phiên Người dùng (PAM limits)
Một sai lầm rất phổ biến của quản trị viên hệ thống là chỉ tăng một thông số duy nhất trong /etc/sysctl.conf rồi cho rằng hệ thống đã sẵn sàng chịu tải lớn, nhưng khi lưu lượng thực tế tăng lên trên 65.535 kết nối, dịch vụ vẫn báo lỗi socket() failed (24: Too many open files). Để mở khóa hoàn toàn khả năng cấp phát FD trên Linux, chúng ta bắt buộc phải cấu hình đồng bộ theo thứ tự từ trên xuống dưới qua 3 tầng kiểm soát của hạt nhân:
- Tầng 1 — Tổng số File Descriptors tối đa trên toàn hệ điều hành (
fs.file-max): Theo tài liệu chính thức của Linux Kernel (Documentation/admin-guide/sysctl/fs.rst), thông sốfs.file-maxquy định tổng số cấu trúc tệp (struct file) mà nhân Linux được phép cấp phát cho toàn bộ các tiến trình cộng lại trên máy chủ. Để phục vụ quy mô triệu kết nối, giá trị này cần được đặt lên mức2097152(2 triệu) hoặc cao hơn tùy theo dung lượng RAM vật lý. - Tầng 2 — Trần cứng tối đa mà một tiến trình đơn lẻ được phép yêu cầu (
fs.nr_open): Đây là chốt chặn của nhân Linux đặt trên lời gọi hệ thốngsetrlimit(RLIMIT_NOFILE). Mặc định trên các nhân Linux hiện đại,fs.nr_opencó giá trị là1048576(1024 * 1024). Lưu ý sống còn: Mọi cấu hình giới hạn ở tầng người dùng (limits.conf) hay tầng dịch vụ (systemd LimitNOFILE) đều không được phép vượt quá giá trị củafs.nr_open. Nếu bạn cấu hìnhLimitNOFILE=2000000trong khifs.nr_openvẫn đang là1048576, nhân Linux sẽ từ chối cấp quyền và làm tiến trình dịch vụ lỗi ngay khi khởi động! Do đó, chúng ta cần nângfs.nr_open = 2097152đồng bộ vớifs.file-max. - Tầng 3 — Giới hạn cấp phiên đăng nhập PAM (
/etc/security/limits.d/) và cấp đơn vị dịch vụ (systemd): Giới hạn mềm (soft limit) và giới hạn cứng (hard limit) áp dụng trực tiếp lên bảng FD của từng tiến trình cụ thể (kiểm tra được thông qua tệp/proc/<pid>/limits).
2.2 Tại sao Dịch vụ systemd Phớt lờ /etc/security/limits.conf và Cấu hình Chuẩn LimitNOFILE=1048576
Đây là "cái bẫy" kinh điển nhất trên các hệ điều hành Linux sử dụng trình quản lý khởi tạo systemd (Ubuntu 16.04+, Debian 8+, RHEL/CentOS 7+, Rocky Linux 8/9): quản trị viên chỉnh sửa tệp /etc/security/limits.conf thêm dòng * soft nofile 1048576 và * hard nofile 1048576, sau đó gõ ulimit -n trên cửa sổ dòng lệnh SSH và thấy kết quả trả về đúng là 1048576. Thế nhưng, khi khởi động lại dịch vụ bằng lệnh systemctl restart nginx, tiến trình Nginx vẫn bị giới hạn ở mức 1024 hoặc 524288!
Giải thích cơ chế hạt nhân: Các tệp cấu hình trong /etc/security/limits.conf và /etc/security/limits.d/*.conf thuộc về mô-đun pam_limits.so của hệ thống xác thực PAM (Pluggable Authentication Modules). Mô-đun này chỉ được kích hoạt khi có một phiên đăng nhập tương tác của người dùng (như đăng nhập qua SSH, su, sudo hoặc màn hình Console). Ngược lại, các dịch vụ chạy nền (Daemons như nginx.service, haproxy.service, redis.service, custom-api.service) được khởi chạy trực tiếp bởi tiến trình gốc systemd (PID 1) chứ không đi qua luồng đăng nhập PAM. Theo tài liệu đặc tả chính thức systemd.exec(5), systemd hoàn toàn bỏ qua /etc/security/limits.conf và chỉ áp dụng giới hạn tài nguyên khai báo qua chỉ thị LimitNOFILE= bên trong tệp Unit của dịch vụ đó!
Để đảm bảo cả các công cụ chạy qua phiên SSH (như công cụ kiểm thử tải wrk, vegeta) lẫn các dịch vụ chạy nền dưới quyền systemd đều được cấp phát tới 1.048.576 File Descriptors, chúng ta thiết lập đồng thời hai tệp cấu hình chuẩn Production dưới đây:
# -----------------------------------------------------------------------------
# File 1: /etc/security/limits.d/99-infoteck-nofile.conf
# Áp dụng cho các phiên đăng nhập PAM (SSH, su, sudo, Load Testing CLI tools)
# -----------------------------------------------------------------------------
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
www-data soft nofile 1048576
www-data hard nofile 1048576
# -----------------------------------------------------------------------------
# File 2: /etc/systemd/system/nginx.service.d/override.conf
# Áp dụng trực tiếp cho tiến trình Daemon quản lý bởi systemd (systemd.exec(5))
# Sau khi lưu tệp, chạy: systemctl daemon-reload && systemctl restart nginx
# -----------------------------------------------------------------------------
[Service]
LimitNOFILE=1048576
LimitNPROC=65536
TasksMax=infinity
Sau khi áp dụng tệp override.conf và nạp lại systemd, kỹ sư có thể kiểm chứng trực tiếp bảng giới hạn thực tế đang áp dụng trong không gian nhân của tiến trình Worker bằng lệnh grep "Max open files" /proc/<nginx-worker-pid>/limits để xác nhận cả hai cột Soft Limit và Hard Limit đều hiển thị chính xác 1048576.
3. Tinh chỉnh Hàng đợi Kết nối TCP (somaxconn, netdev_max_backlog, tcp_max_syn_backlog và syn_cookies)
Sau khi đã tháo bỏ nút thắt về số lượng File Descriptors tối đa mà máy chủ có thể giữ mở cùng lúc, thách thức tiếp theo trong bài toán C100K/C1M là Tốc độ Thiết lập Kết nối Mới mỗi Giây (New Connections Per Second - CPS). Giả sử sau một sự cố mạng đường trục hoặc đúng vào giây đầu tiên của một sự kiện lớn, 50.000 thiết bị người dùng đồng loạt gửi gói tin SYN để thiết lập kết nối TCP trong vòng 1 giây. Nếu các hàng đợi bắt tay trong nhân Linux quá hẹp, hàng chục nghìn gói tin sẽ bị vứt bỏ ngay ở cửa ngõ.
3.1 Giải quyết Hiện tượng Tràn Hàng đợi SYN (SYN Queue) và Hàng đợi Chấp nhận (Accept Queue / net.core.somaxconn)
Theo chuẩn giao thức IETF RFC 9293 (Transmission Control Protocol) và tài liệu nhân Linux Documentation/networking/ip-sysctl.rst, quá trình bắt tay 3 bước (3-Way Handshake) trên một Socket đang lắng nghe (LISTEN) được quản lý bởi hai hàng đợi nối tiếp nhau:
- Hàng đợi Bán mở (
SYN Queue— trạng tháiSYN_RECV):- Khi nhân Linux nhận gói tin
SYNđầu tiên từ Client, nó cấp phát một cấu trúc nhẹstruct request_sock, lưu vào bảng băm SYN Queue, gửi trả gói tinSYN+ACKvà chờ gói tinACKcuối cùng từ phía Client. - Độ dài tối đa của hàng đợi này được điều khiển bởi biến số
net.ipv4.tcp_max_syn_backlog(mặc định trên nhiều hệ thống chỉ từ1024đến4096). Khi phục vụ người dùng di động trên mạng 4G/5G có độ trễ vòng lặp (RTT) từ50msđến150ms, mỗi kết nối bán mở sẽ nằm chiếm chỗ trongSYN Queuesuốt khoảng thời gian1 RTTđó. Để hấp thụ các đợt tăng vọt hàng chục nghìn kết nối mới mỗi giây mà không làm rớt góiSYN(biểu hiện qua bộ đếmTcpExtListenDrops), chúng ta phải nângnet.ipv4.tcp_max_syn_backlog = 65536.
- Khi nhân Linux nhận gói tin
- Hàng đợi Chấp nhận Hoàn tất (
Accept Queue— trạng tháiESTABLISHEDchờaccept4()):- Ngay khi nhân Linux nhận được gói tin
ACKthứ ba hợp lệ từ Client, kết nối đã hoàn tất bắt tay 3 bước. Nhân Linux chuyển đối tượng kết nối từSYN Queuesang Accept Queue và đánh thức tiến trình ứng dụng (quaepoll_wait) để gọi hàmaccept4(). - Kích thước thực tế của
Accept Queuetrên bất kỳ cổng lắng nghe nào được nhân Linux quyết định theo công thức toán học cứng:
Effective_Accept_Queue_Size = min(application_listen_backlog, net.core.somaxconn) - Trong các phiên bản nhân Linux trước 5.4, giá trị mặc định của
net.core.somaxconnchỉ là128, và từ Linux 5.4 trở đi mặc định là4096. Điều này có nghĩa là ngay cả khi bạn cấu hìnhlisten 443 ssl backlog=65535;bên trong tệp cấu hình của Nginx (xem thêm cách đồng bộ hóa thông số tầng ứng dụng tại bài viết Cấu hình tối ưu Nginx Reverse Proxy, HTTP/2 và TLS 1.3 hiệu năng cao), nhân Linux vẫn sẽ âm thầm cắt cụt (Truncate) hàng đợi của Nginx xuống mứcnet.core.somaxconn! Nếu tiến trình Worker tạm thời bận xử lý mã hóa TLS trong vài mili-giây khiếnAccept Queuebị đầy, nhân Linux sẽ từ chối nhận góiACKhoàn tất bắt tay (tăng bộ đếmTcpExtListenOverflows). Do đó, việc nângnet.core.somaxconn = 65535là yêu cầu bắt buộc đối với mọi máy chủ Web tải cao.
- Ngay khi nhân Linux nhận được gói tin
Ngoài ra, thông số net.ipv4.tcp_abort_on_overflow quy định cách nhân Linux phản ứng khi Accept Queue bị tràn:
- Ở giá trị mặc định
0(khuyến nghị giữ nguyên0trên môi trường Production): nhân Linux chỉ lặng lẽ bỏ qua gói tinACKcuối cùng của Client và giữ kết nối ở trạng tháiSYN_RECV, để bộ hẹn giờ truyền lại góiSYN+ACKhoạt động và cho phép Client gửi lại góiACKsau vài trăm mili-giây khi ứng dụng đã kịp rút bớt hàng đợiAccept Queue. - Nếu đặt thành
1: nhân Linux sẽ lập tức bắn gói tinTCP RST (Reset)về phía Client và hủy bỏ ngay kết nối (Connection Reset by Peer), gây lỗi tức thì trên trình duyệt của người dùng khi mới chỉ có một đợt nghẽn ngắn hạn.
3.2 Cơ chế Phòng thủ SYN Flood bằng net.ipv4.tcp_syncookies và Mở rộng Hàng đợi Thiết bị Mạng (netdev_max_backlog)
Trước khi gói tin SYN kịp chạm tới tầng giao thức TCP (SYN Queue), nó phải được lấy ra từ bộ đệm vòng phần cứng của Card mạng (NIC RX Ring Buffer) bởi cơ chế NAPI (New API) SoftIRQ và xếp vào hàng đợi đầu vào trên từng nhân CPU (Per-CPU Input Packet Queue). Độ dài tối đa của hàng đợi tầng liên kết dữ liệu này được kiểm soát bởi thông số net.core.netdev_max_backlog (mặc định chỉ là 1000 gói tin). Trên các máy chủ trang bị card mạng 10GbE, 25GbE hoặc 40GbE, tốc độ gói tin đổ vào từ phần cứng thường nhanh hơn tốc độ xử lý của nhân CPU trong các đợt sóng lưu lượng (Micro-bursts). Việc nâng net.core.netdev_max_backlog = 65536 kết hợp với tăng ngân sách quét gói tin của NAPI trong một chu kỳ ngắt mềm (net.core.netdev_budget = 600 và net.core.netdev_budget_usecs = 8000) giúp ngăn chặn hoàn toàn hiện tượng rớt gói ở tầng nhận card mạng.
Ở tầng bắt tay TCP, để bảo vệ máy chủ trước các cuộc tấn công làm ngập hàng đợi bán mở (TCP SYN Flood DDoS — kết hợp cùng các lớp bảo vệ tầng ứng dụng được phân tích tại bài viết Phòng chống tấn công DDoS tầng ứng dụng Layer 7 với Nginx Rate Limiting), chúng ta bắt buộc bật net.ipv4.tcp_syncookies = 1. Nhiều tài liệu cũ thường hiểu sai rằng bật tcp_syncookies = 1 sẽ làm tăng tải CPU cho mọi kết nối thông thường. Thực tế theo mã nguồn nhân Linux (net/ipv4/tcp_input.c), khi đặt bằng 1, cơ chế SYN Cookies hoàn toàn ngủ yên trong điều kiện vận hành bình thường và chỉ tự động kích hoạt khi và chỉ khi hàng đợi net.ipv4.tcp_max_syn_backlog đã bị lấp đầy 100%:
- Khi hàng đợi
SYN Queuebị tràn, thay vì vứt bỏ gói tinSYNmới hoặc tốn bộ nhớ lưu cấu trúcrequest_sock, nhân Linux mã hóa thông tin kết nối (địa chỉ IP/Port nguồn và đích, kích thước gói tin tối đaMSS, dấu thời gian) bằng hàm băm mật mã học trực tiếp vào số thứ tự khởi tạo 32-bit (Initial Sequence Number - ISN) của gói tin phản hồiSYN+ACK. - Khi máy khách hợp pháp gửi lại gói tin
ACKcó số xác nhận bằngISN + 1, nhân Linux kiểm tra ngược hàm băm và khởi tạo thẳng kết nối vàoAccept Queuemà không cần tra cứuSYN Queue, trong khi các góiSYNgiả mạo địa chỉ IP nguồn (Spoofed SYN Flood) không bao giờ phản hồi lại góiACKsẽ không chiếm giữ bất kỳ byte bộ nhớ nào của máy chủ. - Đồng thời, trên các máy chủ mặt tiền, việc giảm số lần thử truyền lại gói tin
SYN+ACKđối với các kết nối bán mở không phản hồi thông quanet.ipv4.tcp_synack_retries = 2(thay vì mặc định5lần kéo dài tới hơn 30 giây) giúp giải phóng các khe cắm trongSYN Queuenhanh gấp 5 lần khi bị tấn công rải rác.
4. Tối ưu Vòng đời Socket: Quản lý Trạng thái TIME_WAIT (tcp_tw_reuse), tcp_fin_timeout và Ephemeral Ports
Nếu như SYN Queue và Accept Queue quyết định khả năng mở kết nối ở đầu vào, thì khả năng đóng và tái sử dụng tài nguyên ở cuối vòng đời kết nối lại là tử huyệt thường xuyên làm tê liệt các máy chủ Reverse Proxy, API Gateway và Load Balancer khi lưu lượng giao dịch tăng cao.
4.1 Bản chất Trạng thái TIME_WAIT (2MSL theo RFC 9293) và Rủi ro Cạn kiệt Cổng Tạm thời (Ephemeral Port Exhaustion)
Theo đặc tả chuẩn IETF RFC 9293 (Mục 3.6 - Closing a Connection), bên nào chủ động gửi gói tin FIN đầu tiên để đóng kết nối TCP (Active Closer) sẽ bắt buộc phải chuyển Socket sang trạng thái TIME_WAIT sau khi nhận được gói FIN từ phía đối diện và gửi đi gói ACK cuối cùng. Trong nhân Linux, thời gian giữ một Socket ở trạng thái TIME_WAIT được khóa cứng trong hằng số biên dịch hạt nhân TCP_TIMEWAIT_LEN là 60 giây.
Trạng thái TIME_WAIT tồn tại vì hai lý do kỹ thuật cốt lõi nhằm bảo vệ tính toàn vẹn của giao thức TCP:
- Đảm bảo phía đóng bị động (Passive Closer) nhận được gói
ACKcuối cùng: Nếu góiACKcuối cùng bị rớt trên đường truyền, phía bên kia sẽ gửi lại góiFIN. Nếu Socket ở phía đóng chủ động vẫn đang nằm trong trạng tháiTIME_WAIT, nó có thể gửi lại góiACKmột cách êm ái thay vì bắn góiRSTbáo lỗi. - Ngăn chặn các gói tin đi lạc từ phiên kết nối cũ (Wandering Duplicate Segments) làm hỏng phiên kết nối mới: Ngăn không cho một kết nối mới được tạo lập ngay lập tức với cùng bộ 4 thông số định danh (
4-tuple:Source IP,Source Port,Destination IP,Destination Port) trong khi các gói tin trễ của phiên trước vẫn còn trôi nổi trên các bộ định tuyến mạng.
Tại sao TIME_WAIT lại gây sập hệ thống Reverse Proxy? Hãy xét một máy chủ Nginx Reverse Proxy (10.0.1.10) đứng trước một máy chủ ứng dụng Backend (10.0.2.50:8080). Mỗi kết nối TCP từ Nginx gọi sang Backend được định danh bởi bộ 4 thông số: (10.0.1.10, <Ephemeral_Source_Port>, 10.0.2.50, 8080). Vì 3 thông số (Source IP, Destination IP, Destination Port) đã cố định, số lượng kết nối đồng thời tối đa mà Nginx có thể mở tới địa chỉ Backend đó phụ thuộc hoàn toàn vào số lượng Cổng nguồn tạm thời (Ephemeral Source Ports) còn trống trên máy chủ Nginx!
Trên Linux mặc định, dải cổng tạm thời (net.ipv4.ip_local_port_range) chỉ từ 32768 đến 60999 — tức là chỉ có 28.232 cổng. Nếu mỗi kết nối sau khi hoàn tất yêu cầu HTTP bị đóng lại và giữ chặt cổng nguồn trong trạng thái TIME_WAIT suốt 60 giây, thì ngay khi lưu lượng vượt quá 28.232 / 60 = 470 yêu cầu mỗi giây (nếu chưa bật Keep-Alive hoàn hảo), toàn bộ 28.232 cổng nguồn sẽ bị lấp kín bởi các Socket TIME_WAIT. Mọi yêu cầu tiếp theo từ Nginx gọi về Backend sẽ thất bại tức khắc với lỗi Cannot assign requested address (EADDRNOTAVAIL - errno 99)!
4.2 Tinh chỉnh An toàn tcp_tw_reuse, tcp_fin_timeout và Tại sao Linux 4.12 Loại bỏ Hoàn toàn tcp_tw_recycle
Để giải quyết triệt để bài toán tích tụ TIME_WAIT và FIN_WAIT_2 mà không vi phạm tính an toàn của giao thức TCP, chúng ta kết hợp 4 biện pháp cấu hình chuẩn xác sau:
- Mở rộng dải cổng tạm thời (
net.ipv4.ip_local_port_range = 10240 65535): Mở rộng dải cổng nguồn dành cho các kết nối chiều đi ra lên hơn55.295cổng (lưu ý: không bao giờ hạ cận dưới xuống thấp hơn10240nếu trên cùng máy chủ có các dịch vụ lắng nghe ở các cổng như8080,8443,9000,9100, nhằm tránh việc nhân Linux cấp phát nhầm cổng dịch vụ làm cổng tạm thời). - Kích hoạt tái sử dụng an toàn Socket
TIME_WAITcho kết nối chiều đi ra (net.ipv4.tcp_tw_reuse = 1kèmnet.ipv4.tcp_timestamps = 1): Theo tài liệu chính thứcip-sysctl.rstcủa Linux Kernel, khi cảtcp_timestampsvàtcp_tw_reuseđược bật bằng1, nhân Linux cho phép lấy lại một Socket đang nằm ở trạng tháiTIME_WAITđể cấp phát cho một kết nối mới chiều đi ra (connect()) ngay khi dấu thời gian TCP (TCP Timestamp) của kết nối mới lớn hơn dấu thời gian ghi nhận cuối cùng của kết nối trước đó (tức chỉ sau khoảng 1 giây thay vì phải chờ đủ 60 giây). Cơ chế này hoàn toàn an toàn về mặt giao thức vì tính năng PAWS (Protection Against Wrapped Sequences) dựa trên nhãn thời gian sẽ tự động lọc bỏ mọi gói tin đi lạc từ phiên cũ. - Rút ngắn thời gian chờ trạng thái mồ côi
FIN_WAIT_2(net.ipv4.tcp_fin_timeout = 15) và giới hạn trầntcp_max_tw_buckets = 262144: Khi máy chủ chủ động đóng kết nối và đã nhận đượcACKcho góiFINcủa mình, Socket chuyển sang trạng tháiFIN_WAIT_2để chờ góiFINtừ phía máy khách. Nếu trình duyệt di động của khách hàng mất sóng đột ngột hoặc tắt ứng dụng mà không gửi góiFIN, Socket sẽ bị treo ởFIN_WAIT_2suốt60 giâymặc định. Giảmnet.ipv4.tcp_fin_timeoutxuống15 giâyvà giới hạn số lượng Socket mồ côi tối đa (net.ipv4.tcp_max_orphans = 65536) giúp thu hồi bộ nhớ hạt nhân nhanh gấp 4 lần. - CẢNH BÁO KỸ THUẬT QUAN TRỌNG — Tuyệt đối không cấu hình
net.ipv4.tcp_tw_recycle: Rất nhiều bài viết sao chép cấu hình cũ trên Internet vẫn khuyên bậtnet.ipv4.tcp_tw_recycle = 1. Các kỹ sư cần nắm rõ hai sự thật kỹ thuật:- Thứ nhất, khi
tcp_tw_recyclecòn tồn tại trên các nhân Linux cũ, nó áp dụng cơ chế kiểm tra dấu thời gian tăng dần theo từng địa chỉ IP nguồn. Khi hàng trăm người dùng di động hoặc nhân viên trong một tòa nhà cùng đi qua một cổng NAT Gateway (Carrier-Grade NAT) dùng chung một địa chỉ IP công khai, đồng hồ hệ thống giữa các điện thoại/máy tính khác nhau có độ lệch tự nhiên. Hậu quả là máy chủ Linux bậttcp_tw_recycle = 1sẽ âm thầm vứt bỏ toàn bộ gói tinSYNcủa những thiết bị có giá trị đồng hồ nhỏ hơn thiết bị vừa kết nối trước đó! - Thứ hai, vì lỗi nghiêm trọng phá vỡ kết nối sau NAT này, đội ngũ phát triển nhân Linux đã xóa bỏ hoàn toàn thông số
net.ipv4.tcp_tw_recyclekhỏi mã nguồn Linux Kernel kể từ phiên bản 4.12 (tháng 7/2017). Trên mọi hệ điều hành hiện đại (Kernel 5.x, 6.x), việc khai báonet.ipv4.tcp_tw_recycletrong tệpsysctlsẽ gây ra lỗisysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory.
- Thứ nhất, khi
5. Tinh chỉnh Bộ đệm Socket (TCP Buffer Sizing) và Kích hoạt Thuật toán Chống Tắc nghẽn TCP BBR
Sau khi các kết nối đã được thiết lập và duy trì ổn định ở quy mô hàng trăm nghìn Socket, tốc độ truyền tải dữ liệu thực tế (Throughput) và độ trễ phản hồi (Latency) trên mỗi kết nối phụ thuộc trực tiếp vào hai yếu tố: kích thước cửa sổ bộ đệm đọc/ghi (TCP Receive/Send Buffers) và thuật toán điều khiển tắc nghẽn (TCP Congestion Control Algorithm).
5.1 Tính toán Tích Băng thông - Độ trễ (Bandwidth-Delay Product - BDP) cho Bộ đệm Đọc/Ghi (tcp_rmem, tcp_wmem)
Trong giao thức TCP, bên gửi chỉ được phép truyền đi một lượng dữ liệu tối đa chưa được xác nhận (In-Flight Unacknowledged Data) không vượt quá kích thước cửa sổ nhận (Receive Window) và bộ đệm gửi (Send Buffer). Để lấp đầy hoàn toàn một đường truyền mạng tốc độ cao, kích thước bộ đệm tối đa của Socket phải lớn hơn hoặc bằng Tích Băng thông - Độ trễ (Bandwidth-Delay Product - BDP):
BDP (Bytes) = (Bandwidth_bits_per_sec / 8) * RTT_seconds
Hãy áp dụng công thức này vào một kịch bản thực tế: máy chủ Web có đường truyền 10 Gbps (10.000.000.000 bits/s = 1.250.000.000 Bytes/s) phục vụ truyền tải dữ liệu lớn hoặc đồng bộ liên trung tâm dữ liệu với độ trễ vòng lặp RTT = 20 ms (0.02 giây). Khi đó:
BDP = 1.250.000.000 * 0.02 = 25.000.000 Bytes (xấp xỉ 23.84 MB)
Nếu trần bộ đệm tối đa mặc định của nhân Linux (net.core.rmem_max và net.core.wmem_max) chỉ dừng ở mức 212992 Bytes (208 KB), thì dù đường truyền vật lý là 10 Gbps, thông lượng tối đa trên một luồng TCP với độ trễ 20ms sẽ bị bóp nghẹt ở mức: 208 KB / 0.02s = 10.4 MB/s (chỉ đạt chưa tới 0.1 Gbps)!
Ngược lại, đối với một máy chủ duy trì 500.000 kết nối đồng thời, nếu chúng ta đặt giá trị bộ đệm mặc định (default) quá lớn (ví dụ 1 MB mỗi kết nối), nhân Linux sẽ tiêu tốn hàng trăm Gigabyte RAM ngay lập tức. Giải pháp kỹ thuật chuẩn xác của Linux Kernel là bật cơ chế Tự động điều chỉnh bộ đệm nhận (net.ipv4.tcp_moderate_rcvbuf = 1) kết hợp với việc cấu hình bộ 3 giá trị min default max trong net.ipv4.tcp_rmem và net.ipv4.tcp_wmem:
- Giá trị
min(4096Bytes =4 KB) vàdefault(65536đến131072Bytes =64 KB–128 KB): Giữ ở mức khiêm tốn để hàng trăm nghìn kết nối nhỏ hoặc nhàn rỗi (Keep-Alive, WebSocket, API JSON nhỏ) tiêu tốn cực ít bộ nhớ RAM. - Giá trị
max(16777216Bytes =16 MB, đồng bộ vớinet.core.rmem_maxvànet.core.wmem_max): Cho phép nhân Linux tự động mở rộng cửa sổ trượt TCP (vớinet.ipv4.tcp_window_scaling = 1) lên tới16 MBđối với những kết nối thực sự cần tải tệp lớn hoặc truyền luồng dữ liệu băng thông cao. - Đồng thời, bật
net.ipv4.tcp_slow_start_after_idle = 0: Mặc định trên Linux thông số này bằng1, khiến mọi kết nối HTTP/1.1 Keep-Alive hoặc HTTP/2 Multiplexing chỉ cần nhàn rỗi quá vài trăm mili-giây (giữa hai lần người dùng bấm chuyển trang) sẽ bị thu hẹp cửa sổ tắc nghẽn (cwnd) về lại mức khởi đầu chậm (Slow Start). Đặttcp_slow_start_after_idle = 0giúp duy trì tốc độ truyền tải tối đa trên các kết nối bền vững.
5.2 Chuyển đổi từ Thuật toán Dựa trên Mất gói (CUBIC) sang Thuật toán Dựa trên Mô hình Băng thông (TCP BBR + fq)
Mặc định trên hầu hết các bản phân phối Linux, thuật toán điều khiển tắc nghẽn TCP là CUBIC — một thuật toán dựa trên sự kiện mất gói tin (Loss-Based Congestion Control). Cơ chế của CUBIC là liên tục tăng kích thước cửa sổ truyền dữ liệu cho đến khi bộ đệm tại các thiết bị chuyển mạch trung gian bị tràn và gây rớt gói tin; ngay khi phát hiện mất dù chỉ 1 gói tin (hoặc do nhiễu sóng vô tuyến tự nhiên trên mạng 4G/5G/Wi-Fi của người dùng di động), CUBIC lập tức coi đó là dấu hiệu nghẽn mạng và cắt giảm mạnh tốc độ gửi (cwnd giảm 30%), làm thông lượng sụt giảm thê thảm.
Theo công trình nghiên cứu nổi tiếng của nhóm kỹ sư mạng tại Google (Cardwell, Cheng, Gunn, Yeganeh, & Jacobson — BBR: Congestion-Based Congestion Control, ACM Queue / Communications of the ACM, 2017), thuật toán TCP BBR (Bottleneck Bandwidth and Round-trip propagation time) thay đổi hoàn toàn tư duy điều khiển luồng:
- Thay vì chờ đến khi rớt gói tin mới giảm tốc, TCP BBR liên tục đo lường trực tiếp hai thông số vật lý của đường truyền trên từng gói xác nhận
ACK: Băng thông tối đa tại cổ chai (BtlBw- Bottleneck Bandwidth) và Thời gian lan truyền vòng lặp tối thiểu (RTprop- Round-Trip Propagation Time). - Kết hợp với bộ lập lịch hàng đợi gói tin công bằng
fq(Fair Queueingpacket scheduler —net.core.default_qdisc = fq), BBR chủ động điều tiết khoảng cách thời gian giữa các gói tin gửi đi (Packet Pacing) đúng bằng tốc độ của điểm nghẽn cổ chai. Nhờ đó, BBR vừa tránh làm tràn bộ đệm tại các bộ định tuyến trung gian (triệt tiêu hiện tượng Bufferbloat gây tăng độ trễ), vừa duy trì thông lượng cao ổn định ngay cả trên các kết nối Internet di động có tỷ lệ rớt gói ngẫu nhiên.
Tệp cấu hình hoàn chỉnh /etc/sysctl.d/99-infoteck-c100k.conf dưới đây tổng hợp toàn bộ các thông số hạt nhân đã phân tích từ Mục 2 đến Mục 5, được thiết kế chuẩn mực cho máy chủ Linux Production (Kernel 5.15+ / 6.x, RAM từ 32GB trở lên):
# File: /etc/sysctl.d/99-infoteck-c100k.conf
# Linux Kernel 5.15+ / 6.x | Production C100K/C1M High-Concurrency Web & API Server Tuning
# Áp dụng cấu hình bằng lệnh: sysctl --system
# =============================================================================
# 1. VFS FILE DESCRIPTORS & EPoll WATCH LIMITS
# =============================================================================
fs.file-max = 2097152
fs.nr_open = 2097152
fs.epoll.max_user_watches = 1048576
# =============================================================================
# 2. NIC BACKLOG, SOFTIRQ BUDGET & TCP LISTEN / SYN QUEUES
# =============================================================================
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65536
net.core.netdev_budget = 600
net.core.netdev_budget_usecs = 8000
net.ipv4.tcp_max_syn_backlog = 65536
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_abort_on_overflow = 0
# =============================================================================
# 3. SOCKET LIFECYCLE, TIME_WAIT MANAGEMENT & EPHEMERAL PORTS
# (Lưu ý: tcp_tw_recycle đã bị loại bỏ từ Linux 4.12, tuyệt đối không khai báo)
# =============================================================================
net.ipv4.ip_local_port_range = 10240 65535
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_max_tw_buckets = 262144
net.ipv4.tcp_max_orphans = 65536
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
# =============================================================================
# 4. TCP BUFFER SIZING (BDP OPTIMIZATION) & PERSISTENT CONNECTION TUNING
# =============================================================================
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_moderate_rcvbuf = 1
net.core.rmem_default = 131072
net.core.wmem_default = 131072
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.optmem_max = 65536
net.ipv4.tcp_rmem = 4096 131072 16777216
net.ipv4.tcp_wmem = 4096 131072 16777216
net.ipv4.tcp_mem = 786432 1048576 1572864
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_mtu_probing = 1
# =============================================================================
# 5. TCP BBR CONGESTION CONTROL & FAIR QUEUEING PACKET SCHEDULER
# =============================================================================
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
6. Kiểm tra Thông số bằng ss -s, nstat, sysctl và Mô phỏng Chịu tải với epoll
Một nguyên tắc bất di bất dịch trong quản trị hệ thống Production là: Không một thông số tinh chỉnh Kernel nào được coi là hoàn tất nếu chưa được đo lường và xác minh bằng các bộ đếm hạt nhân thực tế. Thay vì sử dụng công cụ cổ điển netstat (vốn đọc tuần tự tệp /proc/net/tcp rất chậm và gây khóa hạt nhân khi có hàng trăm nghìn dòng), trên các hệ thống Linux hiện đại chúng ta sử dụng bộ công cụ iproute2 (ss và nstat) giao tiếp trực tiếp với nhân Linux qua giao thức nhị phân tốc độ cao NETLINK_SOCK_DIAG.
6.1 Kịch bản Chẩn đoán Trạng thái Hàng đợi TCP và Bộ đếm Rớt Gói Kernel (ListenOverflows, ListenDrops)
Để giám sát liên tục sức khỏe của ngăn xếp mạng TCP/IP (cũng như đẩy các chỉ số này vào hệ thống cảnh báo tập trung thông qua node_exporter — xem hướng dẫn chi tiết tại bài viết Xây dựng hệ thống Monitoring toàn diện với Prometheus và Grafana), kỹ sư vận hành cần nắm vững ý nghĩa của 4 lệnh kiểm tra cốt lõi trong kịch bản verify-c100k-socket-states.sh:
cat /proc/sys/fs/file-nr: Trả về 3 con số cách nhau bởi dấu Tab, lần lượt là: (1) Số lượng File Descriptors đang được cấp phát trên toàn hệ thống, (2) Số lượng FD rảnh trong vùng đệm cấp phát (0trên nhân Linux hiện đại), và (3) Giới hạn tối đafs.file-max(2097152).ss -s(Socket Summary): Tóm tắt tức thời tổng số Socket đang ở trạng tháiestab(ESTABLISHED),closed,orphaned,timewaittrên toàn máy chủ trong vòng chưa đầy 5 mili-giây.ss -lnt(Listening TCP Sockets Queue Inspection): Trên một Socket đang ở trạng tháiLISTEN, hai cộtRecv-QvàSend-Qtrong kết quả lệnhss -lntmang ý nghĩa đặc biệt:Send-Qtrên SocketLISTEN: Hiển thị kích thước tối đa của Accept Queue đã được áp dụng thành công (ví dụ65535sau khi cảnet.core.somaxconnvàbacklogcủa ứng dụng được nâng lên65535).Recv-Qtrên SocketLISTEN: Hiển thị số lượng kết nối đã hoàn tất bắt tay 3 bước hiện đang nằm chờ trongAccept Queuechưa được tiến trình ứng dụng gọiaccept4()lấy ra. NếuRecv-Qliên tục tiến sát tới giá trị củaSend-Q, đó là bằng chứng trực tiếp cho thấy ứng dụng đang bị nghẽn CPU không kịp nhận kết nối mới.
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPTimeWaitOverflow: Truy vấn trực tiếp bộ đếm SNMP mở rộng của nhân Linux:TcpExtListenOverflows: Số lần Accept Queue bị tràn.TcpExtListenDrops: Tổng số lần kết nối bị từ chối ở bước bắt tay (bao gồm cả trànAccept Queue, trànSYN Queuehoặc thiếu bộ nhớ). Trên một máy chủ đã được tinh chỉnh chuẩn xác, hai bộ đếm này phải giữ giá trị ổn định (tốc độ tăng bằng0).
#!/usr/bin/env bash
# File: verify-c100k-socket-states.sh
# Kịch bản nạp cấu hình sysctl và kiểm chứng trạng thái hàng đợi Kernel C100K/C1M
set -euo pipefail
echo "=== [1/5] APPLYING KERNEL SYSCTL CONFIGURATION ==="
sysctl --system
echo "=== [2/5] VERIFYING VFS FILE DESCRIPTOR ALLOCATION (/proc/sys/fs/file-nr) ==="
cat /proc/sys/fs/file-nr
sysctl fs.file-max fs.nr_open
echo "=== [3/5] VERIFYING ACTIVE TCP BBR & QUEUE DISCIPLINE ==="
sysctl net.core.default_qdisc net.ipv4.tcp_congestion_control
lsmod | grep bbr || true
echo "=== [4/5] INSPECTING SOCKET SUMMARY & LISTEN ACCEPT QUEUE CAPACITY ==="
ss -s
ss -lnt
echo "=== [5/5] AUDITING KERNEL TCP OVERFLOW & DROP COUNTERS (nstat) ==="
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtSYNsToLISTENsDropped TcpExtTCPTimeWaitOverflow
# -----------------------------------------------------------------------------
# Expected Verification Output on Tuned Production Linux Host:
# -----------------------------------------------------------------------------
# === [2/5] VERIFYING VFS FILE DESCRIPTOR ALLOCATION (/proc/sys/fs/file-nr) ===
# 148224 0 2097152
# fs.file-max = 2097152
# fs.nr_open = 2097152
#
# === [3/5] VERIFYING ACTIVE TCP BBR & QUEUE DISCIPLINE ===
# net.core.default_qdisc = fq
# net.ipv4.tcp_congestion_control = tcp_bbr
# tcp_bbr 20480 14
#
# === [4/5] INSPECTING SOCKET SUMMARY & LISTEN ACCEPT QUEUE CAPACITY ===
# Total: 145610
# TCP: 144920 (estab 142105, closed 2410, orphaned 12, timewait 2385)
# 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:*
#
# === [5/5] AUDITING KERNEL TCP OVERFLOW & DROP COUNTERS (nstat) ===
# #kernel
# TcpExtListenOverflows 0 0.0
# TcpExtListenDrops 0 0.0
# TcpExtSYNsToLISTENsDropped 0 0.0
# TcpExtTCPTimeWaitOverflow 0 0.0
6.2 Xây dựng Trình Kiểm chứng Máy chủ Bất đồng bộ Dựa trên Linux epoll (Python select.epoll) cho Bài toán C100K
Để giúp các kỹ sư kiểm chứng trực tiếp cách thức một tiến trình ứng dụng tương tác với các thông số Kernel vừa tinh chỉnh (RLIMIT_NOFILE, net.core.somaxconn, Socket không chặn O_NONBLOCK, và vòng lặp sự kiện kích hoạt theo cạnh EPOLLET - Edge-Triggered của epoll), mô-đun epoll_c100k_verifier.py dưới đây được viết bằng thư viện chuẩn của Python 3.11+.
Khi chạy trên máy chủ Linux, kịch bản thực hiện tự động 3 nhiệm vụ:
- Đọc trực tiếp các tệp ảo trong
/proc/sys/và hàm hệ thốngresource.getrlimit(resource.RLIMIT_NOFILE)để kiểm toán xem máy chủ đã đạt ngưỡng cấu hình C100K hay chưa. - Khởi tạo một máy chủ TCP không chặn (Non-blocking Socket) với cờ
SO_REUSEADDR,SO_REUSEPORT,TCP_NODELAYvà đăng ký vào đối tượngselect.epoll()của nhân Linux ở chế độ Edge-Triggered (select.EPOLLET). - Tạo một lô kết nối đồng thời thử nghiệm ngay trong bộ nhớ để xác minh toàn bộ chu trình bắt tay 3 bước, đưa vào
Accept Queue, đánh thứcepoll.poll(), trao đổi dữ liệu qua bộ đệm Socket và đóng kết nối sạch sẽ với độ trễ tính bằng micro-giây:
# File: epoll_c100k_verifier.py
# Python 3.11+ (Linux Kernel 5.15+ / 6.x) | Zero External Dependencies
# Kiểm toán thông số sysctl C100K và kiểm chứng vòng lặp sự kiện O(1) select.epoll
from dataclasses import dataclass
from pathlib import Path
import errno
import resource
import select
import socket
import time
@dataclass(frozen=True)
class KernelAuditMetric:
parameter: str
current_value: str
recommended_min: int
passed: bool
def read_proc_sys_int(proc_path: str, fallback: int = 0) -> int:
path_obj = Path(proc_path)
if not path_obj.exists():
return fallback
raw_content = path_obj.read_text(encoding="utf-8").strip().split()
return int(raw_content[0]) if raw_content else fallback
def audit_linux_c100k_readiness() -> list[KernelAuditMetric]:
"""Kiểm tra trực tiếp các giới hạn Kernel và RLIMIT_NOFILE của tiến trình hiện tại."""
soft_nofile, hard_nofile = resource.getrlimit(resource.RLIMIT_NOFILE)
file_max = read_proc_sys_int("/proc/sys/fs/file-max", 0)
somaxconn = read_proc_sys_int("/proc/sys/net/core/somaxconn", 0)
syn_backlog = read_proc_sys_int("/proc/sys/net/ipv4/tcp_max_syn_backlog", 0)
cc_path = Path("/proc/sys/net/ipv4/tcp_congestion_control")
cc_algo = cc_path.read_text(encoding="utf-8").strip() if cc_path.exists() else "unknown"
return [
KernelAuditMetric("RLIMIT_NOFILE (soft/hard)", f"{soft_nofile}/{hard_nofile}", 100000, soft_nofile >= 100000),
KernelAuditMetric("fs.file-max", str(file_max), 1000000, file_max >= 1000000),
KernelAuditMetric("net.core.somaxconn", str(somaxconn), 32768, somaxconn >= 32768),
KernelAuditMetric("net.ipv4.tcp_max_syn_backlog", str(syn_backlog), 32768, syn_backlog >= 32768),
KernelAuditMetric("net.ipv4.tcp_congestion_control", cc_algo, 1, cc_algo == "bbr"),
]
def run_epoll_edge_triggered_benchmark(batch_connections: int = 200) -> dict[str, float | int]:
"""Khởi tạo máy chủ TCP Non-blocking sử dụng Linux select.epoll (EPOLLET) và đo thông lượng."""
if not hasattr(select, "epoll"):
return {"verified_connections": 0, "elapsed_ms": 0.0}
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.setblocking(False)
server_sock.bind(("127.0.0.1", 0))
bound_host, bound_port = server_sock.getsockname()
server_sock.listen(65535)
epoll_obj = select.epoll()
epoll_obj.register(server_sock.fileno(), select.EPOLLIN | select.EPOLLET)
active_server_fds: dict[int, socket.socket] = {}
client_sockets: list[socket.socket] = []
completed_roundtrips = 0
start_ns = time.perf_counter_ns()
try:
for idx in range(batch_connections):
cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cli.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
cli.setblocking(False)
result_code = cli.connect_ex((bound_host, bound_port))
if result_code not in (0, errno.EINPROGRESS, errno.EALREADY):
raise OSError(result_code, "Non-blocking connect_ex failed")
client_sockets.append(cli)
for cli in client_sockets:
cli.setblocking(True)
cli.sendall(b"PINGn")
deadline = time.monotonic() + 5.0
while completed_roundtrips < batch_connections and time.monotonic() < deadline:
events = epoll_obj.poll(0.05)
for fileno, event_mask in events:
if fileno == server_sock.fileno():
while True:
try:
conn, _ = server_sock.accept()
conn.setblocking(False)
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
active_server_fds[conn.fileno()] = conn
epoll_obj.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)
except BlockingIOError:
break
elif event_mask & select.EPOLLIN:
peer_sock = active_server_fds.get(fileno)
if peer_sock is not None:
data = peer_sock.recv(1024)
if data == b"PINGn":
peer_sock.sendall(b"PONGn")
completed_roundtrips += 1
for cli in client_sockets:
reply = cli.recv(1024)
if reply != b"PONGn":
raise RuntimeError("Unexpected reply payload from epoll server")
finally:
for cli in client_sockets:
cli.close()
for srv_conn in active_server_fds.values():
epoll_obj.unregister(srv_conn.fileno())
srv_conn.close()
epoll_obj.unregister(server_sock.fileno())
epoll_obj.close()
server_sock.close()
elapsed_ms = (time.perf_counter_ns() - start_ns) / 1_000_000.0
return {
"verified_connections": completed_roundtrips,
"elapsed_ms": round(elapsed_ms, 2),
}
if __name__ == "__main__":
print("=== LINUX KERNEL C100K/C1M SYSCTL & RLIMIT AUDIT ===")
for metric in audit_linux_c100k_readiness():
status_label = "PASS" if metric.passed else "WARN_NEEDS_TUNING"
print(f"[{status_label}] {metric.parameter}: current={metric.current_value} (target>={metric.recommended_min})")
bench_result = run_epoll_edge_triggered_benchmark(batch_connections=200)
print(f"=== EPOLL O(1) EDGE-TRIGGERED VERIFICATION: {bench_result} ===")
7. Tổng kết & Tài liệu tham khảo (References)
Quá trình tuning Linux Kernel cho máy chủ Web triệu kết nối là nền tảng hạ tầng bắt buộc để khai thác trọn vẹn sức mạnh của phần cứng đa nhân hiện đại và các kiến trúc phần mềm bất đồng bộ hướng sự kiện (epoll / io_uring). Bằng cách giải phóng đồng bộ 5 nút thắt cổ chai — từ giới hạn File Descriptors trong VFS và systemd, hàng đợi bắt tay TCP (SYN Queue và Accept Queue), vòng đời thu hồi Socket TIME_WAIT / FIN_WAIT_2 và dải cổng tạm thời, kích thước bộ đệm đọc/ghi theo tích băng thông - độ trễ (BDP), cho đến thuật toán điều khiển tắc nghẽn TCP BBR — máy chủ Linux có thể duy trì hàng trăm nghìn đến hàng triệu kết nối TCP đồng thời với độ trễ ổn định và tỷ lệ rớt gói ở tầng hạt nhân bằng không.
7.1 Bảng Đối chiếu Thông số Kernel Mặc định so với Cấu hình Production C100K/C1M
Bảng tổng hợp dưới đây cung cấp góc nhìn đối chiếu nhanh giữa giá trị mặc định trên các bản phân phối Linux phổ thông và giá trị khuyến nghị chuẩn Production cho máy chủ Web/API chịu tải cực lớn:
Thông số Hạt nhân / Hệ thống (sysctl / systemd) |
Giá trị Mặc định Phổ biến | Giá trị Khuyến nghị Chuẩn C100K/C1M | Tác động Kỹ thuật Cốt lõi trong Không gian Nhân (Kernel Space Impact) |
|---|---|---|---|
fs.file-max & fs.nr_open |
~9223372036854775807 / 1048576 |
2097152 / 2097152 |
Mở rộng tổng số cấu trúc struct file toàn hệ thống và nâng trần cứng cho phép một tiến trình đơn lẻ yêu cầu tới 2 triệu File Descriptors. |
systemd LimitNOFILE |
1024 (Soft) / 524288 (Hard) |
1048576 |
Cấp phát trực tiếp hạn mức 1 triệu File Descriptors cho các dịch vụ Daemon (nginx, haproxy, envoy) vốn không đi qua mô-đun PAM /etc/security/limits.conf. |
net.core.somaxconn |
128 (Kernel < 5.4) / 4096 (5.4+) |
65535 |
Mở rộng trần tối đa của hàng đợi Accept Queue (ESTABLISHED chờ accept4()), ngăn chặn việc cắt cụt tham số backlog của Nginx/Go và triệt tiêu lỗi TcpExtListenOverflows. |
net.ipv4.tcp_max_syn_backlog & net.core.netdev_max_backlog |
1024..4096 / 1000 |
65536 / 65536 |
Mở rộng hàng đợi bán mở SYN Queue (SYN_RECV) và hàng đợi gói tin đầu vào từ NAPI SoftIRQ, hấp thụ các đợt tăng vọt hàng chục nghìn kết nối mới mỗi giây. |
net.ipv4.ip_local_port_range |
32768 60999 (28.232 cổng) |
10240 65535 (55.295 cổng) |
Tăng gần gấp đôi số lượng cổng nguồn tạm thời (Ephemeral Source Ports) cho các kết nối từ Reverse Proxy gọi về cụm máy chủ Upstream Backend. |
net.ipv4.tcp_tw_reuse & net.ipv4.tcp_fin_timeout |
2 (Loopback only) / 60 giây |
1 / 15 giây |
Cho phép tái sử dụng an toàn các Socket ở trạng thái TIME_WAIT cho kết nối chiều đi ra dựa trên TCP Timestamps và giảm 75% thời gian giam giữ Socket ở trạng thái FIN_WAIT_2. |
net.core.rmem_max / wmem_max & tcp_rmem / tcp_wmem |
212992 (208 KB) |
Tối đa 16777216 (16 MB) |
Đáp ứng bài toán Tích Băng thông - Độ trễ (BDP) trên đường truyền 10GbE/25GbE thông qua cơ chế tự động co giãn cửa sổ trượt (tcp_moderate_rcvbuf = 1). |
net.core.default_qdisc & net.ipv4.tcp_congestion_control |
pfifo_fast hoặc fq_codel / cubic |
fq / bbr |
Chuyển từ thuật toán dựa trên mất gói (CUBIC) sang thuật toán đo lường băng thông cổ chai và độ trễ lan truyền (Google TCP BBR), tối ưu hóa tốc độ truyền tải trên mạng di động và WAN. |
7.2 Tài liệu tham khảo & Nguồn đối chiếu Chính thống
- The Linux Kernel Organization: IP Sysctl Documentation (
Documentation/networking/ip-sysctl.rst). Truy cập tại: https://docs.kernel.org/networking/ip-sysctl.html - The Linux Kernel Organization: Documentation for
/proc/sys/fs/(fs.file-max,fs.nr_open& File Descriptor Management). Truy cập tại: https://docs.kernel.org/admin-guide/sysctl/fs.html - freedesktop.org Official Specifications:
systemd.exec(5)— Execution Environment Configuration (LimitNOFILE& Process Resource Limits). Truy cập tại: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html - Cardwell, N., Cheng, Y., Gunn, C. S., Yeganeh, S. H., & Jacobson, V. (2017): BBR: Congestion-Based Congestion Control (ACM Queue, Vol. 14, No. 5 / Communications of the ACM, Vol. 60, No. 2, pp. 58–66). Truy cập tại: https://queue.acm.org/detail.cfm?id=3022184
- Internet Engineering Task Force — IETF RFC 9293 (Eddy, W., Ed., August 2022) & Linux Programmer's Manual: Transmission Control Protocol (TCP) Specification (https://datatracker.ietf.org/doc/html/rfc9293) và
epoll(7)I/O Event Notification Facility (https://man7.org/linux/man-pages/man7/epoll.7.html).