为什么以下代码速度慢?慢一点我的意思是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()
调用。但否则问题仍然存在。
答案 0 :(得分:16)
有趣。您是Nagle's algorithm和TCP 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次写入,就会发生这种情况:
ACK
是TCP标头的一部分,因此如果您正在编写,您也可以免费承认之前的段。做吧。如果您想在客户端继续写两次,则需要禁用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);
}
}