2015-08-28 15 views
12

Perché il seguente codice è lento? E per lento intendo 100x-1000x lento. Esegue ripetutamente la lettura/scrittura direttamente su un socket TCP. La parte curiosa è che rimane lento solo se uso due chiamate di funzione sia per leggere che per scrivere come mostrato di seguito. Se cambio il server o il codice client per utilizzare una singola chiamata di funzione (come nei commenti), diventa super veloce.Perché il socket TCP rallenta se eseguito in più chiamate di sistema?

Codice frammento:

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); 
} 

Ci siamo imbattuti su questo in un programma più ampio, che è stato in realtà usando il buffering stdio. Diventò misteriosamente pigro nel momento in cui le dimensioni del carico utile superarono di poco le dimensioni del buffer. Poi ho fatto un po 'di ricerche con strace e infine ho risolto il problema. Posso risolvere questo problema adottando strategie di buffering, ma mi piacerebbe davvero sapere cosa diavolo sta succedendo qui. Sulla mia macchina, passa da 0,030 s ad oltre un minuto sulla mia macchina (testato sia localmente che su macchine remote) quando cambio le due chiamate lette in una singola chiamata.

Questi test sono stati eseguiti su varie distribuzioni Linux e varie versioni del kernel. Stesso risultato

codice completamente eseguibile con boilerplate rete:

#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; 
} 

EDIT: Questa versione è leggermente diversa dalle altre in quanto frammento di legge/scrive in un ciclo. Quindi, in questa versione, due scritture separate provocano automaticamente due chiamate read() separate, anche se readLoop viene chiamato una sola volta. Ma per il resto il problema rimane ancora.

risposta

15

Interessante. Sei vittima di Nagle's algorithm insieme a TCP delayed acknowledgements.

L'algoritmo di Nagle è un meccanismo utilizzato in TCP per rinviare la trasmissione di piccoli segmenti fino a quando non sono stati accumulati abbastanza dati che rende necessario costruire e inviare un segmento sulla rete. Dalla voce di Wikipedia:

algoritmo di Nagle funziona combinando una serie di piccoli in uscita messaggi, e l'invio di tutti in una volta. In particolare, fino a quando lo è un pacchetto inviato per il quale il mittente non ha ricevuto alcun riconoscimento, il mittente deve mantenere il buffering dell'output finché non ha un valore completo di output del pacchetto , in modo che l'output possa essere inviato tutto in una volta.

Tuttavia, TCP impiega tipicamente qualcosa conosciuto come TCP ritardati riconoscimenti, che è una tecnica che consiste di accumulare insieme un lotto di risposte ACK (poiché TCP utilizza ACK cumulativi), per ridurre il traffico di rete.

che Wikipedia articolo ulteriormente cita questo:

Con entrambi gli algoritmi attivata, le applicazioni che fanno due successive scrive in una connessione TCP, seguita da una lettura che non sarà soddisfatte solo dopo i dati dal la seconda scrittura ha raggiunto la destinazione , con un ritardo costante fino a 500 millisecondi, il "ritardo ACK".

(enfasi mia)

Nel vostro caso specifico, dal momento che il server non invia più dati prima di leggere la risposta, il cliente è la causa del ritardo: se il cliente scrive due volte, the second write will be delayed.

Se l'algoritmo di Nagle viene utilizzato dal mittente, i dati saranno in coda dal mittente fino a quando viene ricevuto un ACK. Se il mittente non invia dati sufficienti per riempire la dimensione massima del segmento (ad esempio, se esegue due piccole scritture seguite da una lettura di blocco), il trasferimento si interromperà fino al timeout del ritardo ACK.

Così, quando il client effettua 2 chiamate di scrittura, questo è ciò che accade:

  1. client invia la prima scrittura.
  2. Il server riceve alcuni dati. Non lo riconosce nella speranza che arriveranno altri dati (in modo da poter raggruppare un gruppo di ACK in un solo ACK).
  3. Il client invia la seconda scrittura. La scrittura precedente non è stata riconosciuta, quindi l'algoritmo di Nagle rimanda la trasmissione fino a quando non arrivano più dati (finché non sono stati raccolti abbastanza dati per creare un segmento) o la scrittura precedente è ACKed.
  4. Il server è stanco di attendere e dopo 500 ms conferma il segmento.
  5. Il client infine completa la seconda scrittura.

Con 1 scrittura, questo è ciò che accade:

  1. client invia la prima scrittura.
  2. Il server riceve alcuni dati. Non lo riconosce nella speranza che arriveranno altri dati (in modo da poter raggruppare un gruppo di ACK in un solo ACK).
  3. Il server scrive sul socket. Un ACK fa parte dell'intestazione TCP, quindi se stai scrivendo, puoi anche riconoscere il segmento precedente senza costi aggiuntivi. Fallo.
  4. Nel frattempo, il client ha scritto una volta, quindi era già in attesa della prossima lettura - non c'era una seconda scrittura in attesa dell'ACK del server.

Se si desidera continuare a scrivere due volte sul lato client, è necessario disabilitare l'algoritmo di Nagle. Questa è la soluzione proposta dall'algoritmo stesso autore:

La soluzione a livello utente è evitare sequenze write-write-leggere prese. scrivere-leggere-scrivere-leggere va bene. scrittura-scrittura-scrittura va bene. Ma scrittura-scrittura-lettura è un killer. Quindi, se puoi, inserisci le tue piccole scritture su TCP e inviale tutte in una volta. L'utilizzo del pacchetto standard di I/O UNIX prima di ogni lettura normalmente funziona.

(See the citation on Wikipedia)

As mentioned by David Schwartz in the comments, questo non può essere il più grande idea per vari motivi, ma illustra il punto e dimostra che questa è infatti la causa del ritardo.

Per disattivarlo, è necessario impostare l'opzione TCP_NODELAY sugli zoccoli con setsockopt(2).

Questo può essere fatto in tcpConnectTo() per il cliente:

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; 
} 

E nel tcpAccept() per il server:

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; 
} 

E 'interessante vedere l'enorme differenza questo fa.

Se preferisci non modificare le opzioni del socket, è sufficiente garantire che il client scriva una volta sola e una sola volta prima della successiva lettura. È ancora possibile leggere il server due volte:

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); 
    } 
} 
+0

Grazie, stavo cercando di controllare, ma non riuscivo a ricordare il nome dell'algoritmo di Nagle. Ma ho ancora una domanda ... le dimensioni particolari non fanno alcuna differenza qui. Puoi indicarmi un riepilogo delle euristiche utilizzate per attivare questo ordinariamente (cioè senza TCP_NODELAY)? – Samee

+0

Per me, penso che sia più facile aggiungere TC_NODELAY piuttosto che fare confusione con la politica di buffering. – Samee

+1

@Samee Sto ancora indagando su questo, ma hai ragione che non dipende dalla particolare dimensione. Aggiungendo 4 letture di 10 byte sul server e 2 scritture di 20 byte sul client, rallenta di nuovo (con 'TCP_NODELAY' disabilitato). Aggiornerò la mia risposta a breve con ulteriori dettagli. –