- Khi gỡ lỗi các vấn đề về độ trễ (latency) trong hệ thống phân tán, mục đầu tiên cần kiểm tra là cấu hình TCP_NODELAY
- Thuật toán Nagle là phương thức được đề xuất trong RFC896 năm 1984, được thiết kế để giảm overhead của header TCP khi truyền các gói nhỏ
- Tuy nhiên, khi kết hợp với cơ chế delayed ACK, việc truyền dữ liệu sẽ bị trì hoãn cho đến khi nhận ACK, từ đó làm xấu đi hiệu năng của các ứng dụng nhạy cảm với độ trễ
- Trong môi trường data center hiện đại, RTT rất ngắn và phần lớn hệ thống đã truyền các thông điệp đủ lớn, nên lợi ích của thuật toán Nagle gần như không còn
- Vì vậy, trong các hệ thống phân tán hiện đại, TCP_NODELAY nên được bật mặc định, và thuật toán Nagle không còn cần thiết nữa
Bối cảnh của thuật toán Nagle
- RFC896 của John Nagle năm 1984 được đề xuất để giải quyết vấn đề overhead 4000% khi 1 byte dữ liệu phải đi kèm header 40 byte, như trong trường hợp truyền dữ liệu nhỏ kiểu nhập bàn phím
- Vấn đề khi đó là mỗi lần người dùng gõ một ký tự lại có một gói nhỏ được gửi đi, làm hiệu quả mạng thấp
- Giải pháp là không cho phép gửi segment mới khi dữ liệu trước đó vẫn chưa được ACK
- Cách tiếp cận này hiệu quả trong môi trường mạng thời đó, nhưng không phù hợp với các hệ thống hiện đại nơi độ trễ (latency) là yếu tố quan trọng
Tương tác giữa thuật toán Nagle và delayed ACK
- Delayed ACK (RFC813, RFC1122) là cơ chế phía nhận không gửi ACK ngay lập tức mà trì hoãn ACK cho đến khi có dữ liệu phản hồi hoặc bộ hẹn giờ hết hạn
- Thuật toán Nagle dừng truyền để chờ ACK, còn delayed ACK lại trì hoãn ACK, nên hai bên rơi vào trạng thái bế tắc vì cùng chờ nhau
- Chính John Nagle cũng gọi sự kết hợp này là “một tổ hợp khủng khiếp”, và chỉ ra rằng hai tính năng được đưa vào độc lập nhưng khi dùng cùng nhau thì gây ra độ trễ
Vấn đề trong môi trường hiện đại
- RTT trong data center vào khoảng 500μs, và ngay cả trong cùng một region cũng chỉ ở mức vài mili giây
- Trong môi trường như vậy, trì hoãn truyền thêm đúng một RTT cũng dẫn đến mất mát hiệu năng
- Ngoài ra, các hệ thống phân tán hiện đại vốn đã truyền những thông điệp đủ lớn do TLS, tuần tự hóa, overhead giao thức, nên vấn đề gói 1 byte gần như không còn tồn tại
- Việc tối ưu thông điệp nhỏ hiện nay đã được xử lý ở tầng ứng dụng
Sự cần thiết của TCP_NODELAY
- Trong các hệ thống phân tán nhạy cảm với độ trễ, nên bật TCP_NODELAY để vô hiệu hóa thuật toán Nagle
- Đây không phải lựa chọn “kém hiệu quả” hay “cấu hình sai”, mà là quyết định phù hợp với phần cứng hiện đại và đặc tính lưu lượng ngày nay
- Tác giả cho rằng TCP_NODELAY nên trở thành giá trị mặc định
- Một số đoạn mã kiểu “gửi ở mỗi lần gọi
write()” có thể chậm hơn, nhưng loại mã đó về bản chất cần được sửa lại
Các tùy chọn liên quan khác
- Tùy chọn TCP_QUICKACK giúp giảm độ trễ ACK, nhưng không phải giải pháp gốc rễ do vấn đề về tính di động và hành vi thiếu nhất quán
- Vấn đề cốt lõi là kernel giữ dữ liệu lâu hơn thời điểm ứng dụng dự định gửi, trong khi dữ liệu lẽ ra phải được truyền ngay khi gọi
write()
Kết luận
- Thuật toán Nagle từng là một phát minh tuyệt vời để nâng cao hiệu quả mạng trong quá khứ, nhưng
trong môi trường mạng tốc độ cao và hệ thống phân tán hiện đại, nó lại trở thành một tính năng lỗi thời gây ra độ trễ
- Vì vậy, luôn bật TCP_NODELAY được xem là nguyên tắc cơ bản trong thiết kế hệ thống hiện đại
1 bình luận
Ý kiến trên Hacker News
Khi đó nhiều host cùng chia sẻ một kênh Ethernet nên dùng CSMA/CD để tránh va chạm
Nhưng ngày nay phần lớn Ethernet là kiến trúc điểm-điểm, trong môi trường full-duplex có thể gửi và nhận đồng thời
Vì vậy CSMA không còn cần thiết nữa, và việc đặt TCP_NODELAY để vô hiệu hóa thuật toán Nagle theo tôi là hợp lý trong đa số trường hợp
Tôi nghĩ việc đặt nó làm mặc định là một trong những sai lầm lớn của lịch sử mạng máy tính
Khoảng năm 2014, khi thay switch trong datacenter, tôi từng phải giữ lại một số thiết bị cũ vì chúng không hỗ trợ 10Mbit half-duplex
Nó giúp tránh tạo ra quá nhiều gói quá nhỏ
Nagle là tối ưu ở tầng TCP, có vai trò gom các gói nhỏ để tăng hiệu quả
Còn CSMA là vấn đề ở tầng vật lý/liên kết dữ liệu, tách biệt với Nagle
Backend viết bằng Go mặc định đã bật TCP_NODELAY, nên không phải nguyên nhân, nhưng phần phân tích về cách nhìn nhận vấn đề của Nagle khá thú vị
Cũng từng có thảo luận trước đó, xem thread này
Với các kiểu giao tiếp dạng chat như giao thức DICOM, đặt TCP_NODELAY=1 giúp tăng thông lượng đáng kể
Xem liên kết liên quan
Tôi nghĩ trong workload ngày nay delayed ACK không còn mang lại nhiều lợi ích
Trong môi trường hiện đại thiên về HTTP, có lẽ tốt hơn là tắt cả Nagle lẫn delayed ACK
Vì RTT giữa các datacenter chỉ ở mức vài trăm micro giây, nên trì hoãn thêm dù chỉ một RTT cũng có thể phản tác dụng
Liên kết wiki
Ứng dụng mới nên là bên quyết định khi nào gửi, khi nào buffer
Trên Linux, đây là một gợi ý cho kernel rằng sẽ còn dữ liệu được gửi tiếp ngay sau đó, rất hữu ích khi gửi riêng header và dữ liệu
Dùng cùng io_uring còn hiệu quả hơn
Sẽ tốt hơn nếu có khả năng xả buffer sau một thông điệp cần phản hồi tức thì
Ngày nay các kênh TCP thường trộn lẫn thông điệp đồng bộ và bất đồng bộ nên mọi thứ còn phức tạp hơn
Tôi ước gì các giao thức như SCTP được dùng rộng rãi hơn
Ngay cả với lớp bọc như TLS, việc tìm ranh giới thông điệp cũng rất phiền
Lý tưởng nhất là có thể bật bit “cho phép buffer” để chia nhỏ một lần truyền lớn, rồi ở cuối chỉ định “gửi ngay”
TCP_CORK là phương án thay thế có phần tương tự, nhưng vẫn hơi thô
File I/O cũng gặp vấn đề tương tự
Nội dung khá thú vị
Ứng dụng nên có khả năng tự điều chỉnh điểm cân bằng giữa độ trễ và thông lượng
Nhưng nếu triển khai ở mức ứng dụng thì phải biết trạng thái unacked data, nên sẽ kém hiệu quả
Chỉ cần có một flush timer 20ms đơn giản thôi cũng đã tốt hơn nhiều