Problemas de design – Enviar pequenos segmentos de dados por TCP com Winsock

Quando você precisa enviar pacotes de dados pequenos por TCP, o design do aplicativo Winsock é especialmente crítico. Um design que não leva em conta a interação de reconhecimento atrasado, o algoritmo Nagle e o buffer do Winsock podem afetar drasticamente o desempenho. Este artigo aborda essas questões usando alguns estudos de casos. Ele também deriva uma série de recomendações para enviar pacotes de dados pequenos com eficiência de um aplicativo Winsock.

Versão original do produto: Winsock
Número de KB original: 214397

Background

Quando uma pilha TCP da Microsoft recebe um pacote de dados, um temporizador de atraso de 200 ms será desativado. Quando um ACK é enviado, o temporizador de atraso é redefinido e iniciará outro atraso de 200 ms quando o próximo pacote de dados for recebido. Para aumentar a eficiência na Internet e nos aplicativos de intranet, a pilha TCP usa os seguintes critérios para decidir quando enviar um ACK em pacotes de dados recebidos:

  • Se o segundo pacote de dados for recebido antes do temporizador de atraso expirar, o ACK será enviado.
  • Se houver dados a serem enviados na mesma direção que o ACK antes que o segundo pacote de dados seja recebido e o temporizador de atraso expirar, a ACK será recolhida com o segmento de dados e enviada imediatamente.
  • Quando o temporizador de atraso expira, o ACK é enviado.

Para evitar que pequenos pacotes de dados congestionem a rede, a pilha TCP permite o algoritmo Nagle por padrão, que agrupa um pequeno buffer de dados de várias chamadas de envio e atrasa o envio até que um ACK para o pacote de dados anterior enviado seja recebido do host remoto. A seguir estão duas exceções ao algoritmo Nagle:

  • Se a pilha tiver agrupado um buffer de dados maior que a MTU (Unidade de Transmissão Máxima), um pacote de tamanho completo será enviado imediatamente sem aguardar o ACK do host remoto. Em uma rede Ethernet, o MTU para TCP/IP é 1460 bytes.

  • A TCP_NODELAY opção soquete é aplicada para desabilitar o algoritmo Nagle para que os pacotes de dados pequenos sejam entregues ao host remoto sem demora.

Para otimizar o desempenho na camada de aplicativo, o Winsock copia buffers de dados do aplicativo para enviar chamadas para um buffer do kernel do Winsock. Em seguida, a pilha usa sua própria heurística (como o algoritmo Nagle) para determinar quando realmente colocar o pacote no fio. Você pode alterar a quantidade de buffer do kernel winsock alocado para o soquete usando a opção SO_SNDBUF (é 8K por padrão). Se necessário, o Winsock pode fazer buffer maior do que o tamanho do SO_SNDBUF buffer. Na maioria dos casos, a conclusão de envio no aplicativo indica apenas que o buffer de dados em uma chamada de envio de aplicativo é copiado para o buffer do kernel do Winsock e não indica que os dados atingiram o meio de rede. A única exceção é quando você desabilitar o buffer do Winsock definindo SO_SNDBUF como 0.

Winsock usa as seguintes regras para indicar uma conclusão de envio para o aplicativo (dependendo de como o envio é invocado, a notificação de conclusão pode ser a função retornando de uma chamada de bloqueio, sinalizando um evento ou chamando uma função de notificação e assim por diante):

  • Se o soquete ainda estiver dentro de SO_SNDBUF cota, Winsock copiará os dados do envio do aplicativo e indicará a conclusão do envio para o aplicativo.

  • Se o soquete estiver além SO_SNDBUF da cota e houver apenas um envio em buffer anterior ainda no buffer do kernel de pilha, o Winsock copiará os dados do envio do aplicativo e indicará a conclusão do envio para o aplicativo.

  • Se o soquete estiver além SO_SNDBUF da cota e houver mais de um envio em buffer anterior no buffer do kernel de pilha, o Winsock copiará os dados do envio do aplicativo. Winsock não indica a conclusão de envio para o aplicativo até que a pilha conclua envios suficientes para colocar o soquete de volta dentro SO_SNDBUF da cota ou apenas uma condição de envio pendente.

Estudo de caso 1

Um cliente do Winsock TCP precisa enviar 10.000 registros para um servidor TCP do Winsock para armazenar em um banco de dados. O tamanho dos registros varia de 20 bytes a 100 bytes de comprimento. Para simplificar a lógica do aplicativo, o design é o seguinte:

  • O cliente só bloqueia o envio. O servidor só bloqueia recv .
  • O soquete do cliente define o SO_SNDBUF como 0 para que cada registro saia em um único segmento de dados.
  • O servidor chama recv em um loop. O buffer postado em recv é de 200 bytes para que cada registro possa ser recebido em uma recv chamada.

Desempenho

Durante o teste, o desenvolvedor descobre que o cliente só poderia enviar cinco registros por segundo para o servidor. O total de 10.000 registros, máximo de 976 kb de dados (10000 * 100 /1024), leva mais de meia hora para enviar ao servidor.

Análise

Como o cliente não define a opção TCP_NODELAY , o algoritmo Nagle força a pilha TCP a aguardar por um ACK antes que ele possa enviar outro pacote no fio. No entanto, o cliente desabilitou o buffer do Winsock definindo a opção SO_SNDBUF como 0. Portanto, as 10.000 chamadas de envio devem ser enviadas e a ACK'ed individualmente. Cada ACK está atrasada em 200 ms porque o seguinte ocorre na pilha TCP do servidor:

  • Quando o servidor recebe um pacote, o temporizador de atraso de 200 ms dispara.
  • O servidor não precisa enviar nada de volta, portanto, a ACK não pode ser retornada.
  • O cliente não enviará outro pacote, a menos que o pacote anterior seja reconhecido.
  • O temporizador de atraso no servidor expira e o ACK é enviado de volta.

Como melhorar

Há dois problemas com esse design. Primeiro, há o problema do temporizador de atraso. O cliente precisa ser capaz de enviar dois pacotes para o servidor dentro de 200 ms. Como o cliente usa o algoritmo Nagle por padrão, ele deve usar apenas o buffer padrão do Winsock e não definido SO_SNDBUF como 0. Depois que a pilha TCP unir um buffer maior que a MTU (Unidade de Transmissão Máxima), um pacote de tamanho completo será enviado imediatamente sem aguardar o ACK do host remoto.

Em segundo lugar, esse design chama um envio para cada registro de tamanho tão pequeno. O envio desse pequeno de um tamanho não é eficiente. Nesse caso, o desenvolvedor pode querer adicionar cada registro a 100 bytes e enviar 80 registros por vez de uma chamada de envio de cliente. Para que o servidor saiba quantos registros serão enviados no total, o cliente pode querer iniciar a comunicação com um cabeçalho de tamanho fixo que contém o número de registros a seguir.

Estudo de caso 2

Um aplicativo cliente do Winsock TCP abre duas conexões com um aplicativo de servidor Do Winsock TCP que fornece serviço de cotações de ações. A primeira conexão é usada como um canal de comando para enviar o símbolo de estoque para o servidor. A segunda conexão é usada como um canal de dados para receber a cotação de ações. Depois que as duas conexões forem estabelecidas, o cliente envia um símbolo de estoque para o servidor por meio do canal de comando e aguarda a volta da cotação de ações pelo canal de dados. Ele envia a próxima solicitação de símbolo de estoque para o servidor somente depois que a primeira cotação de ações tiver sido recebida. O cliente e o servidor não definem a opção SO_SNDBUF e TCP_NODELAY .

  • Desempenho

    Durante o teste, o desenvolvedor descobre que o cliente só poderia obter cinco cotações por segundo.

  • Análise

    Esse design permite apenas uma solicitação de cotação de ações pendente por vez. O primeiro símbolo de estoque é enviado para o servidor por meio do canal de comando (conexão) e uma resposta é imediatamente enviada do servidor para o cliente por meio do canal de dados (conexão). Em seguida, o cliente envia imediatamente a segunda solicitação de símbolo de estoque e o envio retorna imediatamente à medida que o buffer de solicitação na chamada de envio é copiado para o buffer do kernel winsock. No entanto, a pilha TCP do cliente não pode enviar a solicitação do buffer do kernel imediatamente porque o primeiro envio pelo canal de comando ainda não foi reconhecido. Depois que o temporizador de atraso de 200 ms no canal de comando do servidor expirar, o ACK para a primeira solicitação de símbolo volta para o cliente. Em seguida, a segunda solicitação de cotação é enviada com êxito ao servidor depois de ser adiada para 200 ms. A cotação do segundo símbolo de ações retorna imediatamente por meio do canal de dados porque, neste momento, o temporizador de atraso no canal de dados do cliente expirou. Um ACK para a resposta de cotação anterior é recebido pelo servidor. (Lembre-se de que o cliente não pôde enviar uma segunda solicitação de cotação de ações para 200 ms, dando tempo para o temporizador de atraso no cliente expirar e enviar um ACK para o servidor.) Como resultado, o cliente obtém a segunda resposta de cotação e pode emitir outra solicitação de cotação, que está sujeita ao mesmo ciclo.

  • Como melhorar

    O design de duas conexões (canal) é desnecessário aqui. Se você usar apenas uma conexão para a solicitação e a resposta da cotação de ações, o ACK para a solicitação de cotação poderá ser usado na resposta de cotação e voltar imediatamente. Para melhorar ainda mais o desempenho, o cliente poderia multiplex várias solicitações de cotação de ações em uma chamada de envio para o servidor e o servidor também poderia multiplex várias respostas de cotação em uma chamada de envio para o cliente. Se o design de dois canais unidirecionais for necessário por algum motivo, ambos os lados devem definir a opção TCP_NODELAY para que os pacotes pequenos possam ser enviados imediatamente sem precisar esperar por um ACK para o pacote anterior.

Recomendações

Embora esses dois estudos de caso sejam fabricados, eles ajudam a ilustrar alguns cenários piores. Quando você cria um aplicativo que envolve extensas mensagens de segmento de dados pequenos e recvs, você deve considerar as seguintes diretrizes:

  • Se os segmentos de dados não forem críticos por tempo, o aplicativo deverá uni-los em um bloco de dados maior para passar para uma chamada de envio. Como é provável que o buffer de envio seja copiado para o buffer do kernel do Winsock, o buffer não deve ser muito grande. Um pouco menos de 8K é eficaz. Desde que o kernel winsock obtenha um bloco maior que o MTU, ele enviará vários pacotes de tamanho completo e um último pacote com o que resta. O lado de envio, exceto o último pacote, não será atingido pelo temporizador de atraso de 200 ms. O último pacote, se acontecer de ser um pacote numerado ímpar, ainda está sujeito ao algoritmo de reconhecimento atrasado. Se a pilha de extremidade de envio receber outro bloco maior que o MTU, ele ainda poderá ignorar o algoritmo Nagle.

  • Se possível, evite conexões de soquete com fluxo de dados unidirecional. As comunicações sobre soquetes unidirecionais são mais facilmente afetadas pelo Nagle e pelos algoritmos de reconhecimento atrasado. Se a comunicação seguir uma solicitação e um fluxo de resposta, você deverá usar um único soquete para fazer ambos os envios e recvs para que a ACK possa ser ressarida na resposta.

  • Se todos os segmentos de dados pequenos precisarem ser enviados imediatamente, defina TCP_NODELAY a opção na extremidade de envio.

  • A menos que você queira garantir que um pacote seja enviado no fio quando uma conclusão de envio for indicada pelo Winsock, você não deve definir o SO_SNDBUF como zero. Na verdade, o buffer 8K padrão foi determinado heuristicamente a funcionar bem para a maioria das situações e você não deve alterá-lo a menos que você tenha testado que sua nova configuração de buffer winsock lhe dá um desempenho melhor do que o padrão. Além disso, definir SO_SNDBUF como zero é principalmente benéfico para aplicativos que fazem transferência de dados em massa. Mesmo assim, para obter a máxima eficiência, você deve usá-la em conjunto com buffer duplo (mais de um envio pendente a qualquer momento) e E/S sobreposta.

  • Se a entrega de dados não precisar ser garantida, use UDP.

Referências

Para obter mais informações sobre o Reconhecimento Atrasado e o algoritmo Nagle, confira o seguinte:

Braden, R.[1989], RFC 1122, Requisitos para Hosts da Internet- Camadas de Comunicação, Força-Tarefa de Engenharia da Internet.