【转载】网络编程中 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 的理解和测试,有错误的地方请不吝赐教。  


相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
6天前
|
运维 监控 算法
解读 C++ 助力的局域网监控电脑网络连接算法
本文探讨了使用C++语言实现局域网监控电脑中网络连接监控的算法。通过将局域网的拓扑结构建模为图(Graph)数据结构,每台电脑作为顶点,网络连接作为边,可高效管理与监控动态变化的网络连接。文章展示了基于深度优先搜索(DFS)的连通性检测算法,用于判断两节点间是否存在路径,助力故障排查与流量优化。C++的高效性能结合图算法,为保障网络秩序与信息安全提供了坚实基础,未来可进一步优化以应对无线网络等新挑战。
|
3天前
|
监控 算法 安全
基于 PHP 语言深度优先搜索算法的局域网网络监控软件研究
在当下数字化时代,局域网作为企业与机构内部信息交互的核心载体,其稳定性与安全性备受关注。局域网网络监控软件随之兴起,成为保障网络正常运转的关键工具。此类软件的高效运行依托于多种数据结构与算法,本文将聚焦深度优先搜索(DFS)算法,探究其在局域网网络监控软件中的应用,并借助 PHP 语言代码示例予以详细阐释。
18 1
|
9天前
|
机器学习/深度学习 存储 算法
基于MobileNet深度学习网络的活体人脸识别检测算法matlab仿真
本内容主要介绍一种基于MobileNet深度学习网络的活体人脸识别检测技术及MQAM调制类型识别方法。完整程序运行效果无水印,需使用Matlab2022a版本。核心代码包含详细中文注释与操作视频。理论概述中提到,传统人脸识别易受非活体攻击影响,而MobileNet通过轻量化的深度可分离卷积结构,在保证准确性的同时提升检测效率。活体人脸与非活体在纹理和光照上存在显著差异,MobileNet可有效提取人脸高级特征,为无线通信领域提供先进的调制类型识别方案。
|
4天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于模糊神经网络的金融序列预测算法matlab仿真
本程序为基于模糊神经网络的金融序列预测算法MATLAB仿真,适用于非线性、不确定性金融数据预测。通过MAD、RSI、KD等指标实现序列预测与收益分析,运行环境为MATLAB2022A,完整程序无水印。算法结合模糊逻辑与神经网络技术,包含输入层、模糊化层、规则层等结构,可有效处理金融市场中的复杂关系,助力投资者制定交易策略。
|
13天前
|
Kubernetes Shell Windows
【Azure K8S | AKS】在AKS的节点中抓取目标POD的网络包方法分享
在AKS中遇到复杂网络问题时,可通过以下步骤进入特定POD抓取网络包进行分析:1. 使用`kubectl get pods`确认Pod所在Node;2. 通过`kubectl node-shell`登录Node;3. 使用`crictl ps`找到Pod的Container ID;4. 获取PID并使用`nsenter`进入Pod的网络空间;5. 在`/var/tmp`目录下使用`tcpdump`抓包。完成后按Ctrl+C停止抓包。
42 12
|
19天前
|
机器学习/深度学习 数据采集 算法
基于PSO粒子群优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真
本项目展示了基于PSO优化的CNN-LSTM-SAM网络时间序列预测算法。使用Matlab2022a开发,完整代码含中文注释及操作视频。算法结合卷积层提取局部特征、LSTM处理长期依赖、自注意力机制捕捉全局特征,通过粒子群优化提升预测精度。适用于金融市场、气象预报等领域,提供高效准确的预测结果。
|
9天前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
17天前
|
监控 算法 安全
公司电脑网络监控场景下 Python 广度优先搜索算法的深度剖析
在数字化办公时代,公司电脑网络监控至关重要。广度优先搜索(BFS)算法在构建网络拓扑、检测安全威胁和优化资源分配方面发挥重要作用。通过Python代码示例展示其应用流程,助力企业提升网络安全与效率。未来,更多创新算法将融入该领域,保障企业数字化发展。
40 10
|
20天前
|
缓存 监控 算法
基于 C# 网络套接字算法的局域网实时监控技术探究
在数字化办公与网络安全需求增长的背景下,局域网实时监控成为企业管理和安全防护的关键。本文介绍C#网络套接字算法在局域网实时监控中的应用,涵盖套接字创建、绑定监听、连接建立和数据传输等操作,并通过代码示例展示其实现方式。服务端和客户端通过套接字进行屏幕截图等数据的实时传输,保障网络稳定与信息安全。同时,文章探讨了算法的优缺点及优化方向,如异步编程、数据压缩与缓存、错误处理与重传机制,以提升系统性能。
39 2
|
3天前
|
机器学习/深度学习 数据采集 算法
基于WOA鲸鱼优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真
本内容介绍了一种基于CNN-LSTM-SAM网络与鲸鱼优化算法(WOA)的时间序列预测方法。算法运行于Matlab2022a,完整程序无水印并附带中文注释及操作视频。核心流程包括数据归一化、种群初始化、适应度计算及参数更新,最终输出最优网络参数完成预测。CNN层提取局部特征,LSTM层捕捉长期依赖关系,自注意力机制聚焦全局特性,全连接层整合特征输出结果,适用于复杂非线性时间序列预测任务。