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

简介:

       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);
      这里假设 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 连接的处理不一样。      

服务器源码:  
?
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 ;
                 }
             }
         }
     }
}
服务端绑定到本地 8000 端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。  

客户端代码:  
?
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 变量来控制是否分开写 head 和 body ,如果为 true,则先写 head 再写 body,否则将 head 加上 body 一次写入。客户端的逻辑也很简单,连上服务器,发送一行,等待应答并打印 RTT,循环 10 次最后关闭连接。

      首先,我们将 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
      可以看到,每次请求到应答的时间间隔都在 40ms,除了第一次。linux 的 delayed ack 是 40ms,而不是原来以为的 200ms 。第一次立即 ACK ,似乎跟 linux 的 quickack mode 有关,这里我不是特别清楚,有比较清楚的同学请指教。  
         接下来,我们还是将 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
      这时候就正常多了,大部分 RTT 时间都在 1 毫秒以下。果然禁用 Nagle 算法可以解决延迟问题。  
      如果我们不禁用 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 的理解和测试,有错误的地方请不吝赐教。  


相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
目录
相关文章
|
26天前
|
机器学习/深度学习 存储 算法
神经网络分类算法原理详解
神经网络分类算法原理详解
49 0
|
1月前
|
机器学习/深度学习 算法 PyTorch
RPN(Region Proposal Networks)候选区域网络算法解析(附PyTorch代码)
RPN(Region Proposal Networks)候选区域网络算法解析(附PyTorch代码)
225 1
|
1月前
|
机器学习/深度学习 自然语言处理 搜索推荐
神经网络算法 —— Embedding(嵌入)!!
神经网络算法 —— Embedding(嵌入)!!
60 1
|
1月前
|
传感器 算法 计算机视觉
基于肤色模型和中值滤波的手部检测算法FPGA实现,包括tb测试文件和MATLAB辅助验证
该内容是关于一个基于肤色模型和中值滤波的手部检测算法的描述,包括算法的运行效果图和所使用的软件版本(matlab2022a, vivado2019.2)。算法分为肤色分割和中值滤波两步,其中肤色模型在YCbCr色彩空间定义,中值滤波用于去除噪声。提供了一段核心程序代码,用于处理图像数据并在FPGA上实现。最终,检测结果输出到&quot;hand.txt&quot;文件。
|
1月前
|
机器学习/深度学习 算法 计算机视觉
基于yolov2深度学习网络的视频手部检测算法matlab仿真
基于yolov2深度学习网络的视频手部检测算法matlab仿真
|
1月前
|
网络协议 Shell Linux
【Shell 命令集合 网络通讯 】⭐⭐⭐Linux 测试与目标主机之间的网络连接ping 命令 使用指南
【Shell 命令集合 网络通讯 】⭐⭐⭐Linux 测试与目标主机之间的网络连接ping 命令 使用指南
43 1
|
1月前
|
机器学习/深度学习 算法 计算机视觉
|
11天前
|
机器学习/深度学习 算法
【MATLAB】GA_ELM神经网络时序预测算法
【MATLAB】GA_ELM神经网络时序预测算法
282 9
|
11天前
|
机器学习/深度学习 数据采集 算法
|
14天前
|
机器学习/深度学习 自然语言处理 算法

推荐镜像

更多