网络编程中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算法也可以做到不延迟。

   writev是系统调用,在Java里是用到 GatheringByteChannel.write(ByteBuffer[] srcs, int offset, int length)方法来做聚集写。这里可能还有一点值的提下,很多同学看java nio框架几乎都不用这个writev调用,这是有原因的。主要是因为Java的write本身对ByteBuffer有做临时缓存,而writev没有做缓存,导致测试来看write反而比writev更高效,因此通常会更推荐用户将head和body放到同一个Buffer里来避免调用writev。

   下面我们将做个实际的代码测试来结束讨论。这个例子很简单,客户端发送一行数据到服务器,服务器简单地将这行数据返回。客户端发送的时候可以选择分两次发,还是一次发送。分两次发就是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的理解和测试,有错误的地方请不吝赐教。

文章转自庄周梦蝶  ,原文发布时间 2011-06-30

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
3天前
|
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停止抓包。
31 12
|
27天前
|
算法 数据安全/隐私保护 计算机视觉
基于FPGA的图像双线性插值算法verilog实现,包括tb测试文件和MATLAB辅助验证
本项目展示了256×256图像通过双线性插值放大至512×512的效果,无水印展示。使用Matlab 2022a和Vivado 2019.2开发,提供完整代码及详细中文注释、操作视频。核心程序实现图像缩放,并在Matlab中验证效果。双线性插值算法通过FPGA高效实现图像缩放,确保质量。
|
3月前
|
Kubernetes 网络协议 应用服务中间件
Kubernetes Ingress:灵活的集群外部网络访问的利器
《Kubernetes Ingress:集群外部访问的利器-打造灵活的集群网络》介绍了如何通过Ingress实现Kubernetes集群的外部访问。前提条件是已拥有Kubernetes集群并安装了kubectl工具。文章详细讲解了Ingress的基本组成(Ingress Controller和资源对象),选择合适的版本,以及具体的安装步骤,如下载配置文件、部署Nginx Ingress Controller等。此外,还提供了常见问题的解决方案,例如镜像下载失败的应对措施。最后,通过部署示例应用展示了Ingress的实际使用方法。
103 2
|
3月前
|
人工智能 弹性计算 运维
ACK Edge与IDC:高效容器网络通信新突破
本文介绍如何基于ACK Edge以及高效的容器网络插件管理IDC进行容器化。
|
4月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
107 1
|
5月前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
118 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
5月前
|
Kubernetes 应用服务中间件 nginx
搭建Kubernetes v1.31.1服务器集群,采用Calico网络技术
在阿里云服务器上部署k8s集群,一、3台k8s服务器,1个Master节点,2个工作节点,采用Calico网络技术。二、部署nginx服务到k8s集群,并验证nginx服务运行状态。
1678 1
|
5月前
|
Kubernetes 网络协议 网络安全
k8s中网络连接问题
【10月更文挑战第3天】
324 7
|
5月前
|
Kubernetes 容器
基于Ubuntu-22.04安装K8s-v1.28.2实验(三)数据卷挂载NFS(网络文件系统)
基于Ubuntu-22.04安装K8s-v1.28.2实验(三)数据卷挂载NFS(网络文件系统)
283 0
|
6月前
|
Kubernetes 容器 Perl
Kubernetes网络插件体系及flannel基础
文章主要介绍了Kubernetes网络插件体系,特别是flannel网络模型的工作原理、配置和测试方法。
163 3

热门文章

最新文章