Problemi di progettazione : invio di piccoli segmenti di dati su TCP con Winsock

Quando è necessario inviare pacchetti di dati di piccole dimensioni su TCP, la progettazione dell'applicazione Winsock è particolarmente critica. Una progettazione che non tiene conto dell'interazione del riconoscimento ritardato, dell'algoritmo Nagle e del buffering Winsock può influire notevolmente sulle prestazioni. Questo articolo illustra questi problemi usando un paio di studi di casi. Deriva anche una serie di raccomandazioni per l'invio efficiente di pacchetti di dati di piccole dimensioni da un'applicazione Winsock.

Versione originale del prodotto: Winsock
Numero KB originale: 214397

Background

Quando uno stack TCP Microsoft riceve un pacchetto di dati, viene disattivato un timer di ritardo di 200 ms. Quando viene inviato un ACK, il timer di ritardo viene reimpostato e inizierà un altro ritardo di 200 ms quando viene ricevuto il pacchetto di dati successivo. Per aumentare l'efficienza nelle applicazioni Internet e Intranet, lo stack TCP usa i criteri seguenti per decidere quando inviare un ACK sui pacchetti di dati ricevuti:

  • Se il secondo pacchetto di dati viene ricevuto prima della scadenza del timer di ritardo, viene inviato l'ACK.
  • Se sono presenti dati da inviare nella stessa direzione dell'ACK prima che venga ricevuto il secondo pacchetto di dati e il timer di ritardo scada, l'ACK viene sottoposto a piggyback con il segmento di dati e inviato immediatamente.
  • Alla scadenza del timer di ritardo, viene inviato l'ACK.

Per evitare che pacchetti di dati di piccole dimensioni congestino la rete, lo stack TCP abilita l'algoritmo Nagle per impostazione predefinita, che unisce un piccolo buffer di dati da più chiamate di invio e ritarda l'invio fino a quando non viene ricevuto un ACK per il pacchetto di dati precedente inviato dall'host remoto. Di seguito sono riportate due eccezioni all'algoritmo Nagle:

  • Se lo stack ha raggruppato un buffer di dati più grande dell'unità di trasmissione massima (MTU), viene inviato immediatamente un pacchetto di dimensioni complete senza attendere l'ACK dall'host remoto. In una rete Ethernet, la MTU per TCP/IP è di 1460 byte.

  • L'opzione TCP_NODELAY socket viene applicata per disabilitare l'algoritmo Nagle in modo che i pacchetti di dati di piccole dimensioni vengano recapitati all'host remoto senza ritardi.

Per ottimizzare le prestazioni a livello di applicazione, Winsock copia i buffer di dati dalle chiamate di invio dell'applicazione a un buffer del kernel Winsock. Quindi, lo stack usa la propria euristica (ad esempio l'algoritmo Nagle) per determinare quando inserire effettivamente il pacchetto sul filo. È possibile modificare la quantità di buffer del kernel Winsock allocato al socket usando l'opzione SO_SNDBUF (per impostazione predefinita è 8K). Se necessario, Winsock può memorizzare nel buffer più delle dimensioni del SO_SNDBUF buffer. Nella maggior parte dei casi, il completamento dell'invio nell'applicazione indica solo che il buffer di dati in una chiamata di invio dell'applicazione viene copiato nel buffer del kernel Winsock e non indica che i dati hanno raggiunto il supporto di rete. L'unica eccezione è quando si disabilita il buffer di Winsock impostando su SO_SNDBUF 0.

Winsock usa le regole seguenti per indicare un completamento dell'invio all'applicazione (a seconda di come viene richiamato l'invio, la notifica di completamento potrebbe essere la funzione che restituisce da una chiamata di blocco, la segnalazione di un evento o la chiamata di una funzione di notifica e così via):

  • Se il socket è ancora entro SO_SNDBUF quota, Winsock copia i dati dall'invio dell'applicazione e indica il completamento dell'invio all'applicazione.

  • Se il socket è oltre SO_SNDBUF la quota e nel buffer dello stack kernel è ancora presente un solo invio memorizzato nel buffer dello stack, Winsock copia i dati dall'invio dell'applicazione e indica il completamento dell'invio all'applicazione.

  • Se il socket è oltre SO_SNDBUF la quota ed è presente più di un invio memorizzato nel buffer dello stack kernel, Winsock copia i dati dall'invio dell'applicazione. Winsock non indica il completamento dell'invio all'applicazione finché lo stack non viene completato in modo sufficiente per ripristinare il socket entro SO_SNDBUF la quota o una sola condizione di invio in sospeso.

Case study 1

Un client TCP Winsock deve inviare 10000 record a un server TCP Winsock per l'archiviazione in un database. Le dimensioni dei record variano da 20 byte a 100 byte. Per semplificare la logica dell'applicazione, la progettazione è la seguente:

  • Il client blocca solo l'invio. Il server blocca recv solo.
  • Il socket client imposta su SO_SNDBUF 0 in modo che ogni record eserciti in un singolo segmento di dati.
  • Il server chiama recv in un ciclo. Il buffer inserito in recv è di 200 byte in modo che ogni record possa essere ricevuto in una recv sola chiamata.

Prestazioni

Durante i test, lo sviluppatore rileva che il client può inviare solo cinque record al secondo al server. Il totale di 10000 record, massimo 976 KB di dati (10000 * 100/1024), richiede più di mezz'ora per l'invio al server.

Analisi

Poiché il client non imposta l'opzione TCP_NODELAY , l'algoritmo Nagle forza lo stack TCP ad attendere un ACK prima di poter inviare un altro pacchetto sul cavo. Tuttavia, il client ha disabilitato il buffer di Winsock impostando l'opzione SO_SNDBUF su 0. Pertanto, le 10000 chiamate di invio devono essere inviate e ack singolarmente. Ogni ACK viene ritardato di 200 ms perché nello stack TCP del server si verifica quanto segue:

  • Quando il server ottiene un pacchetto, il timer di ritardo di 200 ms si spegne.
  • Il server non deve inviare nulla, pertanto non è possibile eseguire il piggyback dell'ACK.
  • Il client non invierà un altro pacchetto a meno che non venga riconosciuto il pacchetto precedente.
  • Il timer di ritardo nel server scade e l'ACK viene rispedito.

Come migliorare

Questa progettazione comporta due problemi. In primo luogo, c'è il problema del timer di ritardo. Il client deve essere in grado di inviare due pacchetti al server entro 200 ms. Poiché il client usa l'algoritmo Nagle per impostazione predefinita, deve usare solo il buffer winsock predefinito e non impostare su SO_SNDBUF 0. Dopo che lo stack TCP ha raggruppato un buffer più grande dell'unità di trasmissione massima (MTU), viene inviato immediatamente un pacchetto di dimensioni complete senza attendere l'ACK dall'host remoto.

In secondo luogo, questo progetto chiama un invio per ogni record di dimensioni così ridotte. L'invio di questa piccola dimensione non è efficiente. In questo caso, lo sviluppatore potrebbe voler aggiungere ogni record a 100 byte e inviare 80 record alla volta da una chiamata di invio client. Per comunicare al server quanti record verranno inviati in totale, il client potrebbe voler avviare la comunicazione con un'intestazione con dimensioni corrette contenente il numero di record da seguire.

Case study 2

Un'applicazione client TCP Winsock apre due connessioni con un'applicazione server TCP Winsock che fornisce il servizio di quotazioni azionarie. La prima connessione viene usata come canale di comando per inviare il simbolo stock al server. La seconda connessione viene usata come canale dati per ricevere l'offerta azionaria. Dopo aver stabilito le due connessioni, il client invia un simbolo stock al server tramite il canale di comando e attende il ritorno della quotazione azionaria attraverso il canale dati. Invia la successiva richiesta di simbolo di azioni al server solo dopo la ricezione della prima quotazione azionaria. Il client e il server non impostano l'opzione SO_SNDBUF e TCP_NODELAY .

  • Prestazioni

    Durante i test, lo sviluppatore rileva che il client può ottenere solo cinque virgolette al secondo.

  • Analisi

    Questo design consente solo una richiesta di quotazione azionaria in sospeso alla volta. Il primo simbolo stock viene inviato al server tramite il canale di comando (connessione) e una risposta viene immediatamente inviata dal server al client tramite il canale dati (connessione). Quindi, il client invia immediatamente la seconda richiesta di simbolo stock e l'invio restituisce immediatamente come buffer della richiesta nella chiamata di invio viene copiato nel buffer del kernel Winsock. Tuttavia, lo stack TCP client non può inviare immediatamente la richiesta dal buffer del kernel perché il primo invio sul canale di comando non è ancora stato confermato. Dopo la scadenza del timer di ritardo di 200 ms nel canale del comando del server, l'ACK per la prima richiesta di simboli torna al client. La seconda richiesta di offerta viene quindi inviata correttamente al server dopo un ritardo di 200 ms. L'offerta per il secondo simbolo di azioni torna immediatamente attraverso il canale dati perché, in questo momento, il timer di ritardo nel canale dati del client è scaduto. Un ACK per la risposta virgolette precedente viene ricevuto dal server. Tenere presente che il client non è riuscito a inviare una seconda richiesta di quotazione azionaria per 200 ms, concedendo così il tempo per la scadenza del timer di ritardo nel client e l'invio di un ACK al server. Di conseguenza, il client ottiene la seconda risposta virgolette e può inviare un'altra richiesta di offerta, soggetta allo stesso ciclo.

  • Come migliorare

    La progettazione di due connessioni (canale) non è necessaria qui. Se si usa una sola connessione per la richiesta di offerta azionaria e la risposta, l'ACK per la richiesta di offerta può essere sottoposto a piggyback nella risposta di offerta e tornare immediatamente. Per migliorare ulteriormente le prestazioni, il client può eseguire il multiplex delle richieste di quotazioni azionarie multiple in una sola chiamata di invio al server e il server può anche multiplex multiple quote responses in una chiamata di invio al client. Se la progettazione dei due canali unidirezionali è necessaria per qualche motivo, entrambi i lati devono impostare l'opzione TCP_NODELAY in modo che i pacchetti di piccole dimensioni possano essere inviati immediatamente senza dover attendere un ACK per il pacchetto precedente.

Suggerimenti

Anche se questi due case study sono stati creati, consentono di illustrare alcuni scenari di casi peggiori. Quando si progetta un'applicazione che prevede invii estesi di piccoli segmenti di dati e recvs, è necessario prendere in considerazione le linee guida seguenti:

  • Se i segmenti di dati non sono critici nel tempo, l'applicazione deve raggrupparli in un blocco di dati più grande da passare a una chiamata di invio. Poiché è probabile che il buffer di invio venga copiato nel buffer del kernel Winsock, il buffer non deve essere troppo grande. Poco meno di 8K è efficace. Finché il kernel Winsock ottiene un blocco più grande dell'MTU, invierà più pacchetti di dimensioni complete e un ultimo pacchetto con tutto ciò che rimane. Il lato di invio, ad eccezione dell'ultimo pacchetto, non verrà raggiunto dal timer di ritardo di 200 ms. L'ultimo pacchetto, se si verifica un pacchetto con numero dispari, è ancora soggetto all'algoritmo di riconoscimento ritardato. Se lo stack finale di invio ottiene un altro blocco più grande dell'MTU, può comunque ignorare l'algoritmo Nagle.

  • Se possibile, evitare connessioni socket con flusso di dati unidirezionale. Le comunicazioni su socket unidirezionali sono più facilmente interessate dagli algoritmi nagle e di riconoscimento ritardato. Se la comunicazione segue una richiesta e un flusso di risposta, è necessario usare un singolo socket per eseguire entrambe le operazioni di invio e recvs in modo che l'ACK possa essere sottoposto a piggyback nella risposta.

  • Se tutti i segmenti di dati di piccole dimensioni devono essere inviati immediatamente, impostare TCP_NODELAY l'opzione sul lato di invio.

  • A meno che non si voglia garantire che un pacchetto venga inviato in rete quando winsock indica un completamento dell'invio, non è consigliabile impostare su SO_SNDBUF zero. Infatti, il buffer predefinito 8K è stato euristicamente determinato a funzionare bene per la maggior parte delle situazioni e non è consigliabile modificarlo a meno che non sia stato testato che la nuova impostazione del buffer Winsock offre prestazioni migliori rispetto a quelle predefinite. Inoltre, l'impostazione su SO_SNDBUF zero è per lo più utile per le applicazioni che ese necessario trasferire dati in blocco. Anche in questo caso, per ottenere la massima efficienza, è consigliabile usarlo in combinazione con il doppio buffering (più di un invio in sospeso in un determinato momento) e l'I/O sovrapposto.

  • Se il recapito dei dati non deve essere garantito, usare UDP.

Riferimenti

Per altre informazioni sul riconoscimento ritardato e sull'algoritmo Nagle, vedere quanto segue:

Braden, R.[1989], RFC 1122, Requirements for Internet Hosts--Communication Layers, Internet Engineering Task Force.