Comparativa QUIC-TCP: ¿qué protocolo es mejor?
Hemos hablado largo y tendido sobre cuánto nos apasiona el protocolo QUIC (y sobre las razones que nos llevaron a diseñar quicly, nuestra propia implementación de este protocolo). Promete reducir la latencia, mejorar el rendimiento, incorporar resiliencia frente a la movilidad de los clientes y reforzar la privacidad y la seguridad. El grupo de trabajo de QUIC que ha organizado el IETF está ultimando la primera versión de QUIC, que pronto estará lista para su despliegue en internet. Los equipos que han trabajado en su diseño y el mundo de los desarrolladores comparten el entusiasmo por que el despliegue de este nuevo protocolo sea todo un éxito, pero también comparten la inquietud de no saber cuál va a ser el coste computacional de las funciones y las medidas de seguridad adicionales que incorpora QUIC. Algunos expertos vaticinan que QUIC acabará sustituyendo a TCP, pero ¿es eso posible aunque necesite mucha más potencia de cálculo?
Realizamos varias pruebas para tratar de despejar algunas dudas y, en líneas generales, podemos decir que sí, que QUIC tiene tanta eficacia computacional como TCP.
Sin embargo, aún no podemos cantar victoria, primero debemos reconocer lo evidente. Por un lado, la preparación y la prueba de rendimiento que llevamos a cabo no fueron lo suficientemente complejas. Por otro lado, debemos realizar más pruebas con hipótesis de hardware y tráfico que sean más realistas y representativas. Es importante precisar que no habilitamos ninguna transferencia de hardware para TCP o QUIC. Teníamos dos objetivos: poner en práctica una hipótesis simple con tráfico sintético para suprimir algunos de los cuellos de botella computacionales más evidentes y obtener datos reveladores que nos indiquen cómo abaratar los costes de QUIC.
En cualquier caso, fue sorprendente comprobar que los resultados de QUIC eran parecidos a los de TCP incluso en nuestra hipótesis simple.
Se podría decir que en este artículo proponemos una competición entre nuestro coche y un Ferrari en un circuito de carreras, en decir, en un entorno artificial. Entonces, la experiencia de conducir nuestro coche en este espacio no es representativo de la conducción diaria (a menos, claro está, que fueras un piloto de carreras). Sin embargo, elaborar estrategias que nos permitan ser competitivos en ese circuito nos ayuda a descubrir cuellos de botella. Lo que de verdad importa, la información transferible, son las medidas que adoptaríamos para eliminar los cuellos de botella. De eso trata este artículo.
Contexto
El protocolo TCP ha sido la tradicional bestia de carga de internet. De hecho, a lo largo de los años se ha trabajado mucho en optimizar sus implementaciones para dotarlo de mayor eficacia computacional. Por su parte, QUIC es un protocolo que acaba de ver la luz, no ha tenido todavía mucho éxito en sus despliegues y aún debe perfeccionarse para lograr la eficacia computacional. La pregunta que debemos hacernos es si este joven protocolo es comparable con el veterano y respetable TCP o lo que es más importante, si a corto plazo QUIC podría ser tan eficaz como TCP.
Hemos previsto dos factores importantes que incrementarían los costes computacionales de QUIC:
Procesamiento de las confirmaciones: una gran proporción de los paquetes contenidos en una conexión TCP tipo solo transporta confirmaciones. TCP procesa las confirmaciones dentro del kernel, tanto en el lado del emisor como en el lado del receptor. Sin embargo, QUIC las procesa en el espacio de usuario. Esto implica que se generen más copias de datos en el límite usuario-kernel y más cambios de contexto. Además, las confirmaciones TCP se presentan en texto no cifrado, mientras que las confirmaciones QUIC están cifradas, con lo que se incrementa el coste de su envío y recepción.
Sobrecarga del emisor por paquete: el kernel tiene acceso a todos los datos de las conexiones TCP, es más, tiene la capacidad de recordar y reutilizar estados relacionados con cualquiera de los paquetes enviados en una conexión que se prevé que no van a sufrir cambios. Por ejemplo, lo habitual es que el kernel tenga que buscar la ruta correspondiente a la dirección de destino o aplicar reglas de firewall una sola vez al inicio de la conexión. Como el kernel no recibe datos del estado de las conexiones QUIC, estas operaciones relativas al kernel se aplican a todos y cada uno de los paquetes QUIC salientes.
Al ejecutarse QUIC en el espacio del usuario, los costes de este protocolo son superiores a los de TCP. Esto se debe a que todos los paquetes enviados o recibidos a través de QUIC cruzan el límite usuario-kernel, que se conoce como cambio de contexto.
Experimento y primeras impresiones
Para responder a la pregunta anterior, realizamos una prueba de rendimiento simple. Utilizamos quicly undefinedcomo servidor QUIC y como cliente QUIC. Los paquetes QUIC siempre se cifran con TLS 1.3. Con ese fin, quicly undefinedrecurre a picotls, biblioteca TLS para el servidor web H2O. Nuestra configuración TCP de referencia utilizaría picotls undefineda través de TCP en Linux nativo para minimizar las diferencias entre la configuración TCP de referencia y la configuración QUIC.
Hay dos métodos para medir la eficacia computacional: cuantificar el volumen de recursos informáticos necesarios para saturar una red o medir el rendimiento que se puede mantener con toda la potencia de cálculo que haya disponible. Saturar la red añade variabilidad debido a la pérdida de paquetes y a las acciones posteriores de recuperación ante pérdidas y de control de la congestión. Aunque conviene incluir ambos métodos en la medición del rendimiento, nos interesaba evitar dicha variabilidad, de modo que escogimos el segundo método. El criterio que adoptamos para medir la eficacia computacional fue el rendimiento que un emisor es capaz de mantener a plena potencia de cálculo en una red de banda ancha de alta capacidad que estuviera inactiva por cualquier motivo.
La eficiencia computacional del emisor es importante por dos razones. La primera: los emisores suelen ser el eslabón de los protocolos de transporte que está más expuesto a los costes computacionales, lo cual se debe a que el emisor es responsable de las funciones de transporte más costosas desde el punto de vista computacional. Por ejemplo, ejecutar temporizadores para detectar paquetes olvidados en la red y para retransmitirlos, supervisar el tiempo de ida y vuelta de la red o ejecutar estimadores de ancho de banda para que el emisor no congestione la red. La segunda: los servidores suelen ser emisores y mejorar la eficacia computacional del procesamiento del protocolo es más importante en el lado de los servidores. (Lo cual no quiere decir que la eficacia computacional en el lado de los clientes no sea importante, pero es que el procesamiento del protocolo no suele ser su principal cuello de botella).
Antes de sumergirnos en los resultados, me gustaría comentar algunos detalles de nuestra configuración experimental. El emisor por el que optamos era un equipo provisto de un chip Intel Core m3-6Y30 restringido a un solo núcleo y a un único subproceso sobre un sistema operativo Ubuntu 19.10 (con la versión 5.3.0 del kernel Linux). El emisor se conectó a la red local mediante un adaptador USB Gigabit-Ethernet que utilizaba el controlador ASIX AX88179. La transferencia de la suma de comprobación se habilitó para TCP y UDP. No se utilizaron otras optimizaciones de hardware concebidas para TCP, como TCP Segmentation Offload (TSO), las cuales utilizaremos en futuras comparativas junto con optimizaciones de hardware parecidas pensadas para UDP. El receptor era un MacBook Pro de cuatro núcleos de 2,5 GHz que estaba conectado al emisor a través de un conmutador Gigabit-Ethernet estándar. Detalle importante: para asegurarnos de que el emisor no saturara la red al utilizar toda su potencia de cálculo, restringimos la velocidad del reloj de la CPU de 2,2 GHz a 400 MHz. Gracias a esto pudimos confirmar que durante la realización de los experimentos no hubo pérdidas en la red.
Recordemos que en la prueba empleamos un hardware con prestaciones bastante modestas, pero no importó demasiado puesto que no interfería con el objetivo de este primer paso: identificar medidas que nos ayudasen a eliminar los cuellos de botella surgidos y valorar las posibilidades de transferirlas a otros entornos. En los siguientes pasos, analizaremos hardware apto para servidores.
La primera prueba de referencia consistió en medir el rendimiento máximo que se podía lograr a través de iperf con una conexión TCP sin cifrar y sin procesar. El resultado fue de 708 Mb/s.
La segunda prueba de referencia consistió en medir el rendimiento sostenido que se podía lograr mediante TLS 1.3 sobre TCP y con un cifrado picotls undefinedcon AES128-GCM. El resultado fue de 466 Mb/s, un 66 % de lo que obtuvimos con la conexión TCP sin cifrar. La disminución del rendimiento se explica por el coste de la criptografía, el uso de un socket que no es de bloqueo y el coste de interrumpir la ejecución del espacio del usuario para gestionar confirmaciones entrantes. Lo importante, sin embargo, es que las sobrecargas no interfieren en la cuestión que tratamos de responder, ya que estos costes se aplican tanto a TLS sobre TCP como a QUIC.
Por último, medimos el rendimiento sostenido con la versión estándar de quicly. El resultado fue de 196 Mb/s, es decir, un 40 % de los resultados obtenidos con TLS 1.3 sobre TCP. QUIC acarreaba los costes que habíamos previsto. De hecho, la cifra inicial fue un baño de realidad.
Nuestros objetivos eran cuantificar los costes de ejecución de QUIC y averiguar qué otras medidas podíamos adoptar para abaratarlos. Aparte, no queríamos limitarnos a nuestra propia implementación, sino que tendríamos en cuenta también cambios o modificaciones del protocolo. Todo esto lo conseguimos en tres pasos, que ahora explicaremos.
Reducción de la frecuencia de las confirmaciones
Al igual que TCP, la especificación de QUIC recomienda que el receptor envíe una confirmación por cada dos paquetes que reciba. Aunque se trata de un comportamiento predeterminado razonable, recibir y procesar confirmaciones genera costes computacionales a cualquier centro de datos. Una solución podría ser que el emisor redujera el número de confirmaciones que envía, pero ese cambio posiblemente mermara el rendimiento de la conexión, sobre todo al principio de esta.
En otras palabras, imagina que cada confirmación que recibe el emisor le permite incrementar la velocidad. Cuanto antes reciba una confirmación, antes podrá aumentar la velocidad de envío. Si esa velocidad es baja, el receptor recibirá menos paquetes por cada trayecto de ida y vuelta, por tanto, regresarán menos confirmaciones del receptor. Si rebajamos el número de confirmaciones para ese tipo de conexiones, es posible reducir de una forma ponderable la velocidad de envío y el rendimiento global del emisor.
En cuanto a las conexiones cuyo rendimiento sea alto, reducir el número de confirmaciones podría suponer que una proporción suficiente de estas logre regresar de modo que no afecte a la velocidad del emisor de una forma ponderable. (En realidad, estamos simplificando la idea; el resto de las consideraciones pertinentes se analizan en detalle aquí). Según la configuración de nuestra prueba, podemos reducir la frecuencia de las confirmaciones sin sacrificar el rendimiento del emisor.
Redujimos la frecuencia de las confirmaciones de una confirmación por cada dos paquetes a una por cada diez. Gracias a este ajuste, quicly undefinedpudo mantener un rendimiento de 240 Mb/s. O sea, que la red sale beneficiada gracias a la reducción del número de paquetes de confirmación que circulan por la red. Además, este experimento demuestra que el emisor también sale beneficiado, ya que se rebaja la sobrecarga computacional. Estos resultados fueron el empujón definitivo que necesitábamos para implementar la propuesta de extensión QUIC Delayed Acknowledgements, que veremos en detalle más adelante.
Combinación de paquetes con Generic Segmentation Offload (GSO)
A partir de las recomendaciones de Willem de Bruijn y Eric Dumazet («Optimizing UDP for content delivery: GSO, pacing and zerocopy»), el siguiente paso consistió en verificar si Generic Segmentation Offload de UDP podría ayudar a reducir la sobrecarga provocada por escrituras de un solo paquete y los cambios de contexto por paquete. Generic Segmentation Offload (GSO) es una función de Linux que permite a las aplicaciones del espacio del usuario facilitar al kernel una serie de paquetes combinados en una sola unidad. Dicha unidad atraviesa el kernel en forma de paquete virtual grande. Es decir, a partir de ese momento el kernel toma menos decisiones sobre paquetes virtuales grandes en vez de tomarlas una vez por cada paquete pequeño. A diferencia de TCP Segmentation Offload, el uso de GSO en Linux no conlleva necesariamente una transferencia de hardware. Si el hardware no es compatible con la transferencia de segmentación de paquetes UDP, el paquete virtual grande se divide en varios paquetes UDP pequeños cuando llega al controlador de la tarjeta de red. Justamente eso fue lo que sucedió en nuestro experimento.
Al combinar un máximo de 10 paquetes UDP en un solo objeto y enviarlos mediante GSO, el rendimiento de QUIC aumentó de 240 Mb/s a 348 Mb/s. Es decir, experimentó un repunte del 45 %. Entonces, quisimos comprobar si combinar más paquetes mejoraría aún más el rendimiento. Para ello, probamos a fusionar 20 paquetes UDP con GSO. El resultado fue un incremento adicional del rendimiento del 45 %; QUIC volaba a 431 Mb/s.
Habíamos dado un gran paso. El coste por paquete de QUIC era un cuello de botella evidente y considerable, y la solución con GSO que habíamos probado había funcionado de forma extraordinaria. El próximo paso era escoger un tamaño para GSO, un aspecto que analizaremos más adelante. A continuación, nos fijamos en otro parámetro, que suele pasar desapercibido, aunque no debería: el tamaño de los paquetes.
Incremento del tamaño de los paquetes
La especificación de QUIC recomienda no arriesgar mucho al establecer valores predeterminados para el tamaño de paquete QUIC mínimo: 1200 bytes; por su parte, quicly undefinedutiliza 1280 bytes. A las implementaciones se les permite incrementar el tamaño de los paquetes si hay motivos para pensar que la ruta podría admitir paquetes más pesados. Como la ruta tenía capacidad para paquetes QUIC de 1472 bytes y con dicha ruta TCP utilizaba paquetes de 1460 bytes, era lógico concluir que QUIC podría utilizar también paquetes más pesados. Aumentar el tamaño máximo de los paquetes reduce el coste computacional, ya que disminuye el número de paquetes necesarios para transferir una determinada cantidad de datos. Lo cual, a su vez, reduce la ineficacia computacional tanto en el lado del emisor como en el del receptor, porque hay un coste de procesamiento fijo por paquete en ambos lados.
Así que cambiamos el tamaño de los paquetes QUIC de 1280 bytes a 1460, lo cual nos permitía acercarnos al tamaño de carga útil de TCP. Tras implementar este cambio, quicly undefinedconsiguió mantener un rendimiento de 466 Mb/s, lo que significó un incremento del rendimiento del 8 %.
¡QUIC ya era más rápido que TLS sobre TCP!
Extrapolación de los resultados a la producción
Este experimento nos ha permitido descubrir qué medidas podemos adoptar para mejorar la eficacia de quicly: reducir la frecuencia de las confirmaciones, combinar paquetes con GSO y utilizar el tamaño de paquete más grande posible. Pues bien, ahora llega el momento de extrapolar los resultados y adoptar las optimizaciones descubiertas de forma que estas funcionen bien en diversos entornos, y hacerlo con el menor riesgo de que se produzcan posibles efectos secundarios.
Reducción de la frecuencia de las confirmaciones
Surgen dos problemas al reducir la frecuencia de las confirmaciones a una velocidad fija de una confirmación por cada 10 paquetes. El primero, como hemos indicado antes, que puede mermar de forma ponderable el rendimiento de la conexión si ya es bajo de por sí. El segundo, que el cliente en estos experimentos es quicly, mientras que en situaciones de producción el cliente suele ser un navegador, cuyo control no está en nuestras manos.
La extensión QUIC Delayed Acknowledgements es la respuesta a ambos problemas. Así, se permite que el emisor controle de manera dinámica la frecuencia de las confirmaciones del receptor en función de su velocidad de envío actual.
Desde que hicimos este experimento, hemos implementado la extensión Delayed Acknowledgements de tal forma que el emisor mantenga la frecuencia de las confirmaciones en una confirmación por cada octava parte de su ventana de congestión. Esta es, a su vez, de aproximadamente una octava parte de su tiempo de ida y vuelta. La configuración experimental anterior nos indica que se reducen las confirmaciones a una por cada 60 paquetes, lo cual es una reducción significativamente mayor que la lograda en nuestro experimento.
Combinación de paquetes con GSO
El experimento de GSO demostró que combinar más paquetes se traducía en un incremento de la eficacia de QUIC. Sin embargo, esto supone un coste para GSO, ya que el emisor vierte a la red todos estos paquetes a ráfagas. De este modo, se incrementa la presión a corto plazo sobre los búferes de la red y aumentan las probabilidades de pérdida de paquetes.
Ante esta circunstancia, ¿cuántos paquetes debería combinar quiclyundefined? El tamaño de ráfaga aceptable especificado para un emisor QUIC es de 10 (que, para nuestro caso, parece una recomendación bastante buena). undefinedDe hecho, quicly acaba de implementar la opción de enviar ráfagas GSO de 10 paquetes.
Hemos observado que actualmente el kernel de un emisor no puede medir estos paquetes. Es decir, no tiene capacidad para enviar los paquetes que componen una ráfaga GSO a una velocidad que impida que la red tenga que absorber la ráfaga. Nos interesaría contar con una función de ese tipo si se llegara a implementar en el kernel Linux.
Selección de un tamaño para los paquetes
Cuanto más grandes sean los paquetes, mejor es el rendimiento obtenido. Aun así, los paquetes de mayor tamaño corren el riesgo de olvidarse en las rutas de algunas redes. Una solución podría ser implementar mecanismos como Path MTU Discovery, que permiten detectar el tamaño máximo de los paquetes que se pueden utilizar en una conexión. Sin embargo, este tipo de mecanismos tiene la desventaja de que solo sirve para conexiones de larga duración. En la mayoría de las conexiones, y al comienzo de todas ellas, corresponde al emisor determinar cuál es el tamaño de paquete adecuado.
En lugar de utilizar un tamaño de paquete fijo, el servidor quicly undefinedahora decide su propio tamaño de paquete en función del tamaño de los primeros paquetes que reciba del cliente. Dado que estos paquetes lograron llegar al servidor tras recorrer la red, cabe esperar que paquetes del mismo tamaño tengan altas probabilidades de recorrer toda la red y regresar al cliente.
Conclusión y siguientes pasos
Tras haber implantado estos cambios, quicly ahora alcanza 464 Mb/s (un 1 % más rápido que TLS 1.3 sobre TCP) cuando los primeros paquetes QUIC que envía el cliente tienen un tamaño de 1460 bytes y 425 Mb/s (solo un 8 % más lento que TLS 1.3 sobre TCP) cuando los primeros paquetes QUIC que envía el cliente tienen un tamaño de 1350 bytes, es decir, el tamaño de paquete predeterminado que utiliza el navegador Chrome.
La carga de trabajo y el entorno utilizados para este experimento son insignificantes motas de polvo en el vasto ecosistema de cargas y entornos. Nuestro objetivo era comprobar si el rendimiento de QUIC podía compararse al de TCP en esta extremadamente específica microprueba de rendimiento. En posteriores pruebas y experimentos, analizaremos y mejoraremos el rendimiento de QUIC aplicado a otras situaciones y con otros ajustes representativos.
Sin embargo, este experimento demuestra que, si se utilizan optimizaciones y mecanismos de protocolos con prudencia, QUIC tiene muchas posibilidades de equipararse a TCP en términos de eficacia computacional.
Nosotros vamos a seguir con la puesta a punto del rendimiento de nuestra implementación con más pruebas y despliegues. No te preocupes, nos aseguraremos de contarte todos lo que descubramos. ¡No te lo pierdas!