悲劇發生在這種情況,假設一端發送資料并等待另一端應答,協定上分為頭部和資料,發送的時候不幸地選擇了write-write,然後再read,也就是先發送頭部,再發送資料,最後等待應答。發送端的僞代碼是這樣
write(head);
write(body);
read(response);
接收端的處理代碼類似這樣:
read(request);
process(request);
write(response);
這裡假設head和body都比較小,當預設啟用nagle算法,并且是第一次發送的時候,根據nagle算法,第一個段head可以立即發送,因為沒有等待确認的段;接收端收到head,但是包不完整,繼續等待body達到并延遲ack;發送端繼續寫入body,這時候nagle算法起作用了,因為head還沒有被ack,是以body要延遲發送。這就造成了發送端和接收端都在等待對方發送資料的現象,發送端等待接收端ack head以便繼續發送body,而接收端在等待發送方發送body并延遲ack,悲劇的無以言語。這種時候隻有等待一端逾時并發送資料才能繼續往下走。
正因為nagle算法和delayed ack的影響,再加上這種write-write-read的程式設計方式造成了很多網貼在讨論為什麼自己寫的網絡程式性能那麼差。然後很多人會在文章裡建議禁用nagle算法吧,設定tcp_nodelay為true即可禁用nagle算法。但是這真的是解決問題的唯一辦法和最好辦法嗎?
其實問題不是出在nagle算法身上的,問題是出在write-write-read這種應用程式設計上。禁用nagle算法可以暫時解決問題,但是禁用nagle算法也帶來很大壞處,網絡中充塞着小封包,網絡的使用率上不去,在極端情況下,大量小封包導緻網絡擁塞甚至崩潰。是以,能不禁止還是不禁止的好,後面我們會說下什麼情況下才需要禁用nagle算法。對大多數應用來說,一般都是連續的請求——應答模型,有請求同時有應答,那麼請求包的ack其實可以延遲到跟響應一起發送,在這種情況下,其實你隻要避免write-write-read形式的調用就可以避免延遲現象,利用writev做聚集寫或者将head和body一起寫,然後再read,變成write-read-write-read的形式來調用,就無需禁用nagle算法也可以做到不延遲。
下面我們将做個實際的代碼測試來結束讨論。這個例子很簡單,用戶端發送一行資料到伺服器,伺服器簡單地将這行資料傳回。用戶端發送的時候可以選擇分兩次發,還是一次發送。分兩次發就是write-write-read,一次發就是write-read-write-read,可以看看兩種形式下延遲的差異。注意,在windows上測試下面的代碼,用戶端和伺服器必須分在兩台機器上,似乎winsock對loopback連接配接的處理不一樣。
伺服器源碼:
package net.fnil.nagle;
import java.io.bufferedreader;
import java.io.inputstream;
import java.io.inputstreamreader;
import java.io.outputstream;
import java.net.inetsocketaddress;
import java.net.serversocket;
import java.net.socket;
public class server {
public static void main(string[] args) throws exception {
serversocket serversocket = new serversocket();
serversocket.bind(new inetsocketaddress(8000));
system.out.println("server startup at 8000");
for (;;) {
socket socket = serversocket.accept();
inputstream in = socket.getinputstream();
outputstream out = socket.getoutputstream();
while (true) {
try {
bufferedreader reader = new bufferedreader(new inputstreamreader(in));
string line = reader.readline();
out.write((line + "\r\n").getbytes());
}
catch (exception e) {
break;
}
}
}
}
服務端綁定到本地8000端口,并監聽連接配接,連上來的時候就阻塞讀取一行資料,并将資料傳回給用戶端。
用戶端代碼:
public class client {
// 是否分開寫head和body
boolean writesplit = false;
string host = "localhost";
if (args.length >= 1) {
host = args[0];
if (args.length >= 2) {
writesplit = boolean.valueof(args[1]);
system.out.println("writesplit:" + writesplit);
socket socket = new socket();
socket.connect(new inetsocketaddress(host, 8000));
inputstream in = socket.getinputstream();
outputstream out = socket.getoutputstream();
bufferedreader reader = new bufferedreader(new inputstreamreader(in));
string head = "hello ";
string body = "world\r\n";
for (int i = 0; i < 10; i++) {
long label = system.currenttimemillis();
if (writesplit) {
out.write(head.getbytes());
out.write(body.getbytes());
else {
out.write((head + body).getbytes());
string line = reader.readline();
system.out.println("rtt:" + (system.currenttimemillis() - label) + " ,receive:" + line);
in.close();
out.close();
socket.close();
用戶端通過一個writesplit變量來控制是否分開寫head和body,如果為true,則先寫head再寫body,否則将head加上body一次寫入。用戶端的邏輯也很簡單,連上伺服器,發送一行,等待應答并列印rtt,循環10次最後關閉連接配接。
首先,我們将writesplit設定為true,也就是分兩次寫入一行,在我本機測試的結果,我的機器是ubuntu 11.10:
writesplit:true
rtt:8 ,receive:hello world
rtt:40 ,receive:hello world
rtt:39 ,receive:hello world
可以看到,每次請求到應答的時間間隔都在40ms,除了第一次。linux的delayed ack是40ms,而不是原來以為的200ms。第一次立即ack,似乎跟linux的quickack mode有關,這裡我不是特别清楚,有比較清楚的同學請指教。
接下來,我們還是将writesplit設定為true,但是用戶端禁用nagle算法,也就是用戶端代碼在connect之前加上一行:
socket.settcpnodelay(true);
再跑下測試:
rtt:0 ,receive:hello world
rtt:1 ,receive:hello world
這時候就正常多了,大部分rtt時間都在1毫秒以下。果然禁用nagle算法可以解決延遲問題。
如果我們不禁用nagle算法,而将writesplit設定為false,也就是将head和body一次寫入,再次運作測試(記的将settcpnodelay這行删除):
writesplit:false
rtt:7 ,receive:hello world
最後一個問題,什麼情況下才應該禁用nagle算法?當你的應用不是這種連續的請求——應答模型,而是需要實時地單向發送很多小資料的時候或者請求是有間隔的,則應該禁用nagle算法來提高響應性。一個最明顯是例子是telnet應用,你總是希望敲入一行資料後能立即發送給伺服器,然後馬上看到應答,而不是說我要連續敲入很多指令或者等待200ms才能看到應答。
上面是我對nagle算法和delayed ack的了解和測試,有錯誤的地方請不吝賜教。
文章轉自莊周夢蝶 ,原文釋出時間 2011-06-30