18 điểm bởi GN⁺ 2025-12-23 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2025-12-23
Ý kiến trên Hacker News
  • Giải thích bối cảnh ra đời của thuật toán Nagle từ thời mạng đa điểm trước đây
    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 muốn biết liệu động cơ liên quan đến CSMA có thực sự tồn tại trong thiết kế của thuật toán Nagle hay chỉ đơn giản là nhắc đến bối cảnh lịch sử
    • Thực ra thuật toán Nagle chỉ nhằm mục đích gộp gói tin (coalescing)
      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
    • Tham khảo thêm: Ethernet dùng CSMA/CD, còn WiFi dùng CSMA/CA
      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
    • Nagle khá hợp lý khi ứng dụng không quan tâm đến kích thước gói tin hoặc không nhạy với độ trễ
      Nó giúp tránh tạo ra quá nhiều gói quá nhỏ
    • Có vẻ đang có sự nhầm lẫn giữa các layer mạng
      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
  • Tôi phát hiện bài này khi đang debug vấn đề độ trễ mạng trong quá trình phát triển game
    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
    • Cũng khuyến nghị bài viết hay của Julia Evans tại đâ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ể
    • Không biết bạn đang phát triển game gì. Tôi cũng rất thích làm game với EbitengineGolang nên khá quan tâm
  • Chính Nagle từng nói khoảng 10 năm trước rằng vấn đề thực sự là delayed ACK
    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
    • Bài gốc cũng đề cập điều này
      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
  • Trong tiếng Ba Lan, “nagle” có nghĩa là “đột nhiên”, và tôi ngạc nhiên vì nó quá hợp với tên thuật toán
    • Có vẻ như đây là một ví dụ khác của Nominative determinism
      Liên kết wiki
    • Thú vị ở chỗ khi “NODELAY on” thì gửi đột ngột ngay, còn khi “off” thì gửi dồn một lượt, như thể nghĩa của từ này bao trùm cả hai cấu hình
    • Thực ra đây là thuật toán dựa trên RFC 896 do John Nagle viết
  • Tôi thấy việc đặt thuật toán Nagle làm mặc định của kernel là điều kỳ lạ
    Ứng dụng mới nên là bên quyết định khi nào gửi, khi nào buffer
  • Tôi ngạc nhiên vì bài viết không nhắc đến MSG_MORE
    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
    • Thực ra còn có thể gửi nhiều mảnh dữ liệu chỉ trong một system call mà không cần copy
  • Tôi nghĩ vấn đề của thuật toán Nagle là socket API không có chức năng gửi ngay (flush)
    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
    • Tôi đồng ý rằng stream API không có chức năng flush là một thiếu sót thiết kế rõ ràng
    • Tôi hiểu triết lý UNIX muốn coi network I/O giống như file, nhưng nếu ngay từ đầu đã có API hướng thông điệp thì có lẽ đã không gặp vấn đề này
      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
    • Có lẽ có thể gắn MSG_MORE cho mọi lệnh send rồi chỉ bỏ nó ở lệnh cuối để tạo hiệu ứng flush một cách gián tiếp
    • Stream API thực sự bất tiện ở nhiều mặt
      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ự
    • Tôi tò mò TCP_CORK là gì
  • (2024) Thảo luận trước đó có tại liên kết này
  • Tập podcast của Oxide and Friends có bàn về chủ đề này
    Nội dung khá thú vị
    • Oxide là công ty đang thiết kế lại server OS và phần cứng, nên cách tiếp cận xem xét lại các giao thức truyền thống rất phù hợp với triết lý thương hiệu của họ
  • Thuật toán Nagle tạo cảm giác như đưa policy vào kernel, nên khá gượng ép
    Ứ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
    • Nếu không có delayed ack thì đây là một thuật toán hợp lý, và lý do nó tồn tại trong TCP stack là vì người ta muốn giải quyết vấn đề ngay ở layer đó
      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ả
    • Về mặt lý thuyết thì đúng, nhưng thực tế là đa số code userspace không quan tâm đến tầng thấp của mạng
      Chỉ cần có một flush timer 20ms đơn giản thôi cũng đã tốt hơn nhiều
    • Thực ra TCP_NODELAY được thiết lập theo từng socket, nên có thể xem nó gần với một quyết định ở userspace do chính ứng dụng lựa chọn
    • Trade-off của một chương trình có thể ảnh hưởng đến chương trình khác, nên tôi nghĩ kernel cần đóng vai trò trọng tài từ góc nhìn toàn hệ thống