- Ao depurar problemas de latência em sistemas distribuídos, a primeira coisa a verificar é a configuração de TCP_NODELAY
- O algoritmo de Nagle é uma abordagem proposta em 1984 no RFC896, projetada para reduzir a sobrecarga de cabeçalhos TCP ao transmitir pacotes pequenos
- Porém, quando combinado com o mecanismo de ACK atrasado (delayed ACK), a transmissão de dados fica adiada até o recebimento do ACK, o que piora o desempenho de aplicações sensíveis à latência
- Em ambientes modernos de datacenter, o RTT é muito curto, e a maioria dos sistemas já transmite mensagens grandes, então os benefícios do algoritmo de Nagle praticamente desapareceram
- Portanto, em sistemas distribuídos modernos, o TCP_NODELAY deve ser ativado por padrão, e o algoritmo de Nagle não é mais necessário
Contexto do algoritmo de Nagle
- O RFC896 de John Nagle, de 1984, foi proposto para resolver o problema da sobrecarga de 4000% de 1 byte de dados contra 40 bytes de cabeçalho em transmissões pequenas, como entrada de teclado
- Na época, o problema era que pequenos pacotes eram enviados cada vez que o usuário digitava um caractere, reduzindo a eficiência da rede
- A solução era restringir o envio de novos segmentos enquanto os dados anteriores ainda não tivessem sido confirmados com ACK
- Essa abordagem era eficaz no ambiente de rede da época, mas é inadequada para sistemas modernos em que a latência é importante
Interação entre o algoritmo de Nagle e Delayed ACK
- Delayed ACK (RFC813, RFC1122) é um método em que o lado receptor não envia o ACK imediatamente, mas o atrasa até que haja dados de resposta ou que um temporizador expire
- O algoritmo de Nagle interrompe a transmissão enquanto espera pelo ACK, e o delayed ACK atrasa esse ACK, então surge um impasse em que os dois lados ficam esperando um pelo outro
- O próprio John Nagle descreveu essa combinação como uma “combinação horrível”, observando que os dois recursos foram introduzidos de forma independente, mas causam latência quando usados juntos
Problemas no ambiente moderno
- Dentro de datacenters, o RTT é de cerca de 500μs, e mesmo dentro da mesma região fica em um nível de poucos milissegundos, ou seja, muito curto
- Nesse ambiente, atrasar a transmissão por um RTT inteiro leva a perda de desempenho
- Além disso, sistemas distribuídos modernos já transmitem mensagens suficientemente grandes devido a TLS, serialização e overhead de protocolo, então o problema de pacotes de um único byte quase não existe mais
- A otimização para mensagens pequenas agora é tratada na camada de aplicação
Por que o TCP_NODELAY é necessário
- Em sistemas distribuídos sensíveis à latência, recomenda-se ativar o TCP_NODELAY para desativar o algoritmo de Nagle
- Isso não é algo “ineficiente” nem uma “configuração incorreta”, e sim uma escolha alinhada ao hardware moderno e às características atuais do tráfego
- O autor defende que TCP_NODELAY deveria ser o padrão
- Parte do código que “envia a cada chamada de write()” pode ficar mais lento, mas esse tipo de código deveria ser corrigido na origem
Outras opções relacionadas
- A opção TCP_QUICKACK reduz o atraso de ACK, mas não é uma solução fundamental devido a problemas de portabilidade e comportamento inconsistente
- O problema central é que o kernel mantém os dados por mais tempo do que o momento pretendido pela aplicação, e eles deveriam ser enviados imediatamente na chamada de write()
Conclusão
- O algoritmo de Nagle foi uma excelente invenção para melhorar a eficiência de rede no passado, mas
em redes modernas de alta velocidade e em ambientes de sistemas distribuídos, ele se tornou uma funcionalidade ultrapassada que, na prática, introduz latência
- Portanto, manter o TCP_NODELAY sempre ativado é apresentado como um princípio básico do projeto de sistemas modernos
1 comentários
Comentários do Hacker News
Naquele tempo, vários hosts compartilhavam um único canal Ethernet, então usava-se CSMA/CD para evitar colisões
Mas hoje a maior parte da Ethernet é ponto a ponto, em um ambiente full duplex onde envio e recebimento podem ocorrer ao mesmo tempo
Portanto, o CSMA não é mais necessário, e parece razoável na maioria dos casos definir
TCP_NODELAYpara desativar o algoritmo de NagleAcho que tê-lo como padrão foi um dos grandes erros da história das redes
Por volta de 2014, ao trocar switches de datacenter, tive que manter alguns equipamentos antigos porque eles não suportavam 10Mbit half duplex
Ele evita a geração de pacotes pequenos demais
Nagle é uma otimização da camada TCP, servindo para agrupar pacotes pequenos e aumentar a eficiência
CSMA é um problema da camada física/de enlace, separado de Nagle
O backend escrito em Go já tinha TCP_NODELAY ativado por padrão, então não era a causa, mas achei interessante a parte sobre como os problemas do Nagle são percebidos
Também houve uma discussão anterior; dá para ver nesta thread
Em comunicações conversacionais como no protocolo DICOM, configurar
TCP_NODELAY=1melhora bastante o throughputVeja este link relacionado
Acho que, nas cargas de trabalho atuais, delayed ACK não traz grandes vantagens
No ambiente moderno centrado em HTTP, me parece melhor desligar tanto Nagle quanto delayed ACK
Como o RTT entre datacenters fica na casa de centenas de microssegundos, atrasar até mesmo um RTT pode acabar sendo pior
Link da wiki
NODELAYestá ligado, envia “de repente”, e quando está desligado, envia tudo de uma vez depois, então o significado da palavra parece servir para os dois modosA aplicação é que deveria decidir quando enviar e quando bufferizar
No Linux, isso serve como uma dica ao kernel de que mais dados serão enviados em seguida, sendo útil quando se envia cabeçalho e dados separadamente
Com io_uring, fica ainda mais eficiente
flush)Seria bom ter uma forma de esvaziar o buffer e enviar logo após uma mensagem que exige resposta imediata
Hoje em dia, os canais TCP misturam mensagens síncronas e assíncronas, o que torna tudo mais complexo
Gostaria que protocolos como SCTP fossem mais usados
flushparece uma omissão clara de projetoEm encapsulamentos como TLS, também é trabalhoso encontrar os limites das mensagens
flushcolocando MSG_MORE em todosende removendo só no últimoIdealmente, deveria ser possível marcar um bit de “bufferização permitida” para dividir uma transmissão grande e, no fim, indicar “envio imediato”
TCP_CORK é a alternativa mais parecida, mas é meio grosseira
I/O de arquivo sofre de problema parecido
É um conteúdo bem interessante
A aplicação deveria poder ajustar diretamente o equilíbrio entre latência e throughput
Mas, para implementá-lo no nível da aplicação, seria preciso conhecer os dados ainda não confirmados (
unacked data), o que seria ineficienteMesmo um simples timer de flush de 20ms já teria sido muito melhor
TCP_NODELAYé configurado por socket, então dá para ver isso mais como uma decisão de user space feita diretamente pela aplicação