【转载】网络编程中 Nagle 算法和 Delayed ACK 的测试


     Nagle 算法 的立意是良好的,是为了避免网络中充塞小封包,可以提高网络的利用率。但是当 Nagle 算法遇到 delayed ACK 悲剧就发生了。Delayed ACK 的本意也是为了提高 TCP 性能,在应答数据中捎带上 ACK,同时避免 糊涂窗口综合症 ,也可以一个 ACK 确认多个段来节省开销。
    悲剧发生在这种情况,假设一端发送数据并等待另一端应答,协议上分为头部和数据,发送的时候不幸地选择了 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 端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。

客户端代码:
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 变量来控制是否分开写 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: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 
      可以看到,每次请求到应答的时间间隔都在 40ms,除了第一次。linux 的 delayed ack 是 40ms,而不是原来以为的 200ms 。第一次立即 ACK ,似乎跟 linux 的 quickack mode 有关,这里我不是特别清楚,有比较清楚的同学请指教。
       接下来,我们还是将 writeSplit 设置为 true ,但是客户端禁用 Nagle 算法,也就是客户端代码在 connect 之前加上一行:
        Socket socket =  new  Socket(); 
        socket.setTcpNoDelay( true ); 
        socket.connect( new  InetSocketAddress(host, 8000));
      再跑下测试:
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 
      这时候就正常多了,大部分 RTT 时间都在 1 毫秒以下。果然禁用 Nagle 算法可以解决延迟问题。
      如果我们不禁用 Nagle 算法,而将 writeSplit 设置为 false,也就是将 head 和 body 一次写入,再次运行测试(记的将 setTcpNoDelay 这行删除):
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 的理解和测试,有错误的地方请不吝赐教。







你可能感兴趣的:(nagle)