Nagle 算法 的立意是良好的,是为了避免网络中充塞小封包,可以提高网络的利用率。但是当 Nagle 算法遇到 delayed ACK 悲剧就发生了。Delayed ACK 的本意也是为了提高 TCP 性能,在应答数据中捎带上 ACK,同时避免 糊涂窗口综合症 ,也可以一个 ACK 确认多个段来节省开销。
悲剧发生在这种情况,假设一端发送数据并等待另一端应答,协议上分为头部和数据,发送的时候不幸地选择了 write-write,然后再 read,也就是先发送头部,再发送数据,最后等待应答。发送端的伪代码是这样:
1
2
3
|
write(head);
write(body);
read(response);
|
1
2
3
|
read(request);
process(request);
write(response);
|
正因为 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 连接的处理不一样。
服务器源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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
;
}
}
}
}
}
|
客户端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
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.Socket;
public
class
Client {
public
static
void
main(String[] args)
throws
Exception {
// 是否分开写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 设置为 true,也就是分两次写入一行,在我本机测试的结果,我的机器是 ubuntu 11.10:
1
2
3
4
5
6
7
8
9
10
11
|
WriteSplit:
true
RTT:8 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:39 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
RTT:40 ,receive:hello world
|
接下来,我们还是将 writeSplit 设置为 true ,但是客户端禁用 Nagle 算法,也就是客户端代码在 connect 之前加上一行:
1
2
3
|
Socket socket =
new
Socket();
socket.setTcpNoDelay(
true
);
socket.connect(
new
InetSocketAddress(host,
8000
));
|
1
2
3
4
5
6
7
8
9
10
11
|
WriteSplit:
true
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:1 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
|
如果我们不禁用 Nagle 算法,而将 writeSplit 设置为 false,也就是将 head 和 body 一次写入,再次运行测试(记的将 setTcpNoDelay 这行删除):
1
2
3
4
5
6
7
8
9
10
11
|
WriteSplit:
false
RTT:7 ,receive:hello world
RTT:1 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
RTT:0 ,receive:hello world
|
结果跟禁用 Nagle 算法的效果类似。既然这样,我们还有什么理由一定要禁用 Nagle 算法呢?通过我在
xmemcached
的压测中的测试,启用 Nagle 算法在小数据的存取上甚至有一定的效率优势,memcached 协议本身就是个连续的请求应答的模型。上面的测试如果在 windows 上跑,会发现 RTT 最大会在 200ms 以上,可见 winsock 的delayed ack 超时是 200ms 。
最后一个问题,什么情况下才应该禁用 Nagle 算法?当你的应用不是这种连续的请求应答模型,而是需要实时地单向发送很多小数据的时候或者请求是有间隔的,则应该禁用 Nagle 算法来提高响应性。一个最明显是例子是 telnet 应用,你总是希望敲入一行数据后能立即发送给服务器,然后马上看到应答,而不是说我要连续敲入很多命令或者等待 200ms才能看到应答。
上面是我对 Nagle 算法和 delayed ACK 的理解和测试,有错误的地方请不吝赐教。