O uso contínuo da opção TCP_NODELAY
(brooker.co.za)- Problemas de latência em sistemas distribuídos continuam sendo resolvidos apenas com a ativação de
TCP_NODELAY, o que indica que o comportamento padrão do TCP pode não combinar com cargas de trabalho modernas - O algoritmo de Nagle foi criado na RFC896, em 1984, para reduzir o custo de cabeçalhos de pequenos pacotes TCP, impedindo o envio de um novo segmento antes de receber um ACK
- Quando usado junto com delayed ACK, um lado espera pelo ACK e o outro espera por dados de resposta ou por um temporizador, o que prejudica aplicações em pipeline sensíveis à latência
- Mesmo que o RTT dentro de um datacenter seja de cerca de 500 μs, servidores modernos conseguem fazer muito nesse intervalo, então o benefício de atrasar o envio em um RTT não é claro
- Em sistemas distribuídos modernos, TLS, encoding, serialização e o tamanho das mensagens de aplicação reduziram o problema de pacotes de um único byte; em ambientes sensíveis à latência, desativar o algoritmo de Nagle é mais natural
A configuração a verificar primeiro ao depurar latência
- Quando surge um problema de latência em sistemas distribuídos, a primeira coisa a verificar costuma ser se
TCP_NODELAYestá ativado - Muitos desenvolvedores de sistemas distribuídos já perderam tempo com problemas que se resolviam com essa simples opção de socket
- Essa repetição mostra que o comportamento padrão do TCP talvez não seja adequado aos sistemas distribuídos atuais, ou que o próprio algoritmo de Nagle pode estar ultrapassado
O problema que o algoritmo de Nagle tentava resolver
- RFC896 é um documento de 1984 que tratava do problema dos pacotes pequenos
- Na época, ao enviar via TCP dados que chegavam caractere por caractere, como entrada de teclado, havia a ineficiência de anexar um cabeçalho de 40 bytes a cada 1 byte de dados
- Isso gerava 4000% de overhead, com 40 bytes de cabeçalho para cada 1 byte de dados úteis
- Mesmo que fosse tolerável sob carga leve, era ruim para a vazão da rede
- O objetivo do algoritmo de Nagle era amortizar melhor o custo dos cabeçalhos TCP e aumentar a vazão
- Pacotes pequenos surgiam principalmente em aplicações de interação humana, como shells, e em implementações que entregavam dados ao kernel aos poucos por meio de várias chamadas
write
- Pacotes pequenos surgiam principalmente em aplicações de interação humana, como shells, e em implementações que entregavam dados ao kernel aos poucos por meio de várias chamadas
- O comportamento central é não enviar imediatamente novos dados de transmissão como um segmento TCP separado quando os dados enviados anteriormente ainda não receberam ACK
- O algoritmo de Nagle é frequentemente explicado junto com um temporizador, mas a própria RFC896 não usa um temporizador separado além do tempo de ida e volta (RTT) da rede
A latência que surge quando combinado com delayed ACK
- Delayed ACK é uma técnica que não envia imediatamente a confirmação de recebimento de um pacote, esperando até que haja dados a devolver ou que um temporizador expire
- RFC813 é um documento inicial de 1982 que propôs atrasar ACKs e trata da possibilidade de o receptor, em certas situações, adiar o envio do ACK e configurar um temporizador para enviá-lo depois
- RFC1122 formaliza melhor o delayed ACK
- As duas funcionalidades são razoáveis separadamente, mas podem criar latência quando usadas juntas
- O algoritmo de Nagle espera pelo recebimento do ACK antes de enviar mais dados
- Delayed ACK adia o envio do ACK até que dados de resposta estejam prontos ou o temporizador expire
- Isso ajuda a preencher pacotes, mas não é bom para aplicações em pipeline sensíveis à latência
- Um comentário de John Nagle no Hacker News também vê o problema não como a prevenção de tinygrams, mas como a combinação de atraso de ACKs com um temporizador fixo
- É um caso em que duas funcionalidades de protocolo razoáveis se combinam para produzir um comportamento indesejado, e esse tipo de interação dificulta o design de protocolos
Onde isso não combina com sistemas distribuídos modernos
- Mesmo sem delayed ACK, o comportamento do algoritmo de Nagle pode diferir do que sistemas distribuídos modernos desejam
- No ambiente atual, o próprio RTT é um custo difícil de ignorar
- Um único RTT dentro de um datacenter costuma ser de cerca de 500 μs
- O RTT entre datacenters na mesma região fica em alguns ms
- Rotas globais podem chegar a centenas de ms
- Servidores modernos conseguem processar muito trabalho mesmo em centenas de μs, então é difícil ver como claramente vantajosa a decisão de atrasar o envio dos dados por um RTT
- A justificativa original do algoritmo de Nagle era reduzir o overhead de cabeçalho de 40 vezes causado por pacotes de um único byte
- Bancos de dados distribuídos e sistemas distribuídos modernos, em geral, não enviam pacotes de um único byte
- Os próprios dados enviados pela aplicação são maiores
- Overheads de protocolos como TLS são adicionados
- Também há overhead de encoding e serialização
- O problema de evitar mensagens pequenas continua importante, mas essa responsabilidade foi efetivamente deslocada para a camada de aplicação
- Enviar dados encapsulados em JSON um byte por vez não é eficiente, independentemente do algoritmo de Nagle
Por que ver TCP_NODELAY como a escolha padrão
- Se você está construindo um sistema distribuído sensível à latência em hardware moderno de nível de datacenter, pode ativar
TCP_NODELAYpara desativar o algoritmo de Nagle - Considerando o tráfego, a composição das aplicações e o desempenho do hardware em sistemas modernos, o algoritmo de Nagle pode não ser mais necessário
- Também é possível defender que
TCP_NODELAYdeveria ser o padrão - Código que chama
writea cada byte pode ficar mais lento comTCP_NODELAYcomo padrão - Se eficiência é importante, esse código deveria corrigir a implementação da aplicação, em vez de depender do algoritmo de Nagle
TCP_QUICKACK é mais uma opção auxiliar
TCP_QUICKACKpode ser citado como alternativa, mas sua falta de portabilidade e sua semântica peculiar dificultam escolhê-lo primeiro- É preciso conferir diretamente o significado na página man do tcp no Linux
- O problema maior é que
TCP_QUICKACKnão resolve a questão fundamental de o kernel manter dados por mais tempo do que a intenção do programa - Se o programa chamou
write(), ele espera que owrite()de fato seja executado
1 comentários
Opiniões no Hacker News
Ao longo da minha carreira, corrigi várias vezes problemas de latência causados pelo algoritmo de Nagle e, hoje, é a primeira coisa de que desconfio.
A lógica em si faz sentido, mas não serve para algumas cargas de trabalho; acho que, ao criar um socket, o engenheiro deveria escolher explicitamente, em vez de deixar isso para o padrão do sistema operacional.
O problema não é se é uma opção boa ou ruim, mas que existe uma configuração que muda de forma bastante agressiva o modo como os dados são transmitidos, e muita gente nem sabe que ela existe.
Até agora, encontrei um bug todas as vezes.
Ex.: https://cloud-haskell.atlassian.net/browse/DP-108 ou https://github.com/agentm/curryer/issues/3
Dito isso, não concordo com “não é uma opção boa/ruim”.
Trata-se de uma heurística do lado do kernel para “corrigir magicamente” aplicações mal escritas e, como o artigo diz, aplicações normais não fazem chamadas de sistema
write()de rede de 1 byte.Esse tipo de software deve ser corrigido.
Acho que esse recurso faz sentido em casos raros em que você é administrador de sistemas do kernel e não consegue corrigir o software que roda na máquina por motivos como política interna da equipe.
Fora isso, ele torna software normal mais complexo.
Significa que você precisa desativar explicitamente uma mágica estranha criada para aumentar um pouco a vazão de software mal escrito, e que, em software bem escrito, cria latências grandes e surpreendentes.
John Nagle diz, em uma thread linkada daqui, que os ACKs atrasados são piores, e concordo com isso.
Mas o padrão Send/Send/Receive que o algoritmo de Nagle agrava é um caso de uso totalmente válido e comum, aplicável a qualquer coisa que faça RPC com pipeline sobre TCP.
Acho que tanto os ACKs atrasados quanto o algoritmo de Nagle deveriam vir desativados por padrão.
O nome também deveria ser algo como TCP_DELAY, e isso só deveria ser ativado quando você não quisesse implementar um buffering básico no espaço de usuário.
As pessoas não deveriam precisar saber dessas coisas, e o comportamento padrão deveria ser o menos surpreendente possível.
write, uma opção para ativar TCP_DELAY fica bem esquisita.Isso exigiria um engenheiro de software inteligente o bastante para conhecer essa opção, mas não inteligente o suficiente para dividir bem as chamadas de
writeou criar por conta própria um buffering melhor, ao estilo Nagle, adequado à sua aplicação.Se não houver algo como
io_uringpara compensar o custo das chamadas de sistema, o lado do espaço de usuário funciona melhor.Pelo que lembro, essa era a principal motivação.
A conclusão é meio estranha. O algoritmo de Nagle era claramente uma tentativa de agrupar escritas, e, independentemente de hardware, rede, aplicação ou caso de uso, em alguns casos agrupar escritas é melhor.
Ainda hoje, muita computação usa agrupamento de escritas, e aplicações de rede também se beneficiam disso.
Protocolos de nível mais alto e mais novos, como QUIC, agrupam escritas e, na prática, movem para o espaço de usuário o tratamento independente de conexões e erros do TCP, fazendo com que o protocolo empurre os dados para a aplicação o mais rápido possível, enquanto a conexão e o tratamento de erros de fluxos individuais ficam a cargo da aplicação, não da pilha TCP/IP do host nem dos roteadores.
Se as redes voltarem a ficar saturadas como antes, o algoritmo de Nagle retornará em uma forma modificada para QUIC, provavelmente mais fundo no código da aplicação, esperando para enviar pacotes QUIC até que algum critério específico seja atingido.
Tudo em tecnologia é reinventado quando hardware ou software atinge um gargalo. Como o desempenho dos dois não cresce na mesma velocidade, no fim isso sempre acontece.
Além da largura de banda, o algoritmo de Nagle é útil quando pequenos pacotes levam à saturação do número de pacotes por segundo.
Isso permitia acessar serviços com uma teletipo física, mas fez com que o TCP não conhecesse limites de mensagens; hoje é possível empurrar parte desse conhecimento para dentro, mas o software inicial não conseguia.
Em contraste, muitos protocolos que não são TCP, como QUIC, SCTP e TP4, oferecem explicitamente limites de mensagens.
A interface com o sistema não é uma porta serial emulada, mas sim baseada em mensagens, no máximo remontadas.
O protocolo não tem contexto suficiente para agrupar corretamente.
Por outro lado, e se desativássemos o ACK atrasado?
O problema é o comportamento patológico que surge quando a prevenção de pacotes pequenos interage com o ACK atrasado
A opção exposta para desativar a prevenção de pacotes pequenos é
TCP_NODELAY, mas como desativar o ACK atrasado?Digo, quando você quer fazer benchmark das quatro combinações e ver qual se encaixa melhor
Pesquisando um pouco, no Linux existe a opção de socket
TCP_QUICKACK, mas ela precisa ser definida a cada recebimentoTambém existem
/proc/sys/net/ipv4/tcp_delack_mine/proc/sys/net/ipv4/tcp_ato_minNo FreeBSD existem
net.inet.tcp.delayed_ackenet.inet.tcp.delacktimeTCP_QUICKACKcorrige a pior forma, mas não resolve o problema inteiroO algoritmo de Nagle ainda pode esperar até um tempo de ida e volta inteiro antes de enviar dados e, seguindo a RFC, isso praticamente só acrescenta atraso sem trazer benefício
TCP_QUICKACKa cada recebimento — o que estavam pensando?Por que alguém iria querer deixá-lo desligado só parte do tempo?
quickack 1ao fim da rota para desativar o ACK atrasado nesse caminhoEm um mundo em que a largura de banda era limitada e o tamanho mínimo de pacote era 64 bytes, além de exigir o intervalo entre quadros, enviar um pacote TCP para cada byte era um desperdício enorme de banda
Na maioria das redes Ethernet, o tamanho mínimo ainda é esse, e o mesmo vale para enviar ACK vazio
Mas minha posição padrão é esta: não é
TCP_NODELAY, é simplesmente TCPNão me convence muito o argumento de que Nagle não é mais necessário
Telnet não é importante hoje, mas acho que ainda há muitos aplicativos que fazem algo como
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: ")etc.Mesmo que isso não seja um overhead de 40 vezes, pode chegar a algo como 5 vezes
Ninguém escreve em um arquivo desse jeito esperando desempenho mágico. Mesmo o sistema operacional tendo seu próprio buffer
Não há motivo para esperar algo diferente só ao escrever em um socket, e Nagle nem sequer salva você do overhead de chamadas de sistema
Confirmei tanto lendo o código quanto observando o comportamento com
stracewrite(2), a única abordagem que faz sentido é colocar em um buffer, então acho que esse padrão já não é tão comumEm servidores, E/S assíncrona costuma ser necessária para escalar bem, e em clientes também é uma experiência ruim bloquear em chamadas de rede
Especialmente em ambientes como os de hoje, com mudanças frequentes de rede e muitos momentos fora de alcance
Mesmo deixando de lado o aspecto de rede, chamadas de sistema são bem caras, então isso prejudica o desempenho
Fico me perguntando se alguém conhece uma boa forma de ativar TCP_NODELAY em um socket quando não se tem acesso ao código-fonte da aplicação
Não encontrei uma configuração de kernel para aplicar isso permanentemente, nem um comando para alterar depois
Consegui desativar o ACK atrasado colocando
quickack 1na tabela de roteamento, mas ativar TCP_NODELAY de fora da aplicação parece especialmente difícilRecentemente estou enfrentando exatamente o problema descrito aqui entre uma aplicação que eu possuo e uma aplicação de código fechado com a qual ela interage
socket(2)não funcionaria?Seria chamar a função real, depois fazer algo como
setsockopte retornar o socket modificadoSe originalmente era
your_app —> server, ficariayour_app -> localhost_socat -> serverO socat tem uma opção de linha de comando para definir
tcp_nodelayMas você teria que convencer o aplicativo de código fechado a se conectar ao localhost
Se ele faz consulta DNS, talvez dê para fazê-lo se conectar ao localhost com uma entrada em
/etc/hostsComo o app se comunica com o socat por um socket local, o
tcp_nodelaydo lado do app não tem efeitosetsockoptviaptrace?/proc//fd/e definir as opções do socket talvez funcione. Não testeiLD_PRELOADUns 15 anos atrás, joguei um MMO extremamente em tempo real, e toda a comunicação era TCP
Quando você clicava em um botão, sua ação nem aparecia na tela até o pacote de resposta voltar
No fim, as crianças que jogavam esse jogo, eu incluído, todas descobriram que ativar TCP_NODELAY deixava o jogo muito mais fluido
O efeito era especialmente grande para jogadores da Califórnia, perto dos servidores do jogo
Um efeito colateral interessante era que, antes da mudança, quando o stream TCP travava, o jogo congelava por um instante e depois reproduzia muito rapidamente os eventos recebidos que tinham sido perdidos
Normalmente esse evento era a cena da minha morte
Depois da mudança, em vez disso, a conexão simplesmente caía
Episódio relacionado do podcast Oxide and Friends: https://www.youtube.com/watch?v=mqvVmYhclAg
Se você usa uma linguagem moderna que ativa TCP_NODELAY por padrão, como Go, isso não se aplica :-)
https://github.com/golang/go/issues/57530
Eu não sabia
Não bastaria usar uma biblioteca de rede “moderna”?
Nem sempre é isso. Às vezes é DNS
Em um roteador perto de uma obra, poeira se depositou na fresta entre o laser e a fibra óptica, atenuando o sinal o suficiente, e vimos 40–50% de perda de pacotes
Depois de encontrar o ponto da perda, o NOC enviou um e-mail para a operadora de transporte correspondente e, no dia seguinte, o técnico enviado respondeu contando essa história
Ainda assim, normalmente dá para contornar com um patch, então não é um grande problema
TCP_NODELAYou buffering de streamsA Web, que é um sistema realmente complexo, também falha por causa de cache