Việc liên tục sử dụng tùy chọn TCP_NODELAY
(brooker.co.za)- Các vấn đề về độ trễ trong hệ thống phân tán thường lặp đi lặp lại ở chỗ chỉ cần bật
TCP_NODELAYlà giải quyết được, cho thấy hành vi mặc định của TCP có thể không còn phù hợp với workload hiện đại - Thuật toán Nagle được tạo ra trong RFC896 năm 1984 để giảm chi phí header của các gói TCP nhỏ, bằng cách ngăn gửi segment mới trước khi nhận được ACK
- Khi dùng cùng delayed ACK, một bên chờ ACK còn bên kia chờ dữ liệu phản hồi hoặc timer, gây bất lợi cho các ứng dụng pipeline nhạy cảm với độ trễ
- Ngay cả khi RTT nội bộ data center khoảng 500μs, máy chủ hiện đại vẫn có thể xử lý rất nhiều việc trong khoảng thời gian đó, nên lợi ích của việc trì hoãn gửi thêm một RTT là không rõ ràng
- Hệ thống phân tán hiện đại ít gặp vấn đề gói tin 1 byte hơn do TLS, encoding, serialization và kích thước thông điệp ứng dụng; trong môi trường nhạy cảm với độ trễ, việc vô hiệu hóa thuật toán Nagle là lựa chọn tự nhiên hơn
Thiết lập cần xem trước khi debug độ trễ
- Khi hệ thống phân tán gặp vấn đề về độ trễ, việc đầu tiên thường là kiểm tra
TCP_NODELAYđã được bật hay chưa - Nhiều lập trình viên hệ thống phân tán đã mất thời gian vì những vấn đề có thể được giải quyết chỉ bằng một tùy chọn socket đơn giản này
- Sự lặp lại này cho thấy hành vi mặc định của TCP có thể không phù hợp với hệ thống phân tán hiện nay, hoặc bản thân thuật toán Nagle có thể đã lỗi thời
Vấn đề mà thuật toán Nagle muốn giải quyết
- RFC896 là tài liệu năm 1984 xử lý vấn đề gói tin nhỏ
- Vào thời đó, khi gửi qua TCP dữ liệu đi vào từng ký tự như nhập từ bàn phím, mỗi 1 byte dữ liệu lại kèm 40 byte header, gây kém hiệu quả
- Mỗi 1 byte dữ liệu hữu ích phải kèm 40 byte header, tạo ra overhead 4000%
- Có thể chịu được khi tải nhẹ, nhưng bất lợi cho throughput mạng
- Mục tiêu của thuật toán Nagle là phân bổ chi phí TCP header tốt hơn để tăng throughput
- Các gói nhỏ chủ yếu xuất hiện trong ứng dụng tương tác với con người như shell, hoặc các triển khai chuyển dữ liệu từng chút một vào kernel qua nhiều lần gọi
write
- Các gói nhỏ chủ yếu xuất hiện trong ứng dụng tương tác với con người như shell, hoặc các triển khai chuyển dữ liệu từng chút một vào kernel qua nhiều lần gọi
- Hành vi cốt lõi là khi dữ liệu đã gửi trước đó vẫn chưa nhận được ACK, dữ liệu mới sẽ không được gửi ngay dưới dạng một TCP segment riêng
- Thuật toán Nagle thường được giải thích cùng với timer, nhưng bản thân RFC896 không dùng timer riêng ngoài thời gian khứ hồi mạng (RTT)
Độ trễ phát sinh khi kết hợp với delayed ACK
- delayed ACK là cơ chế không gửi xác nhận nhận gói ngay lập tức, mà đợi đến khi có dữ liệu để gửi ngược lại hoặc timer hết hạn
- RFC813 là tài liệu ban đầu năm 1982 đề xuất trì hoãn ACK, mô tả rằng trong một số tình huống, bên nhận có thể trì hoãn việc gửi ACK và đặt timer để gửi sau
- RFC1122 chính thức hóa delayed ACK hơn nữa
- Hai tính năng này riêng lẻ đều hợp lý, nhưng khi dùng cùng nhau có thể tạo ra độ trễ
- Thuật toán Nagle chờ nhận ACK trước khi gửi thêm dữ liệu
- delayed ACK trì hoãn việc gửi ACK cho đến khi dữ liệu phản hồi sẵn sàng hoặc timer hết hạn
- Điều này giúp lấp đầy gói tin, nhưng không tốt cho các ứng dụng pipeline nhạy cảm với độ trễ
- Bình luận của John Nagle trên Hacker News cũng xem vấn đề là sự kết hợp giữa trì hoãn ACK và timer cố định, chứ không phải việc ngăn tinygram
- Đây là ví dụ về hai tính năng giao thức hợp lý khi kết hợp lại tạo ra hành vi không mong muốn, và những tương tác như vậy khiến việc thiết kế giao thức trở nên khó khăn
Những điểm không phù hợp với hệ thống phân tán hiện đại
- Ngay cả khi không có delayed ACK, hành vi của thuật toán Nagle vẫn có thể khác với cách mà hệ thống phân tán hiện đại mong muốn
- Trong môi trường hiện nay, bản thân RTT là một chi phí khó bỏ qua
- Một RTT đơn trong nội bộ data center thường khoảng 500μs
- RTT giữa các data center trong cùng region là vài ms
- Đường truyền toàn cầu có thể lên tới hàng trăm ms
- Máy chủ hiện đại có thể xử lý nhiều công việc ngay cả trong vài trăm μs, nên khó xem việc trì hoãn gửi dữ liệu thêm một RTT là lợi ích rõ ràng
- Lý do ban đầu của thuật toán Nagle là giảm overhead header gấp 40 lần phát sinh từ các gói tin 1 byte
- Cơ sở dữ liệu phân tán và hệ thống phân tán hiện đại nhìn chung không gửi các gói tin 1 byte
- Dữ liệu mà ứng dụng gửi vốn đã lớn hơn
- Có thêm overhead từ các giao thức như TLS
- Cũng có overhead encoding và serialization
- Vấn đề cần tránh thông điệp nhỏ vẫn quan trọng, nhưng trách nhiệm đó về cơ bản đã chuyển sang tầng ứng dụng
- Gửi dữ liệu bọc trong JSON từng byte một là cách không hiệu quả, bất kể có thuật toán Nagle hay không
Vì sao xem TCP_NODELAY là lựa chọn mặc định
- Nếu xây dựng hệ thống phân tán nhạy cảm với độ trễ trên phần cứng cấp data center hiện đại, có thể bật
TCP_NODELAYđể vô hiệu hóa thuật toán Nagle - Xét đến lưu lượng, cấu trúc ứng dụng và hiệu năng phần cứng của các hệ thống hiện đại, thuật toán Nagle có thể không còn cần thiết
- Cũng có thể lập luận rằng
TCP_NODELAYnên là giá trị mặc định - Code gọi
writecho từng byte có thể chậm hơn khiTCP_NODELAYlà mặc định - Nếu hiệu quả là quan trọng, những code như vậy nên được sửa ở phần triển khai ứng dụng thay vì dựa vào thuật toán Nagle
TCP_QUICKACK gần với một lựa chọn bổ trợ hơn
TCP_QUICKACKcó thể được nhắc đến như một phương án thay thế, nhưng khó chọn làm ưu tiên vì thiếu tính portable và có ý nghĩa khá đặc thù- Cần trực tiếp kiểm tra ý nghĩa trong Linux tcp man page
- Vấn đề lớn hơn là
TCP_QUICKACKkhông giải quyết được vấn đề gốc: kernel giữ dữ liệu lâu hơn ý định của chương trình - Nếu chương trình gọi
write(), nó kỳ vọngwrite()thực sự được thực thi
1 bình luận
Ý kiến trên Hacker News
Trong suốt sự nghiệp, tôi đã nhiều lần khắc phục các vấn đề độ trễ do thuật toán Nagle gây ra, và giờ đây nó là thứ đầu tiên tôi nghi ngờ
Bản thân logic thì hợp lý, nhưng không phù hợp với một số workload; tôi cho rằng khi tạo socket, kỹ sư nên được yêu cầu chọn rõ ràng, chứ không nên phó mặc cho giá trị mặc định của hệ điều hành
Vấn đề không phải là tùy chọn tốt hay xấu, mà là có một thiết lập thay đổi cách truyền dữ liệu khá mạnh tay, trong khi nhiều người không biết nó tồn tại
Cho đến nay lần nào tôi cũng tìm ra bug
Ví dụ: https://cloud-haskell.atlassian.net/browse/DP-108 hoặc https://github.com/agentm/curryer/issues/3
Tuy nhiên tôi không đồng ý với câu “không phải là tùy chọn tốt/xấu”
Đây là một heuristic phía kernel để “sửa một cách thần kỳ” các ứng dụng viết sai, và như bài viết nói, ứng dụng bình thường không gọi system call mạng
write()1 bytePhần mềm như vậy cần được sửa
Trường hợp tính năng này có ý nghĩa, theo tôi, chỉ là tình huống hiếm gặp khi bạn là quản trị hệ thống kernel nhưng vì những lý do như chính trị trong đội nhóm mà không thể sửa phần mềm đang chạy trên máy
Ngoài ra, nó làm phần mềm bình thường trở nên phức tạp hơn
Nghĩa là để tăng nhẹ throughput cho phần mềm viết sai, ta phải tắt rõ ràng một thứ phép thuật kỳ lạ, thứ lại tạo ra độ trễ lớn và bất ngờ cho phần mềm được viết đúng
John Nagle nói trong thread được liên kết ở đây rằng delayed ACK còn tệ hơn, và tôi đồng ý
Nhưng mẫu Send/Send/Receive mà thuật toán Nagle làm trầm trọng thêm là một use case hoàn toàn hợp lệ và phổ biến, áp dụng cho mọi thứ làm RPC pipeline trên TCP
Tôi cho rằng cả delayed ACK lẫn thuật toán Nagle đều nên mặc định tắt
Tên cũng nên là kiểu TCP_DELAY, và chỉ bật khi bạn không muốn triển khai buffering cơ bản ở user space
Người ta không nên phải biết những thứ như thế này; hành vi mặc định nên là thứ không gây bất ngờ
writetệ, thì một tùy chọn bật TCP_DELAY trở nên khá kỳ quặcĐiều đó có nghĩa là cần một kỹ sư phần mềm đủ thông minh để biết tùy chọn này, nhưng lại không đủ thông minh để chia nhỏ các lệnh gọi
writehợp lý hoặc tự xây một kiểu buffering giống Nagle tốt hơn, phù hợp với ứng dụng của mìnhNếu không có thứ gì như
io_uringđể bù chi phí system call, phía user space sẽ hoạt động tốt hơnTheo tôi nhớ thì đó là động cơ chính
Kết luận hơi lạ. Thuật toán Nagle rõ ràng là một nỗ lực gom các lần ghi, và bất kể phần cứng, mạng, ứng dụng hay use case, trong một số trường hợp gom ghi vẫn tốt hơn
Ngày nay nhiều hoạt động tính toán vẫn dùng gom ghi, và các ứng dụng mạng cũng hưởng lợi
Các giao thức cấp cao mới hơn như QUIC gom các lần ghi, và về cơ bản chuyển kết nối độc lập cùng xử lý lỗi của TCP lên user space, để giao thức đẩy dữ liệu vào ứng dụng nhanh nhất có thể, còn việc xử lý kết nối và lỗi của từng stream riêng lẻ do ứng dụng đảm nhiệm chứ không phải TCP/IP stack của host hay router
Nếu mạng lại bão hòa như ngày xưa, thuật toán Nagle sẽ quay lại dưới dạng biến thể cho QUIC, có lẽ là chờ gửi gói QUIC cho đến khi đạt tiêu chí nào đó ở sâu hơn trong mã ứng dụng
Mọi thứ trong công nghệ đều được phát minh lại khi phần cứng hoặc phần mềm chạm tới nút thắt cổ chai. Vì hiệu năng của hai bên không tăng cùng tốc độ, rốt cuộc lúc nào cũng sẽ như vậy
Ngoài băng thông, trong những trường hợp số gói mỗi giây bị bão hòa vì các gói nhỏ, thuật toán Nagle cũng hữu ích
Nhờ vậy bạn có thể kết nối tới dịch vụ bằng teletypewriter vật lý, nhưng TCP lại không biết ranh giới message; ngày nay dù có thể nhét một phần kiến thức đó vào, phần mềm thời kỳ đầu thì không thể
Ngược lại, nhiều giao thức không phải TCP như QUIC, SCTP, TP4 cung cấp ranh giới message một cách rõ ràng
Giao diện với hệ thống không phải là một cổng serial được mô phỏng, mà là dựa trên message, nhiều nhất là được tái lắp ráp
Giao thức không có đủ ngữ cảnh để gom cho đúng
Ngược lại, nếu tắt delayed ACK thì sao?
Vấn đề là hành vi bệnh lý nảy sinh khi cơ chế tránh gói nhỏ tương tác với delayed ACK.
Tùy chọn được phơi ra để tắt cơ chế tránh gói nhỏ là
TCP_NODELAY, vậy làm sao tắt delayed ACK?Ý tôi là khi muốn benchmark cả bốn tổ hợp để xem cái nào phù hợp nhất.
Tìm hiểu một chút thì thấy Linux có tùy chọn socket
TCP_QUICKACK, nhưng phải đặt lại mỗi lần nhận dữ liệu.Cũng có
/proc/sys/net/ipv4/tcp_delack_minvà/proc/sys/net/ipv4/tcp_ato_min.FreeBSD có
net.inet.tcp.delayed_ackvànet.inet.tcp.delacktime.TCP_QUICKACKsửa được dạng tệ nhất, nhưng không giải quyết toàn bộ vấn đề.Thuật toán Nagle vẫn có thể chờ tới một round-trip time tối đa trước khi gửi dữ liệu, và nếu theo RFC thì gần như chẳng được lợi gì mà chỉ thêm độ trễ.
TCP_QUICKACKmà phải đặt mỗi lần nhận dữ liệu thì họ đã nghĩ gì vậy?Tại sao lại chỉ muốn tắt nó trong một vài thời điểm?
quickack 1vào cuối route để tắt delayed ACK cho đường đó.Trong một thế giới nơi băng thông bị hạn chế, kích thước gói tối thiểu là 64 byte và còn cần cả khoảng cách giữa các frame, việc gửi một gói TCP cho từng byte là lãng phí băng thông khủng khiếp.
Trên hầu hết mạng Ethernet, kích thước tối thiểu đó đến nay vẫn như vậy, và gửi ACK rỗng cũng tương tự.
Nhưng lập trường mặc định của tôi là thế này: đó không phải là
TCP_NODELAY, mà đơn giản là TCP.Tôi không thật sự bị thuyết phục bởi lập luận rằng Nagle không còn cần thiết nữa.
Telnet ngày nay không còn quan trọng, nhưng tôi nghĩ vẫn có nhiều ứng dụng kiểu như sau:
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: "), v.v.Có thể đây không phải overhead gấp 40 lần, nhưng cũng có thể khoảng gấp 5 lần.
Khi ghi vào file, ta không viết kiểu này rồi kỳ vọng hiệu năng thần kỳ, dù hệ điều hành cũng có buffer riêng.
Không có lý do gì để kỳ vọng khác đi chỉ khi ghi vào socket, và ngay từ đầu Nagle cũng không cứu bạn khỏi overhead của system call.
Tôi xác nhận cả bằng cách đọc code lẫn quan sát hoạt động với
strace.write(2)nhỏ, cách duy nhất hợp lý là đưa vào buffer, nên tôi nghĩ mẫu này giờ không còn phổ biến đến vậy.Trên server, để mở rộng tốt thì thường cần I/O bất đồng bộ, còn trên client thì bị chặn bởi lời gọi mạng cũng là trải nghiệm tệ.
Điều này càng đúng trong môi trường ngày nay, nơi mạng thay đổi thường xuyên và hay rơi khỏi vùng phủ sóng.
Ngay cả bỏ qua khía cạnh mạng, system call cũng khá đắt, nên không tốt cho hiệu năng.
Tôi muốn biết có cách nào tốt để bật TCP_NODELAY cho socket khi không có quyền truy cập mã nguồn ứng dụng không.
Tôi không tìm được thiết lập kernel nào để áp dụng vĩnh viễn, hay lệnh nào để thay đổi sau đó.
Tôi đã có thể tắt delayed ACK bằng cách đưa
quickack 1vào bảng định tuyến, nhưng bật TCP_NODELAY từ bên ngoài ứng dụng có vẻ đặc biệt khó.Gần đây tôi đang gặp đúng vấn đề được mô tả ở đây giữa một ứng dụng do tôi sở hữu và một ứng dụng mã nguồn đóng mà nó tương tác.
socket(2)có chạy được không?Gọi hàm thật rồi làm gì đó như
setsockopt, sau đó trả về socket đã được chỉnh sửa.Nếu ban đầu là
your_app —> serverthì biến thànhyour_app -> localhost_socat -> server.socat có tùy chọn dòng lệnh để đặt
tcp_nodelay.Tuy nhiên bạn phải thuyết phục được ứng dụng mã nguồn đóng kết nối tới localhost.
Nếu nó tra cứu DNS, có thể dùng mục trong
/etc/hostsđể khiến nó kết nối tới localhost.Ứng dụng sẽ giao tiếp với socat qua socket cục bộ, nên
tcp_nodelayphía ứng dụng không ảnh hưởng.setsockoptbằngptracecó được không?/proc//fd/rồi đặt tùy chọn socket cũng có thể hoạt động. Tôi chưa thử.LD_PRELOADKhoảng 15 năm trước tôi chơi một MMO đòi hỏi tính thời gian thực rất cao, và toàn bộ giao tiếp đều là TCP.
Khi bấm nút, hành động của tôi thậm chí còn không hiện lên màn hình cho tới khi gói phản hồi quay về.
Cuối cùng bọn trẻ chơi game đó, gồm cả tôi, đều phát hiện ra rằng bật TCP_NODELAY khiến game mượt hơn rất nhiều.
Hiệu quả đặc biệt lớn với những người chơi ở California, gần máy chủ game.
Một tác dụng phụ thú vị là trước khi thay đổi, nếu TCP stream bị khựng, game sẽ dừng một lúc rồi phát lại rất nhanh các sự kiện nhận được mà nó đã bỏ lỡ.
Thường thì sự kiện đó là cảnh tôi chết.
Sau thay đổi, thay vào đó nó chỉ đơn giản là mất kết nối.
Tập podcast Oxide and Friends liên quan: https://www.youtube.com/watch?v=mqvVmYhclAg
Nếu dùng một ngôn ngữ hiện đại bật TCP_NODELAY mặc định như Go thì không áp dụng :-)
https://github.com/golang/go/issues/57530
Giờ mới biết
Chẳng phải chỉ cần dùng một thư viện networking “hiện đại” là được sao
Không phải lúc nào cũng vậy. Đôi khi là DNS
Bụi ở gần công trường rơi vào khe giữa laser và sợi quang trên một router, làm tín hiệu suy hao đủ nhiều, và thấy mất gói 40–50%
Sau khi xác định điểm mất gói, NOC gửi email cho nhà mạng truyền dẫn đó, và một ngày sau kỹ thuật viên được cử đến đã trả lời cho biết câu chuyện đó
Dù vậy thường vẫn có thể vá để đi vòng nên không phải chuyện lớn
TCP_NODELAYhoặc buffering streamWeb, vốn là một hệ thống thật sự phức tạp, cũng thất bại vì cache