El uso continuo de la opción `TCP_NODELAY`
(brooker.co.za)- Los problemas de latencia en sistemas distribuidos a menudo se resuelven simplemente activando
TCP_NODELAY, lo que sugiere que el comportamiento predeterminado de TCP puede no encajar con las cargas de trabajo modernas - El algoritmo de Nagle fue creado en 1984 en el RFC896 para reducir el costo de cabeceras de paquetes TCP pequeños, y funciona impidiendo enviar un nuevo segmento antes de recibir un ACK
- Cuando se usa junto con delayed ACK, un lado espera el ACK y el otro espera datos de respuesta o un temporizador, lo que perjudica a las aplicaciones en pipeline sensibles a la latencia
- Aunque el RTT dentro de un datacenter sea de alrededor de 500μs, los servidores modernos pueden hacer mucho trabajo en ese tiempo, así que no está claro el beneficio de retrasar un envío por un RTT completo
- En los sistemas distribuidos modernos, por TLS, la codificación, la serialización y el tamaño de los mensajes de la aplicación, el problema de paquetes de un solo byte ha disminuido, y en entornos sensibles a la latencia es más natural desactivar el algoritmo de Nagle
La configuración que se revisa primero al depurar latencia
- Cuando aparece un problema de latencia en un sistema distribuido, lo primero que suele revisarse es si
TCP_NODELAYestá habilitado - Muchos desarrolladores de sistemas distribuidos han perdido tiempo con problemas que se resuelven con esta simple opción de socket
- Esta repetición sugiere que el comportamiento predeterminado de TCP no se adapta bien a los sistemas distribuidos actuales, o que el propio algoritmo de Nagle podría estar desactualizado
El problema que el algoritmo de Nagle intentaba resolver
- RFC896 es un documento de 1984 que trató el problema de los paquetes pequeños
- En ese momento, al enviar por TCP datos que llegaban un carácter a la vez, como la entrada de teclado, se producía la ineficiencia de agregar una cabecera de 40 bytes por cada 1 byte de datos
- Por cada byte útil de datos se añadían 40 bytes de cabecera, generando un overhead del 4000%
- Podía tolerarse con cargas ligeras, pero perjudicaba el throughput de la red
- El objetivo del algoritmo de Nagle era mejorar el throughput amortizando mejor el costo de las cabeceras TCP
- Los paquetes pequeños solían provenir de aplicaciones interactivas humanas como un shell, o de implementaciones que entregaban datos al kernel poco a poco mediante varias llamadas a
write
- Los paquetes pequeños solían provenir de aplicaciones interactivas humanas como un shell, o de implementaciones que entregaban datos al kernel poco a poco mediante varias llamadas a
- El comportamiento central consiste en no enviar inmediatamente nuevos datos como un segmento TCP separado si los datos enviados anteriormente todavía no han sido confirmados con ACK
- A menudo se explica el algoritmo de Nagle junto con temporizadores, pero el propio RFC896 no usa un temporizador aparte del round-trip time (RTT) de la red
La latencia que aparece al combinarlo con delayed ACK
- delayed ACK consiste en no enviar inmediatamente la confirmación de recepción de un paquete, sino esperar hasta que haya datos para devolver o hasta que expire un temporizador
- RFC813 es un documento temprano de 1982 que propuso retrasar ACK, y trata cómo en ciertas situaciones el receptor puede posponer el envío del ACK y programar un temporizador para enviarlo después
- RFC1122 formalizó más el delayed ACK
- Ambas funciones son razonables por separado, pero juntas pueden introducir latencia
- El algoritmo de Nagle espera recibir un ACK antes de enviar más datos
- delayed ACK retrasa el envío del ACK hasta que haya datos de respuesta listos o expire un temporizador
- Esto ayuda a llenar mejor los paquetes, pero no es bueno para aplicaciones en pipeline sensibles a la latencia
- Un comentario de John Nagle en Hacker News también ve el problema no como prevención de tinygrams, sino como la combinación de ACK retrasado y temporizadores fijos
- Es un ejemplo de cómo dos funciones razonables de un protocolo pueden combinarse para producir un comportamiento no deseado, y de por qué estas interacciones dificultan el diseño de protocolos
Dónde no encaja con los sistemas distribuidos modernos
- Incluso sin delayed ACK, el comportamiento del algoritmo de Nagle puede no coincidir con lo que quieren los sistemas distribuidos modernos
- En el entorno actual, el propio RTT ya es un costo nada despreciable
- Un RTT único dentro de un datacenter suele ser de alrededor de 500μs
- El RTT entre datacenters de la misma región es de varios ms
- En rutas globales puede llegar a varios cientos de ms
- Los servidores modernos pueden procesar mucho trabajo incluso en unos pocos cientos de μs, así que no es evidente que retrasar el envío de datos por un RTT completo aporte un beneficio claro
- La justificación original del algoritmo de Nagle era reducir el overhead de cabecera 40 veces mayor que aparecía en paquetes de un solo byte
- Las bases de datos distribuidas y los sistemas distribuidos modernos por lo general no envían paquetes de un solo byte
- Los datos que envía la aplicación ya son más grandes
- Se añade overhead de protocolos como TLS
- También se suma overhead de codificación y serialización
- El problema de evitar mensajes pequeños sigue siendo importante, pero la responsabilidad se ha trasladado de forma efectiva a la capa de aplicación
- Enviar datos envueltos en JSON un byte a la vez no es eficiente, independientemente del algoritmo de Nagle
Por qué ver TCP_NODELAY como la opción predeterminada
- Si se construye un sistema distribuido sensible a la latencia sobre hardware moderno de nivel datacenter, se puede habilitar
TCP_NODELAYpara desactivar el algoritmo de Nagle - Considerando el tráfico, la composición de las aplicaciones y el rendimiento del hardware moderno, puede que el algoritmo de Nagle ya no sea necesario
- Incluso es posible defender que
TCP_NODELAYdebería ser el valor predeterminado - El código que llama a
writepor cada byte puede volverse más lento conTCP_NODELAYcomo valor predeterminado - Si la eficiencia importa, ese código debería corregir su implementación en la aplicación en lugar de depender del algoritmo de Nagle
TCP_QUICKACK está más cerca de ser una opción complementaria
TCP_QUICKACKpuede mencionarse como alternativa, pero por su falta de portabilidad y su significado peculiar no es fácil tomarlo como primera opción- Conviene verificar directamente su significado en la página man de tcp en Linux
- Un problema más importante es que
TCP_QUICKACKno resuelve el problema de fondo: que el kernel retenga datos más tiempo del que el programa pretende - Si un programa llamó a
write(), espera quewrite()realmente se lleve a cabo
1 comentarios
Opiniones de Hacker News
A lo largo de mi carrera corregí varias veces problemas de latencia causados por el algoritmo de Nagle, y ahora es lo primero que sospecho.
La lógica en sí es válida, pero no encaja con algunas cargas de trabajo; creo que el ingeniero debería elegirlo explícitamente al crear el socket, no dejarlo como valor predeterminado del sistema operativo.
El problema no es si es una opción buena o mala, sino que existe una configuración que cambia de forma bastante agresiva cómo se transmiten los datos y mucha gente ni sabe que existe.
Hasta ahora encontré un bug todas las veces.
Ej.: https://cloud-haskell.atlassian.net/browse/DP-108 o https://github.com/agentm/curryer/issues/3
Dicho eso, no estoy de acuerdo con lo de que “no es una opción buena/mala”.
Es una heurística del lado del kernel para “arreglar mágicamente” aplicaciones mal escritas y, como dice el artículo, una aplicación normal no hace llamadas de sistema
write()de red de 1 byte.Ese software debería corregirse.
El único caso en que esta función tiene sentido, en mi opinión, es el de un administrador de sistemas del kernel que, por razones como la política interna del equipo, no puede arreglar el software que corre en la máquina.
Fuera de eso, vuelve más complejo al software normal.
Significa que hay que desactivar explícitamente una magia rara introducida para aumentar un poco el throughput de software mal escrito, y que en software bien escrito genera latencias grandes y sorprendentes.
John Nagle dice en el hilo enlazado aquí que los ACK retrasados son peores, y estoy de acuerdo con eso.
Pero el patrón Send/Send/Receive que empeora el algoritmo de Nagle es un caso de uso completamente válido y común: aplica a cualquier cosa que haga RPC con pipeline sobre TCP.
Creo que tanto los ACK retrasados como el algoritmo de Nagle deberían estar desactivados por defecto.
El nombre también debería ser algo como TCP_DELAY, y solo debería activarse cuando uno no quiera implementar un buffering básico en espacio de usuario.
La gente no debería tener que saber estas cosas, y el comportamiento predeterminado no debería ser sorprendente.
write, una opción para activar TCP_DELAY se vuelve bastante extraña.Implica que necesitas un ingeniero de software lo bastante listo como para conocer esa opción, pero no lo bastante listo como para agrupar bien las llamadas a
writeo crear por su cuenta un buffering mejor, estilo Nagle, adecuado para su aplicación.Si no tienes algo como
io_uringque compense el costo de las llamadas de sistema, el lado de espacio de usuario funciona mejor.Según recuerdo, esa era la motivación principal.
La conclusión es un poco rara. El algoritmo de Nagle era claramente un intento de agrupar escrituras, y en algunos casos agrupar escrituras es mejor, independientemente del hardware, la red, la aplicación o el caso de uso.
Incluso hoy, mucha computación usa escrituras agrupadas, y las aplicaciones de red también se benefician.
Protocolos de más alto nivel y más nuevos, como QUIC, agrupan escrituras y, en la práctica, trasladan al espacio de usuario el manejo independiente de conexiones y errores de TCP, haciendo que el protocolo empuje los datos a la aplicación lo antes posible y que la conexión y el manejo de errores de cada stream individual queden a cargo de la aplicación, no del stack TCP/IP del host ni de los routers.
Si las redes vuelven a saturarse como antes, el algoritmo de Nagle regresará en forma de modificaciones a QUIC, probablemente más profundo dentro del código de la aplicación, esperando para enviar paquetes QUIC hasta alcanzar ciertos criterios.
Todo en tecnología se reinventa cuando el hardware o el software llega a un cuello de botella. Como el rendimiento de ambos no crece al mismo ritmo, al final siempre ocurre.
Además del ancho de banda, cuando la saturación se da por la cantidad de paquetes por segundo debido a paquetes pequeños, el algoritmo de Nagle resulta útil.
Eso permitió conectarse a servicios con un teletipo físico, pero hizo que TCP no conociera los límites de los mensajes; aunque hoy se pueda inyectar parte de ese conocimiento, el software inicial no podía.
En cambio, muchos protocolos que no son TCP, como QUIC, SCTP y TP4, proporcionan explícitamente límites de mensajes.
La interfaz con el sistema no es un puerto serial emulado, sino, como mucho, una basada en mensajes que se reensamblan.
El protocolo no tiene suficiente contexto para agrupar correctamente.
A la inversa, ¿qué tal desactivar los ACK retrasados?
El problema es el comportamiento patológico que aparece cuando interactúan la prevención de paquetes pequeños y los ACK retrasados.
La opción expuesta para desactivar la prevención de paquetes pequeños es
TCP_NODELAY, pero ¿cómo se pueden desactivar los ACK retrasados?Es decir, cuando uno quiere hacer benchmarks de las cuatro combinaciones y ver cuál encaja mejor.
Buscando un poco, en Linux existe la opción de socket
TCP_QUICKACK, pero hay que configurarla cada vez que se recibe algo.También están
/proc/sys/net/ipv4/tcp_delack_miny/proc/sys/net/ipv4/tcp_ato_min.En FreeBSD están
net.inet.tcp.delayed_ackynet.inet.tcp.delacktime.TCP_QUICKACKcorrige la peor forma, pero no resuelve el problema completo.El algoritmo de Nagle todavía puede esperar hasta un tiempo de ida y vuelta completo antes de enviar datos y, si se sigue el RFC, casi no aporta beneficios y solo agrega latencia.
TCP_QUICKACKcada vez que se recibe algo... ¿en qué estaban pensando?¿Por qué alguien querría tenerlo desactivado solo parte del tiempo?
quickack 1al final de una ruta para desactivar los ACK retrasados en ese camino.En un mundo donde el ancho de banda era limitado y el tamaño mínimo de paquete era de 64 bytes, además de requerir el intervalo entre tramas, enviar un paquete TCP por cada byte era un enorme desperdicio de ancho de banda.
En la mayoría de las redes Ethernet ese mínimo sigue siendo así, y lo mismo aplica a enviar ACK vacíos.
Pero mi postura básica es esta: no es
TCP_NODELAY, es simplemente TCP.No me convence mucho el argumento de que Nagle ya no es necesario.
Telnet no es importante hoy, pero parece que todavía hay muchas aplicaciones que hacen cosas como:
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: "), etc.Aunque esto no implique una sobrecarga de 40 veces, sí podría ser de unas 5 veces.
Uno no escribe a un archivo de esta forma y espera rendimiento mágico, aunque el sistema operativo también tenga su propio búfer.
No hay razón para esperar algo distinto solo al escribir en un socket, y Nagle, para empezar, tampoco te salva de la sobrecarga de las llamadas al sistema.
Lo confirmé tanto leyendo el código como observando el comportamiento con
strace.write(2)pequeño es la única forma que tiene sentido, así que no creo que este patrón sea tan común ahora.En servidores normalmente se necesita E/S asíncrona para escalar bien, y en clientes bloquearse por llamadas de red también da una mala experiencia.
Más todavía en el entorno actual, con cambios frecuentes de red y muchas situaciones fuera de cobertura.
Incluso dejando de lado el aspecto de red, las llamadas al sistema son bastante caras, así que es malo para el rendimiento.
Me pregunto si alguien conoce una buena forma de activar TCP_NODELAY en un socket cuando no se tiene acceso al código fuente de la aplicación.
No encontré ninguna configuración del kernel para aplicarlo de forma permanente ni ningún comando para cambiarlo a posteriori.
Pude desactivar los ACK retrasados poniendo
quickack 1en la tabla de ruteo, pero activar TCP_NODELAY desde fuera de la aplicación parece especialmente difícil.Últimamente estoy sufriendo exactamente el problema descrito aquí entre una aplicación que poseo y una aplicación de código cerrado con la que interactúa.
socket(2)?Sería llamar a la función real, luego hacer algo como
setsockopty devolver el socket modificado.Si originalmente era
your_app —> server, hacer que seayour_app -> localhost_socat -> server.socat tiene una opción de línea de comandos para configurar
tcp_nodelay.Eso sí, habría que convencer a la app de código cerrado de conectarse a localhost.
Si hace una consulta DNS, es posible que una entrada en
/etc/hostspermita hacer que se conecte a localhost.Como la app se comunica con socat mediante un socket local, el
tcp_nodelaydel lado de la app no afecta.setsockoptmedianteptrace?/proc//fd/y configurar la opción del socket podría funcionar. No lo probé.LD_PRELOADHace unos 15 años jugué un MMO con requisitos de tiempo real muy fuertes, y toda la comunicación era TCP.
Si hacía clic en un botón, mi acción ni siquiera aparecía en pantalla hasta que volvía el paquete de respuesta.
Al final, los chicos que jugábamos ese juego, incluido yo, descubrimos que al activar TCP_NODELAY el juego se volvía mucho más fluido.
El efecto era especialmente grande para los jugadores de California, que estaban cerca del servidor del juego.
Un efecto secundario interesante era que, antes del cambio, si el flujo TCP se detenía, el juego se congelaba un momento y luego reproducía muy rápido los eventos recibidos que se habían perdido.
Normalmente, ese evento era mi muerte.
Después del cambio, en lugar de eso simplemente se desconectaba.
Episodio relacionado del podcast Oxide and Friends: https://www.youtube.com/watch?v=mqvVmYhclAg
Si usas un lenguaje moderno que activa TCP_NODELAY por defecto, como Go, esto no aplica :-)
https://github.com/golang/go/issues/57530
No lo sabía
¿No bastaría con usar una librería de networking “moderna”?
No siempre es eso. A veces es DNS
En un router cerca de una obra, el polvo se había asentado en el espacio entre el láser y la fibra óptica, atenuando lo suficiente la señal, y se veía una pérdida de paquetes de 40 a 50%
Tras ubicar el punto de pérdida, el NOC le envió un correo al operador de transporte correspondiente, y al día siguiente el técnico enviado respondió contando esa historia
Aun así, normalmente se puede aplicar un parche de desvío, así que no es gran cosa
TCP_NODELAYo buffering de streamsLa Web, que es un sistema realmente complejo, también falla por la caché