La latencia y el ancho de banda

Medir velocidad de la conexión

Si intentamos optimizar la velocidad de carga de nuestra web debemos entender los conceptos de latencia y ancho de banda relacionados con las conexiones en Internet. La latencia es el tiempo que transcurre desde que enviamos una petición al servidor y recibimos la respuesta, usualmente expresado en milisegundos. El ancho de banda es el el máximo caudal de datos expresado en bits por segundo que puede transmitir una red.

El término ancho de banda es de uso general. La típica frase Tengo una ADSL de 5 megas refiriéndose a una conexión con un caudal de 5 mega bits por segundo (5 Mbps o 5 Mb/s). Esta es una medida con múltiplos decimales. Si un byte (B) tiene 8 bits (b), la operación 5 Mb/s ÷ 8 b/B = 0.625 MB/s nos da los bytes por segundo. Esto aparenta que si un sitio web tiene que descargar 0.625 MB de recursos lo podrá hacer en un segundo. Y 0.625 MB son bastantes para cubrir una página con muchas imágenes y otros recursos. Si prácticamente todo el mundo tiene ADSL ¿porqué preocuparse si la cosa parece estar en torno a un segundo? Como veremos en este tema eso no es así por el problema de la latencia. Pero veámos primero un poco más sobre el ancho de banda.

El ancho de banda de nuestra conexión ADSL lo podemos medir con herramientas como Speedtest.net, como se observa en la primera imagen más arriba. Pero esto no nos dice nada acerca de la conexión que utilizan los usuarios de nuestra web. Es posible que muchos se conecten con anchos de banda diferentes. Además el camino desde el ordenador del usuario hasta nuestro servidor puede pasar por muchos tipos de conexiones intermedias. Por ejemplo, alguién usa un portátil que se conecta con wifi a un router y este mediante cable con un servidor del ISP, que a su vez enruta la conexión por otros sistemas intermediarios hasta llegar a nuestro servidor. El ancho de banda efectivo de la conexión está límitado por el menor de los anchos de banda de todos los sistemas por donde pasa nuestra conexión. Y el problema se agrava con conexiones móviles.

Tener un gran ancho de banda es bueno cuando estamos descargando un flujo continuo de datos, como por ejemplo un archivo grande de vídeo. Pero una página web es un conjunto de archivos de distinto tipo y, a veces, de diferentes orígenes, no siendo por tanto posible descargarlo como un flujo continuo de datos. Primero se descarga el archivo con el HTML, desde donde hay vínculos para luego descargar archivos CSS, JS e imágenes, entre otros. Todos estos recursos se pueden descargar con varias conexiones que pueden actuar en paralelo, pero cada conexión tiene un coste de tiempo entre que se hace la petición y se recibe el primer byte. Por lo tanto la velocidad de carga de una página web entonces tiene más que ver con la latencia de las conexiones que con el ancho de banda.

Hay mucho que podemos consultar sobre optimización web. Pero para mi ha sido una gran ayuda Igvita.com de Ilya Grigorik, especialmente su libro High-Performance Browser Networking, por ahora gratis on-line.

Latencia y RTT (Round Trip delay Time)

Hay una diferencia entre los términos latencia y RTT (Round Trip delay Time). Este último se refiere al tiempo que tarda una petición en alcanzar el destino y volver al punto de partida, no incluyendo otras operaciones que puedan realizarse en el destino. Una forma de medir el RTT mínimo de una red es haciendo un ping a una IP:

C:\WINDOWS\SYSTEM32>ping www.wextensible.com

Haciendo ping a www.wextensible.com [217.160.124.225] con 32 bytes de datos:

Respuesta desde 217.160.124.225: bytes=32 tiempo=122ms TTL=46
Respuesta desde 217.160.124.225: bytes=32 tiempo=126ms TTL=46
Respuesta desde 217.160.124.225: bytes=32 tiempo=125ms TTL=46
Respuesta desde 217.160.124.225: bytes=32 tiempo=124ms TTL=46

Estadísticas de ping para 217.160.124.225:
    Paquetes: enviados = 4, recibidos = 4, perdidos = 0
    (0% perdidos),
Tiempos aproximados de ida y vuelta en milisegundos:
    Mínimo = 122ms, Máximo = 126ms, Media = 124ms

C:\WINDOWS\SYSTEM32>

Se trata de una conexión ADSL desde Tenerife (Canarias, España) a este sitio Wextensible alojado en 1and1 en Alemania (alrededor de 3200 km). Esta conexión tiene un RTT de unos 124 ms. Y este es un dato que de alguna forma debemos anotar si queremos seguir haciendo más pruebas. Aunque la conexión se hiciera directamente con fibra de vidrio donde la velocidad sería 2/3 de la velocidad de la luz (300000 km/s × 2/3 = 200000 km/s), recorrer esa distancia de ida y vuelta llevaría 2 × 3200 ÷ 200000 = 0.032 segundos. La física impone un RTT mínimo de 32 ms en una conexión de 3200 km. Con cable y saltando por nodos intermedios el RTT se monta hasta esos 124 ms.

Por un lado un ping no es exactamente igual que un RTT de una conexión TCP, pero es una primera estimación que nos puede servir de orientación. Por otro lado este RTT de 124 ms que obtengo es un valor optimista en horarios de bajo tráfico, como fines de semana o festivos. Al igual que el resto de pruebas que expongo en este tema, he buscado a propósito las mejores condiciones con objeto de obtener valores que no se vean perturbados por congestiones puntuales, donde el RTT puede alcanzar valores superiores.

En la latencia web interviene el anterior RTT y otras demoras de la conexión como el tiempo de operación de conformar la respuesta en el destino (servidor web). Pero una gran parte de las demoras está ocasionada por el uso del protocolo TCP/IP como veremos a continuación.

TCP/IP

TCP/IP es el modelo básico para las conexiones de Internet. Fue creado por Bob Kahn y Vint Cerf en 1974, cumpliéndose precisamente este año 2014 los cuarenta años de existencia. Por un lado IP (Internet Protocol RFC791) sirve para transmitir datos estructurados en paquetes conmutados (datagramas) entre distintas redes. Pero no es un protocolo orientado a la conexión, por lo que se dice que la entrega de datos no es confiable. El protocolo intentará buscar la mejor ruta pero no garantiza que un datagrama llegue a su destino. Para esto IP trabaja junto a TCP (Transmision Control Protocol RFC793) para garantizar que los datos (que se envian en paquetes) serán entregados en el destino libres de errores y en el mismo orden en el que fueron enviados. Esto debe ser necesariamente así porque un sólo error en un bit de un archivo HTML, JS o CSS podría afectar a un caracter y por tanto haría inutilizable el archivo. Además todos los datos deben llegar y montarse en el navegador en el orden correcto.

Pero esta garantía de entrega que nos da TCP tiene un coste en latencia web. Para entenderlo tenemos que intentar comprender como funciona una conexión TCP. La conexión pasa por tres fases: establecimiento, transferencia y finalización. En el establecimiento se aplica la negociación de tres pasos (three-way handshake). Veámos este ejemplo de comunicación que aparece en la especificación del protocolo:

      TCP A                                                TCP B
========================================================================
  1.  CLOSED                                               LISTEN

  2.  SYN-SENT    --> <SEQ=100><CTL=SYN>               --> SYN-RECEIVED

  3.  ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK>  <-- SYN-RECEIVED

  4.  ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK>       --> ESTABLISHED

  5.  ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED
    

TCP B es el receptor, como un servidor web que está constantemente escuchando en un puerto a la espera de recibir establecimientos de conexión. TCP A (emisor) puede ser por ejemplo un navegador. Un usuario pide una página web y el proceso se inicia enviando un comando SYN al servidor junto a un número de secuencia SEQ=100. Cuando el servidor lo recibe le devuelve un reconocimiento de llamada SYN ACK y su propio número de secuencia SEQ=300, al mismo tiempo que incrementa la secuencia del emisor con ACK=101 lo que prueba que este reconocimiento sigue al anterior. El emisor lo recibe, incrementa la secuencia del servidor con un ACK=301 enviando un ACK e inmediatamente sin esperar respuesta del servidor enviará otro ACK con los datos de la petición.

Por lo tanto antes de que los datos de la petición empiecen a fluir desde el servidor hasta el navegador ya se ha consumido tres conexiones:

  • Una desde el navegador al servidor haciendo una petición de establecimiento.
  • Otra desde el servidor al navegador aceptando la petición.
  • Y una respondiendo el navegador de nuevo al servidor antes de seguir inmediatamente enviando los datos de la petición del recurso (cabeceras HTTP).

Los dos ACK finales que envía el navegador en ese ejemplo salen juntos y podemos considerar que llegan también al mismo tiempo al servidor. Si tenemos un RTT de 124 ms como el visto en el apartado anterior, entonces podemos estimar que en un sólo sentido será la mitad 62 ms. Por lo que la negociación de tres pasos nos llevará 3 × 62 = 186 ms antes de que el servidor empiece a procesar las cabeceras HTTP de la petición. O dicho de otra forma, la conexión TCP completa nos cuesta 1.5 RTT.

Control de flujo y control de congestión en TCP

TCP conecta dispositivos con prestaciones muy diferentes. Es posible que un servidor pueda transmitir datos a una velocidad mayor de lo que el sistema donde los recibe un navegador pueda procesarlos. Para eso TCP implementa el control de flujo que previene al emisor de sobrecargar al receptor con datos que no podría procesar. Cada parte informa a la otra en cada ACK de su propia ventana de recepción (rwnd) que viene a ser el tamaño en bytes del espacio de buffer disponible para la recepción. En el transcurso de la conexión el rwnd puede ir variando e incluso llegar a cero, en cuyo caso la otra parte detiene el envío de datos pero manteniendo la conexión hasta que la ventana de recepción vuelva a incrementarse.

A medida que las redes hacían mayor uso de TCP se producían congestiones que el control de flujo anterior no era capaz de arreglar. Aunque el emisor y el receptor conocieran el tamaño del buffer que la otra parte era capaz de procesar, ambos no sabían nada acerca del ancho de banda de la red que los conectaba. Conociendo este dato se podría adecuar la cantidad de datos a transmitir ajustándola al ancho de banda disponible en cada momento. Esta mejora se llama el control de congestión de TCP (RFC2581) con cuatro controles llamados slow start, congestion avoidance, fast retransmit y fast recovery. Inicialmente sólo me preocupa entender que limitación impone TCP a la velocidad de carga de una página web, siendo suficiente saber como se comporta el control slow start.

Conexión TCP con control slow start

Se introduce una nueva variable TCP llamada ventana de congestión (cwnd) que limita la cantidad de datos que pueden transmitir. En cualquier momento un emisor no puede enviar más datos que el mínimo de rwnd y cwnd. Con esta regla se intenta controlar la variabilidad del proceso de datos en el receptor y al mismo tiempo el ancho de banda variable de la red intermedia que puede sufrir momentos de congestión.

Para lograrlo se usa el control slow start, donde el emisor empieza con un cwnd pequeño y lo va incrementando a medida que el receptor reciba los datos. Se define el concepto de segmento como un paquete TCP de reconocimiento (un ACK) con o sin datos. Un paquete TCP es usualmente de unos 1500 bytes pero quitando las cabeceras TCP quedan unos 1460 bytes para datos. Este valor se conoce como SMSS (sender maximum segment size). Se usan de 2 a 4 segmentos como ventana inicial (IW) según la especificación RFC2581:

IW = min(4×SMSS, max(2×SMSS, 4380 bytes))

Si SMSS = 1460 entonces:

IW = min(5840, max(2920, 4380)) = min(5840, 4380) = 4380 bytes

Estos 4380 corresponden exactamente a 3 segmentos de 1460 bytes cada uno.

En la imagen adjunta puede ver un ejemplo. Usaré el RTT descrito más arriba de 62 ms. La conexión queda establecida con la negociación a tres pasos a los 124 ms. El emisor sería un navegador que pediría un GET para solicitar una página web. El receptor sería el servidor web que recibe a los 186 ms las cabeceras HTTP del GET suponiendo que caben en un único segmento TCP. El proceso de servir una página web puede llevar un tiempo en el servidor. En este ejemplo suponemos que tarda 50 ms, tras lo cual el servidor empieza el control slow start enviando 3 segmentos que son 4380 bytes. Si estos segmentos llegan al navegador, éste enviará los tres ACK al servidor, quien al recibirlos entiende que el receptor y la red tienen cabida para ese tamaño de ventana y la duplicará a 6 segmentos para el siguiente envío. A los 422 ms el navegador ya ha recibido 13140 bytes.

El tamaño de la ventana de congestión cwnd seguirá incrementándose exponencialmente hasta que alcance el tamaño de la ventana de recepción rwnd, que tiene en cualquier caso un máximo de 65535 bytes (64 KB) puesto que un paquete TCP tiene un tamaño determinado y la cabecera que especifica el tamaño del rwnd no admite un valor mayor. La serie completa hasta llegar a ese máximo sería así:

PasoSegmentosTamaño
(bytes)
Recibidos
(bytes)
Tiempo
(ms)
1343804380298
26876013140422
3121752030660546
4243504065700 (max 65535)670

En el cuarto paso ya habremos sobrepasado el rwnd y necesitaremos unos 670 ms para recibir algún archivo con tamaño entre 30660 y 65535 bytes. Para este valor tenemos una tasa de transferencia de 65535/0.67 = 97813 B/s muy lejos de los 625000 B/s de una ADSL que corra a 5 Mbps como vimos en el primer apartado.

Para evitar esa disparidad, en los últimos años se ha incrementado el IW como refleja Increasing TCP's Initial Window (RFC6928), para llegar hasta 10 segmentos con lo que podemos partir de 14600 bytes con un MSS de 1460 bytes. En el ejemplo anterior tendríamos 14600 bytes disponibles a los 298 ms con el primer RTT tras el establecimiento de la conexión. Por eso es muy importante que el HTML no supere los 14 KB para tenerlo disponible con el primer RTT.

Conexión HTTP persistente (Keep Alive)

Cabeceras Keep Alive de HTTP

Una conexión HTTP persistente o HTTP keep-alive trata de reutilizar una conexión TCP para servir más de una petición con el mismo establecimiento. Vimos antes que establecer una conexión TCP tiene un coste de 1.5 RTT, unos 186 ms en el ejemplo. Pero si quitamos el último ACK/GET tenemos un único RTT (124 ms). Con keep-alive cuando solicitamos una página web establecemos la conexión TCP y luego podemos descargar varios archivos HTML, JS, CSS o imágenes consecutivamente sin tener que gastar un RTT para cada conexión de cada archivo.

La conexión seguirá abierta hasta que emisor o receptor la cierre. También hay un tiempo de espera o timeout para cortar la conexión por inactividad. Este tiempo puede ser configurado por el servidor. En mi Apache 2.2 que tengo en localhost tiene la directiva KeepAliveTimeout con un valor de 5 segundos por defecto, pero la de este sitio en producción sólo tiene 2 segundos. Ese timeout no se puede modificar en un htaccess, por lo que no tengo posibilidad de incrementarlo. Es muy poco tiempo, pero es lógico que sea así pues es un alojamiento compartido, dado que si un servidor tiene muchas conexiones abiertas le supone una merma de rendimiento.

Todos los navegadores envían siempre una cabecera keep-alive y los servidores con HTTP 1.1 lo tienen activado por defecto como dije antes, pero no está de más comprobarlo porque es un pilar importante para mejorar la velocidad de carga de nuestras páginas.

Test para probar la conexión web con el servidor

Para saber si un servidor web Linux está actualizado a IW10 por lo visto tiene que tener una version del kernel igual o mayor a 2.6.30. Este sitio está en un alojamiento compartido y no tengo mucha información del servidor por lo que intentaré hacer una prueba del comportamiento de una conexión. He desarrollado una utilidad para hacer un test de la conexión web de nuestro servidor. Se trata de descargar archivos de texto mediante XMLHttpRequest y medir el tiempo que tarda desde la petición hasta el recibo del archivo. Es posible cargar una serie de 33 archivos desde 2000 hasta 66000 bytes en tramos de 2000 bytes. O bien una serie con tramos de 1000 bytes. El contenido de los archivos es irrelevante, sólo contienen el caracter "0" repetido para completar el tamaño. Tras recuperar cada archivo no hacemos nada con ese contenido. Los datos se traspasan a una gráfica usando mi visor de gráficas lineales. Hay un control para especificar el tiempo de espera entre dos pruebas, cuya utilidad explicaré más abajo. Estas son una muestras de resultados obtenidos:

Conexión TCP/IP
Gráfica de la recuperación de un archivo con TCP/IPImagen no disponible
Descarga de archivos desde 2000 hasta 66000 bytes en tramos de 2000 bytes y con un tiempo de espera de 2 segundos. A partir 14000, 32000 y 60000 se observan saltos importantes.

Los test de las capturas de imagen anteriores han sido realizados con la conexión descrita más arriba, con un navegador en Tenerife (Canarias, España) hasta el servidor de este sitio Wextensible.com alojado en Alemania y con una ADSL de 5 Mbps. Son unos 3200 km con un RTT de 124 ms. El navegador es Chrome 34 en Windows y el servidor es Apache en Linux. El servidor está configurado con un timeout del keep-alive de 2 segundos. Entre cada prueba dejaremos pasar ese tiempo con objeto de que cada test se haga en las mismas condiciones, incluyendo la conexión TCP y el reinicio de la ventana de congestión.

Lo importante no son las mediciones de tiempo sino más bien el comportamiento de la conexión según el tamaño del archivo. Porque hay otras cosas como el DNS Lookup que no sé si aplican a cada test puesto que el navegador puede pre-resolverlo (preresolve). En cuanto a la capacidad de hacer pre-conexiones TCP que también tienen los navegadores (preconnect) creo que ahora no aplican porque cada conexión previa está ya cerrada por el servidor. De todas formas he probado ejecutando Chrome con estos parámetros para desactivarlos:

--disable-prerender-local-predictor 
--dns-prefetch-disable  
--disable-preconnect
    

Con esto desactivado los resultados obtenidos son similares. Ilya Grigorik nos aclara algo sobre las optimizaciones que puede introducir el navegador en Chapter 10. Primer on Web Performance, capítulo que forma parte de libro High-Performance Browser Networking.

De todas formas como dije lo importante es el comportamiento que se produce más que los tiempos absolutos, observándose en la primera imagen un salto en 14000 bytes así como en 32000 y 60000. En la segunda imagen vemos con detalle que el primer salto es entre 14000 y 15000 bytes y tiene una longitud de aproximadamente un RTT (124 ms). Esto es un indicativo de que el servidor duplicó el tamaño de la ventana de congestión inicial de 14600 bytes.

Como dije en una nota aclaratoria más arriba, estas pruebas las hice en condiciones óptimas, con un único ordenador conectado a la ADSL con el mínimo de procesos abiertos, evitando conexiones a la ADSL de otros dispositivos por wifi y buscando días de fines de semana donde hay menos tráfico web. Desechaba aquellas pruebas que dieran muestras de congestión. En la tercera imagen se observa un pico al descargar el archivo de 20000 bytes por un congestión puntual en cualquier punto de la red.

La última imagen nos permite apreciar el efecto del keep-alive. Al no dejar tiempo de espera entre pruebas la conexión se mantiene abierta. La descarga del primer archivo tiene un coste cercano a los 300 ms, parte de cuyo tiempo es para el establecimiento de la conexión TCP. Sólo se lleva a cabo con el primer archivo y ya no vuelve a producirse para el resto de descargas. También nos aprovechamos del slow-start puesto que la ventana de congestión no se reinicia en ningún momento. Así que si tenemos que traer muchos archivos con XMLHttpRequest esta sería la mejor forma de hacerlo, traerlos todos juntos con una única conexión.

De hecho el navegador se aprovecha de este efecto para decargar más rápido los recursos de una página web. La siguiente imagen es una captura de un test realizado en el sitio WebPagetest a la página de inicio de este sitio (ver el informe completo en pdf):

Test en webpagetest, vista conexión

Esta herramienta es muy útil para observar el comportamiento de la conexión. Vemos las barras de color naranja que son las seis conexiones TCP en paralelo por dominio que puede hacer el navegador para recuperar más rápido los recursos de la página. En la primera se consulta el DNS y se recupera el HTML y, con la misma conexión, el JS. Las otras conexiones recuperan dos o incluso hasta tres archivos por conexión. Esto es posible por el keep-alive de la conexión.

Es obvio que tenemos que comprobar como se está comportando nuestro servidor y buscar la forma de adaptar nuestras páginas para aprovechar todos los recursos posibles:

  • Keep alive para traer varios recursos con una conexión TCP.
  • HTML con un tamaño menor de 14 KB para descargarlo con un único RTT.
  • Conexiones en paralelo del navegador para recuperar varios recursos al mismo tiempo.

Estas son sólo algunas cosas relacionadas con las conexiones para mejorar la velocidad de carga. Porque hay otras como reducir el RTT mediante el uso de CDN's o el problema que supone la pesada carga de las imágenes y la posibilidad de descargarlas desde otro dominio (con lo que se pueden usar otras seis conexiones paralelas), son otros temas que tendré que analizar.