如果在多个系统调用中完成,为什么TCP套接字会减慢?

时间:2015-08-28 15:43:54

标签: c linux performance sockets tcp

为什么以下代码速度慢?慢一点我的意思是100x-1000x慢。它只是直接在TCP套接字上重复执行读/写操作。奇怪的是,只有当我使用两个函数调用进行读取和写入时,它仍然很慢,如下所示。如果我更改服务器或客户端代码以使用单个函数调用(如在注释中),它将变得非常快。

代码段:

class ActorB extends Actor {
  def receive: Receive = caching(Nil)

  def caching(cached: List[String]): Receive = {
    case msg: String => 
      context.become(caching(msg :: cached))
    case Start => 
      cached.reverse.foreach(println)
      context.become(caching(Nil))
  }
}

我们在一个更大的程序中偶然发现了这个,它实际上是使用stdio缓冲。当有效载荷大小超过缓冲区大小的那一刻,它神秘地变得缓慢。然后我用int main(...) { int sock = ...; // open TCP socket int i; char buf[100000]; for(i=0;i<2000;++i) { if(amServer) { write(sock,buf,10); // read(sock,buf,20); read(sock,buf,10); read(sock,buf,10); }else { read(sock,buf,10); // write(sock,buf,20); write(sock,buf,10); write(sock,buf,10); } } close(sock); } 进行了一些挖掘,最后将问题归结为此问题。我可以通过愚弄缓冲策略来解决这个问题,但我真的很想知道这里到底发生了什么。在我的机器上,当我将两个读取调用更改为单个调用时,我的机器上的时间从0.030秒到超过一分钟(在本地和远程机器上测试)。

这些测试是在各种Linux发行版和各种内核版本上完成的。结果相同。

具有网络样板的完全可运行的代码:

strace

编辑:此版本与其他代码段稍有不同,因为它在循环中读/写。因此,在此版本中,即使#include <netdb.h> #include <stdbool.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <netinet/ip.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> static int getsockaddr(const char* name,const char* port, struct sockaddr* res) { struct addrinfo* list; if(getaddrinfo(name,port,NULL,&list) < 0) return -1; for(;list!=NULL && list->ai_family!=AF_INET;list=list->ai_next); if(!list) return -1; memcpy(res,list->ai_addr,list->ai_addrlen); freeaddrinfo(list); return 0; } // used as sock=tcpConnect(...); ...; close(sock); static int tcpConnect(struct sockaddr_in* sa) { int outsock; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; if(connect(outsock,(struct sockaddr*)sa,sizeof(*sa))<0) return -1; return outsock; } int tcpConnectTo(const char* server, const char* port) { struct sockaddr_in sa; if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1; int sock=tcpConnect(&sa); if(sock<0) return -1; return sock; } int tcpListenAny(const char* portn) { in_port_t port; int outsock; if(sscanf(portn,"%hu",&port)<1) return -1; if((outsock=socket(AF_INET,SOCK_STREAM,0))<0) return -1; int reuse = 1; if(setsockopt(outsock,SOL_SOCKET,SO_REUSEADDR, (const char*)&reuse,sizeof(reuse))<0) return fprintf(stderr,"setsockopt() failed\n"),-1; struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(port) , .sin_addr={INADDR_ANY} }; if(bind(outsock,(struct sockaddr*)&sa,sizeof(sa))<0) return fprintf(stderr,"Bind failed\n"),-1; if(listen(outsock,SOMAXCONN)<0) return fprintf(stderr,"Listen failed\n"),-1; return outsock; } int tcpAccept(const char* port) { int listenSock, sock; listenSock = tcpListenAny(port); if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1; close(listenSock); return sock; } void writeLoop(int fd,const char* buf,size_t n) { // Don't even bother incrementing buffer pointer while(n) n-=write(fd,buf,n); } void readLoop(int fd,char* buf,size_t n) { while(n) n-=read(fd,buf,n); } int main(int argc,char* argv[]) { if(argc<3) { fprintf(stderr,"Usage: round {server_addr|--} port\n"); return -1; } bool amServer = (strcmp("--",argv[1])==0); int sock; if(amServer) sock=tcpAccept(argv[2]); else sock=tcpConnectTo(argv[1],argv[2]); if(sock<0) { fprintf(stderr,"Connection failed\n"); return -1; } int i; char buf[100000] = { 0 }; for(i=0;i<4000;++i) { if(amServer) { writeLoop(sock,buf,10); readLoop(sock,buf,20); //readLoop(sock,buf,10); //readLoop(sock,buf,10); }else { readLoop(sock,buf,10); writeLoop(sock,buf,20); //writeLoop(sock,buf,10); //writeLoop(sock,buf,10); } } close(sock); return 0; } 仅调用一次,两次单独的写入也会自动导致两个单独的read()调用。但否则问题仍然存在。

1 个答案:

答案 0 :(得分:16)

有趣。您是Nagle's algorithmTCP delayed acknowledgements的受害者。

Nagle的算法是TCP中用于推迟小段传输的机制,直到累积了足够的数据,这使得值得通过网络构建和发送段。来自维基百科的文章:

  

Nagle的算法通过组合一些小的传出来工作   消息,并立即发送所有消息。具体来说,只要有   是发送方未收到确认的已发送数据包,   发件人应该保持缓冲输出,直到它满了   数据包的输出值,以便可以一次性发送输出。

但是,TCP通常采用称为 TCP延迟确认的东西,这种技术包括将一批ACK回复(因为TCP使用累积ACKS)累积在一起,以减少网络流量。 / p>

维基百科的文章进一步提到了这一点:

  

启用两种算法后,连续执行两次的应用程序   写入TCP连接,然后读取不会   直到第二次写入的数据到达之后才完成   目的地,经历长达500毫秒的持续延迟,   “ACK延迟”

(强调我的)

在您的特定情况下,由于服务器在读取回复之前未发送更多数据,因此客户端会导致延迟:如果客户端写入两次the second write will be delayed

  

如果发送方使用Nagle的算法,数据将是   由发送方排队,直到收到ACK。如果发件人没有   发送足够的数据以填充最大段大小(例如,如果它   执行两次小写操作,然后执行阻塞读取)然后执行   传输将暂停至ACK延迟超时。

因此,当客户端进行2次写入调用时,会发生以下情况:

  1. 客户发出第一次写作。
  2. 服务器接收一些数据。它不承认它希望有更多数据到达(因此它可以在一个ACK中批量处理一堆ACK)。
  3. 客户发出第二次写入。之前的写入尚未被确认,因此Nagle的算法推迟传输,直到更多数据到达(直到收集到足够的数据来创建段)或者先前的写入被确认。
  4. 服务器厌倦了等待,500毫秒后确认该段。
  5. 客户端最后完成第二次写入。
  6. 只需1次写入,就会发生这种情况:

    1. 客户发出第一次写作。
    2. 服务器接收一些数据。它不承认它希望有更多数据到达(因此它可以在一个ACK中批量处理一堆ACK)。
    3. 服务器写入套接字。 ACK是TCP标头的一部分,因此如果您正在编写,您也可以免费承认之前的段。做吧。
    4. 同时,客户端写了一次,所以它已经在等待下一次读取 - 没有第二次写入等待服务器的ACK
    5. 如果您想在客户端继续写两次,则需要禁用Nagle算法。这是算法作者自己提出的解决方案:

        

      用户级解决方案是避免写 - 读 - 读序列   插座。 write-read-write-read很好。写 - 写 - 写得很好。但   写 - 写 - 读是一个杀手。所以,如果可以,请缓冲你的小事   写入TCP并立即发送它们。使用标准的UNIX I / O.   在每次阅读通常有效之前,打包和刷新写入。

      See the citation on Wikipedia

      As mentioned by David Schwartz in the comments,出于各种原因,这可能不是最好的主意,但它说明了这一点,并表明这确实导致了延迟。

      要禁用它,您需要在TCP_NODELAY的套接字上设置setsockopt(2)选项。

      这可以在tcpConnectTo()为客户完成:

      int tcpConnectTo(const char* server, const char* port)
      {
          struct sockaddr_in sa;
          if(getsockaddr(server,port,(struct sockaddr*)&sa)<0) return -1;
          int sock=tcpConnect(&sa); if(sock<0) return -1;
      
          int val = 1;
          if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
              perror("setsockopt(2) error");
      
          return sock;
      }
      

      在服务器的tcpAccept()中:

      int tcpAccept(const char* port)
      {
          int listenSock, sock;
          listenSock = tcpListenAny(port);
          if((sock=accept(listenSock,0,0))<0) return fprintf(stderr,"Accept failed\n"),-1;
          close(listenSock);
      
          int val = 1;
          if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)) < 0)
              perror("setsockopt(2) error");
      
          return sock;
      }
      

      看到它产生的巨大差异很有意思。

      如果你不想弄乱套接字选项,那么足以确保客户端在下一次读取之前写入一次 - 并且只写一次。您仍然可以让服务器读取两次:

      for(i=0;i<4000;++i)
      {
          if(amServer)
          { writeLoop(sock,buf,10);
              //readLoop(sock,buf,20);
              readLoop(sock,buf,10);
              readLoop(sock,buf,10);
          }else
          { readLoop(sock,buf,10);
              writeLoop(sock,buf,20);
              //writeLoop(sock,buf,10);
              //writeLoop(sock,buf,10);
          }
      }