Trong các kiến trúc trạm chuyển tiếp mạng hiện đại, độ trễ phản hồi đóng vai trò sống còn không kém gì băng thông thuần túy. Các cổng proxy biên doanh nghiệp, đường hầm dự phòng WireGuard qua TCP và trạm kết thúc mã hóa TLS thường xuyên tiếp nhận lưu lượng truyền tải nhiều gigabit, đồng thời phải đảm bảo các gói tin tương tác luôn phản hồi tức thì. Tuy nhiên, khi tốc độ đường truyền vật lý tăng từ 1 Gbps lên 100 Gbps, một vấn đề suy giảm hiệu năng tiềm ẩn đã lan rộng: hiện tượng phình đệm (bufferbloat) tại hàng đợi ghi bên trong ngăn xếp mạng nhân Linux.
Khi ứng dụng truyền tải dữ liệu qua kết nối TCP, nhân Linux tự động điều chỉnh kích thước bộ đệm gửi (sk->sk_sndbuf) theo cơ chế tự thích ứng. Trên các tuyến kết nối liên lục địa có tích số băng thông - độ trễ (BDP) lớn, nhân thường xuyên nâng dung lượng bộ đệm gửi lên 4 MB, 8 MB hoặc thậm chí 16 MB cho mỗi socket. Dù khoảng đệm hào phóng này bảo đảm đường truyền tốc độ cao luôn được lấp đầy, nó lại tạo ra một nút thắt cổ chai cấu trúc nghiêm trọng: các ứng dụng tầng người dùng liên tục đẩy ồ ạt hàng megabyte dữ liệu vào hàng đợi nhân nhanh hơn nhiều so với tốc độ mạng có thể phát đi.
Cơ chế đệm dữ liệu thiếu kiểm soát này tạo ra hàng đợi ứ đọng ngầm ngay trong bộ nhớ nhân. Nếu một gói tin ưu tiên cao—chẳng hạn như một thao tác phím tương tác từ xa, gói thoại VoIP hoặc tín hiệu bắt tay xoay vòng khóa bảo mật—cần đi qua đường hầm, nó sẽ bị kẹt lại phía sau hàng megabyte dữ liệu tải tệp lớn đang chờ phát. Để loại bỏ độ trễ xếp hàng nhân tạo này và trả lại quyền kiểm soát luồng dữ liệu chính xác cho tầng ứng dụng, nhân Linux cung cấp tùy chọn socket TCP_NOTSENT_LOWAT.
Điểm nghẽn hàng đợi ghi: Sự phình to BDP và hiện tượng Bufferbloat trong nhân
Nguyên nhân cốt lõi của hiện tượng phình đệm ở chiều ghi bắt nguồn từ sự phân tách giữa vòng lặp sự kiện tầng người dùng và máy trạng thái TCP trong nhân. Một tiến trình bất đồng bộ (sử dụng epoll hoặc io_uring) sẽ ghi dữ liệu bất cứ khi nào socket báo hiệu còn chỗ trống trong bộ đệm gửi (EPOLLOUT):
- Định cỡ đệm trong nhân: Nhân tính toán dung lượng
sk_sndbuf = 2 * BDP, cấp phát tới 16 MB bộ nhớ đệm nhằm bảo đảm cửa sổ truyền tải không bao giờ bị thắt lại. - Ứng dụng ghi ồ ạt: Do nhân báo trạng thái sẵn sàng ghi chừng nào lượng dữ liệu nằm dưới ngưỡng
sk_sndbuf, ứng dụng lập tức trút sạch bộ đệm người dùng vào hàng đợi socket của nhân. - Tích tụ hàng đợi ứ đọng: Dữ liệu chưa thể gửi ngay lên dây mạng (do giới hạn điều tiết nhịp phát pacing, cửa sổ tắc nghẽn hoặc cửa sổ nhận của đối tác) bị dồn lại trong hàng đợi ghi (
sk_write_queue).
Hàng đợi ghi socket truyền thống khi thiếu TCP_NOTSENT_LOWAT:
[ Vòng lặp sự kiện ứng dụng ] ──► write(16 MB)
│
▼
[ Bộ đệm socket nhân: sk_sndbuf (Cấp phát 16 MB) ]
┌─────────────────────────────────────────────────────────────┬─────────────┐
│ Dữ liệu chưa gửi nằm ứ đọng trong nhân (15.5 MB chờ phát) │ Dữ liệu │
│ (Gây nghẽn đầu hàng nghiêm trọng cho gói tin ưu tiên mới) │ trên dây │
└─────────────────────────────────────────────────────────────┴─────────────┘
│
▼ (Thoát chậm ra mạng vật lý theo nhịp)
[ Giao diện mạng vật lý ]
Khi hàng đợi ứ đọng tích tụ trong nhân, hai rủi ro lớn xuất hiện:
- Hiện tượng nghẽn đầu hàng (Head-of-Line Blocking): Ứng dụng hoàn toàn mất khả năng hủy bỏ, đổi độ ưu tiên hoặc chèn gói tin khẩn cấp vì các byte đã bị chôn sâu trong hàng đợi của nhân.
- Hiệu ứng nhân bản bộ nhớ: Nếu một máy chủ chuyển tiếp duy trì 20.000 kết nối đồng thời, mỗi socket giữ 8 MB dữ liệu chưa phát trong các cấu trúc
sk_buff, máy chủ sẽ tiêu tốn tới 160 GB RAM vật lý chỉ để chứa các hàng đợi thụ động.
Cơ chế bên trong TCP_NOTSENT_LOWAT: Nguyên lý nhân và sự kiện socket
Được phát triển bởi lập trình viên nhân Eric Dumazet, TCP_NOTSENT_LOWAT thay đổi căn bản điều kiện kích hoạt trạng thái sẵn sàng ghi của socket. Thay vì đánh giá khả năng ghi dựa trên khoảng trống còn lại trong sk_sndbuf, nhân kiểm tra trực tiếp khối lượng byte chưa gửi (unsent bytes) đang thực sự tồn đọng trong hàng đợi ghi.
Nhân định nghĩa lượng byte chưa gửi là độ chênh lệch giữa tổng số byte đã xếp hàng ghi và số byte hiện đang truyền trên dây mạng:
$$\text{Lượng byte chưa gửi} = \text{write_seq} - \text{snd_nxt}$$
Khi TCP_NOTSENT_LOWAT được thiết lập ở một ngưỡng cụ thể (chẳng hạn 16 KB hoặc 32 KB), nhân sẽ tạm dừng gửi sự kiện EPOLLOUT cho đến khi khối lượng dữ liệu chưa phát giảm xuống dưới mức giới hạn này.
Quy trình điều tiết ghi với TCP_NOTSENT_LOWAT = 16 KB:
[ Bộ đệm ứng dụng ] ── (Lưu tại tầng người dùng để phân loại ưu tiên)
│
│ 1. Vòng lặp sự kiện chờ tín hiệu EPOLLOUT
▼
[ Hàng đợi socket nhân: sk_write_queue ]
┌──────────────────────────────────────────────┬────────────────────────────┐
│ Dữ liệu tồn chưa gửi: Giới hạn <= 16 KB │ Dữ liệu đang bay trên dây │
│ (Nạp gói tin vào nhân đúng lúc vừa vặn) │ (Lấp đầy toàn bộ ống BDP) │
└──────────────────────────────────────────────┴────────────────────────────┘
│
▼ (Chuyển tiếp tức thì ra card mạng)
[ Giao diện mạng vật lý ]
1. Tách biệt dung lượng đệm socket khỏi lượng dữ liệu chưa gửi
Điểm mấu chốt là TCP_NOTSENT_LOWAT không hề thu hẹp sk_sndbuf hay bóp nghẹt cửa sổ tắc nghẽn TCP (snd_cwnd). Ngăn xếp mạng vẫn duy trì một đường ống truyền tải lớn xuyên qua các kết nối đường dài có BDP cao.
Thay vào đó, cơ chế này tách rời dung lượng cửa sổ truyền tải khỏi thói quen đệm trước quá mức của ứng dụng. Nhân tiếp tục theo dõi các gói xác nhận và mở rộng lượng dữ liệu trên dây, nhưng tầng người dùng chỉ bơm các khối dữ liệu mới vào nhân khi các khối trước đó đã được đóng gói và phát đi.
2. Tín hiệu EPOLLOUT hướng sự kiện và điều tiết dòng chảy
Trong mã nguồn ngăn xếp mạng Linux (net/ipv4/tcp_output.c), hàm nội bộ tcp_write_xmit() liên tục đối chiếu độ sâu hàng đợi:
/* Đoạn kiểm tra bên trong hàm tcp_stream_memory_free */
static inline bool tcp_stream_memory_free(const struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 notsent = tp->write_seq - tp->snd_nxt;
return notsent < tp->notsent_lowat;
}
Bất cứ khi nào card mạng phát xong các gói tin đã định lịch và dịch chuyển con trỏ snd_nxt về phía trước, lượng byte tồn đọng sẽ giảm xuống. Ngay khi notsent thấp hơn tp->notsent_lowat, nhân phát tín hiệu đánh thức trên hàng đợi chờ của socket, thông báo cho epoll_wait() rằng bộ mô tả đã sẵn sàng đón nhận thêm dữ liệu.
Đánh đổi giữa độ trễ và bộ nhớ: Sysctl toàn cục so với tinh chỉnh từng socket
Quản trị viên mạng có thể thiết lập giới hạn hàng đợi ghi trên toàn hệ thống hoặc tinh chỉnh cụ thể cho từng bộ mô tả socket độc lập.
Cấu trúc phân cấp cấu hình điều tiết hàng đợi ghi TCP:
[ Tham số Sysctl toàn cục ]
net.ipv4.tcp_notsent_lowat = 16384 (Mặc định cho toàn bộ kết nối)
│
├─► Áp dụng tự động cho các socket TCP thông thường
│
└─► Ghi đè bằng setsockopt() cho từng socket chuyên biệt:
setsockopt(fd, SOL_TCP, TCP_NOTSENT_LOWAT, &val, sizeof(val))
1. Áp dụng toàn hệ thống qua net.ipv4.tcp_notsent_lowat
Phương pháp nhanh chóng để ngăn chặn phình đệm trên toàn bộ máy chủ là sử dụng biến hệ thống sysctl:
# Kiểm tra giá trị hiện tại (thường là 0xFFFFFFFF, nghĩa là đang tắt)
cat /proc/sys/net/ipv4/tcp_notsent_lowat
# Áp dụng ngưỡng 16 KB cho toàn bộ kết nối TCP trên hệ thống
sudo sysctl -w "net.ipv4.tcp_notsent_lowat = 16384"
Việc cấu hình tcp_notsent_lowat = 16384 tạo ra một trần giới hạn 16 KB cho dữ liệu chưa gửi trên mọi socket, ngăn chặn các tác vụ chạy nền chiếm dụng tài nguyên bộ nhớ một cách lãng phí.
2. Kiểm soát chi tiết từng socket trong hệ thống chuyển tiếp đa người dùng
Trong các máy chủ chuyển tiếp lưu lượng lớn, việc gán giá trị bằng mã nguồn cho từng socket mang lại tính linh hoạt vượt trội. Tiến trình có thể thiết lập ngưỡng chặt chẽ (như 4 KB hoặc 8 KB) cho các kết nối tương tác, trong khi gán 32 KB hoặc 64 KB cho các luồng truyền phát khối lượng lớn:
int notsent_limit = 16384; /* Ngưỡng giới hạn 16 KB dữ liệu chưa gửi */
if (setsockopt(client_fd, SOL_TCP, TCP_NOTSENT_LOWAT, ¬sent_limit, sizeof(notsent_limit)) < 0) {
perror("Lỗi thiết lập TCP_NOTSENT_LOWAT");
return -1;
}
Kịch bản vận hành: Tích hợp TCP_NOTSENT_LOWAT vào daemon tốc độ cao
Khi điều chỉnh một proxy mạng bất đồng bộ để tận dụng TCP_NOTSENT_LOWAT, các kỹ sư cần đồng bộ việc quản lý bộ đệm tầng người dùng với chu kỳ sự kiện:
- Giữ bộ đệm tại tầng người dùng: Tránh việc gọi hàm ghi ồ ạt quá mức cần thiết. Ứng dụng nên lưu trữ các khung dữ liệu đang chờ trong bộ nhớ của tiến trình (như hàng đợi vòng hoặc hàng đợi phân đoạn).
- Tuân thủ tín hiệu EPOLLOUT: Khi nhận thông báo
EPOLLOUT, chỉ ghi một khối dữ liệu vừa vặn đến ranh giới logic tiếp theo rồi trả quyền điều khiển về vòng lặp sự kiện. - Xử lý chế độ kích hoạt theo cạnh (Edge-Triggered): Khi chạy
EPOLLET, tiếp tục ghi trong vòng lặp cho đến khi hàmwrite()trả về mã lỗiEAGAINhoặcEWOULDBLOCK, báo hiệu ngưỡng giới hạn đã đạt tới.
Lưu đồ vòng lặp sự kiện ghi socket độ trễ thấp:
[ Có dữ liệu mới sẵn sàng ] ──► Kiểm tra hàng đợi ưu tiên trong bộ nhớ
│
▼
Socket có cờ EPOLLOUT không?
/ \
(Có) (Không)
/ \
▼ ▼
Ghi một khối dữ liệu vào socket Chờ sự kiện epoll
│
▼
Hàm ghi có trả về EAGAIN?
/ \
(Có) (Không)
/ \
▼ ▼
Xóa trạng thái sẵn sàng Ghi tiếp hoặc quay lại vòng lặp
Lợi ích ghép kênh: Xóa bỏ nghẽn đầu hàng trong các luồng dữ liệu đường hầm
Đối với các giao thức ghép nhiều luồng logic trên một kết nối truyền tải TCP duy nhất (như HTTP/2, gRPC hoặc các đường hầm VPN đóng gói), TCP_NOTSENT_LOWAT mang lại lợi ích cấu trúc to lớn: triệt tiêu hoàn toàn sự tắc nghẽn giữa không gian người dùng và nhân mạng.
Xét trường hợp một đường hầm doanh nghiệp vừa tải tệp sao lưu dung lượng lớn vừa truyền lệnh thao tác bàn phím dòng lệnh:
- Khi không có TCP_NOTSENT_LOWAT: Tệp sao lưu lập tức lấp đầy 8 MB bộ đệm nhân. Khi một lệnh bàn phím khẩn cấp được gõ, nó bị xếp vào cuối hàng đợi 8 MB, chịu thêm 200 ms độ trễ nhân tạo.
- Khi có TCP_NOTSENT_LOWAT: Nhân chỉ giữ tối đa 16 KB dữ liệu tệp tại bất kỳ thời điểm nào. Khi xuất hiện gói tin bàn phím, bộ điều phối tầng ứng dụng lập tức đẩy nó lên đầu hàng đợi người dùng. Chỉ sau vài phần triệu giây, ngay khi khối 16 KB trước đó được giải phóng, gói tin khẩn cấp được phát ra mạng ngay tức thì.
Kiến trúc RiceGuard: Kiểm soát bộ đệm thích ứng cho trạm chuyển tiếp tốc độ cao
VPNRice tích hợp cơ chế TCP_NOTSENT_LOWAT trực tiếp vào cấu trúc điều phối truyền tải của RiceGuard. Đối với các kết nối di động hay thay đổi mạng, các trạm chuyển tiếp doanh nghiệp và các đầu mút truyền tải đa phương tiện chạy qua đường hầm TCP, việc giảm thiểu phình đệm là yếu tố then chốt bảo đảm trải nghiệm mượt mà.
Tiến trình RiceGuard liên tục đánh giá các thông số kết nối—bao gồm ước lượng thời gian RTT thời gian thực, nhịp độ phát gói và nhu cầu thông lượng của máy trạm. Hệ thống tự động điều chỉnh TCP_NOTSENT_LOWAT trong khoảng từ 8 KB đến 32 KB, đảm bảo đường truyền luôn tận dụng tối đa băng thông mà độ trễ hàng đợi vẫn được duy trì ở mức dưới mili giây. Nhờ sự kết hợp hài hòa giữa các tùy chọn socket của nhân và thuật toán lập lịch thông minh, RiceGuard mang lại tốc độ phản hồi nhanh nhạy, độ ổn định vững chắc và quyền riêng tư số an toàn cho người dùng khắp thế giới.



