Friday, October 10, 2014

Los números que todos deberíamos conocer

Nada resume mejor la filosofía para diseñar sistemas dentro de Google que los números que todo ingeniero de software debe conocer. Ahí tenemos los tiempos de respuesta de la mayoría de operaciones fundamentales que podemos realizar en un ordenador.

Con esos números podemos diseñar una página web rápida. Si algo va lento entendemos el por qué y en la mayoría de los casos lo podemos solucionar.

Menos conocido es otro documento donde se publican el tiempo de vida de todos los componentes de un centro de datos. Cuanto tarda de media un disco duro en romperse, cuanto tarda una tarjeta de red, cuantos segundos de caída al año son debidos a un problema con un router, cuantos de forma prevista y cuantos de forma imprevista, cuantos son debidos a un problema de suministro eléctrico, etc.

Con esos números en mano, podemos diseñar sistemas robustos. Si estamos sirviendo imágenes sabemos que no podemos guardarlas todas en un solo disco duro, porque si tienes muchos discos duros esas probabilidades de error teóricas se convierten en perdida de fotos de forma rutinaria. Necesitas guardar una copia en otro disco. Si tu disco principal se rompe, necesitas hacer una copia del secundario en otro disco y tirar el original. Pero mientras haces la copia, ¿qué disco sirve las fotos que se estén viendo en ese momento? Desgraciadamente necesitas tener preparadas dos copias distintas de cada foto para no perder fotos y no dejar de servirlas en caso de problema con un disco duro.

Cuando diseñas un sistema tienes que tener en cuenta que tu disco duro se puede romper, que tu ordenador se puede romper, que tu router se puede romper, que pueden configurar mal el enrutado a tu servidor, que se puede cortar la electricidad, que tu rack se puede incendiar, etc.

Desde el punto de vista del sysops / devops / SRE (o ingeniero de sistemas) eso significa que tienes que estar listo para que en cualquier momento tengas que cambiar de servidor o incluso de centro de datos, y eso con un tiempo de caída nulo, o el mínimo aceptable por tus usuarios.

¿Por qué no se hacen esos sistemas más robustos?

El primer motivo es porque esas capas extra de seguridad que se añaden para que un sistema aguante cuando hay un problema en una de las capas no son independientes entre sí.

Por ejemplo, recuerdo que un día un centro de datos se quedó sin electricidad. Todos los centros de datos contaban con un suministro de electricidad secundario usando motores diesel. ¿Cómo podía haber fallado la electricidad general y el suministro de emergencia al mismo tiempo? La electricidad había fallado porque era invierno y había una gran demanda, acrecentada por el alto precio de la gasolina aquel invierno. Cuando encendieron los motores diesel se apagaron a los pocos segundos, sencillamente porque no había gasolina en los tanques. La habían robado. El alto precio de la gasolina había provocado un error en dos sistemas que se habían considerado teóricamente independientes.

En ese caso la solución era sencilla, se introdujo dentro del protocolo de mantenimiento de los centros de datos una revisión y puesta en marcha rutinaria de los motores diesel.

Otra vez perdimos completamente la conectividad a un centro de datos entero. Era algo raro, porque el centro estaba conectado con dos cables de fibra óptica distintos, para que si se producía un problema en uno de ellos el otro aguantase todo el tráfico. El motivo fue que en primavera los cazadores de la zona se distraían disparándole a los cables, y claro, a los pocos minutos de acertar con el primero acertaron con el segundo.

Aquí la solución fue intentar que uno de los cables fuese soterrado, pero tenía un alto sobrecoste.

Como os podéis imaginar es necesaria mucha experiencia práctica para conocer los tipos de errores que se suelen producir, y una vez se conocen esos errores hay que tener cuidado para diseñar un sistema que resista a esos errores.

Tenemos otro ejemplo dramático estos días con el contagio de ébola de una enfermera del Hospital Carlos III. El que diseñó el protocolo para tratar a los pacientes de ébola tienen que preveer que los que tratan a un enfermo de ébola no deben de tocar su piel ni sus fluidos, si lo hacen la probabilidad de contagio es de cerca del 100%. Si les ponen un traje que impida el contacto directo, tienen que preveer que el traje no va a transpirar, va a dar calor y que hay una alta probabilidad de que alguien se toque al quitárselo. Si se desinfecta el traje en una sala intermedia antes de salir baja aún más esa probabilidad. Si se ponen desinfectantes en la sala donde está el paciente para que los auxiliares puedan limpiar todas las zonas del traje en contacto con el paciente mientras le atienden, aún baja más esa probabilidad. Si al salir un compañero va desinfectando todas las partes del traje que te quitas antes de que te las quites aún mejor. Puede ser que hayas tocado algo sin darte cuenta y no lo limpiases inmediatamente en la habitación, lo que es normal con un traje voluminoso, y que tampoco se haya limpiado correctamente en la habitación intermedia por ejemplo porque quedó en algún pliegue que no había sido bien rociado. Para terminar, que otra persona te vaya ordenando que te quites el traje parte a parte siguiendo un orden establecido. La cara debería ser la última en quedar al descubierto ya que es la parte del cuerpo más crítica (por donde puede entrar el virus). Las manos debería ser la penúltima, ya que nos las llevamos muy a menudo a la cara. Antes de liberar la cabeza volver a desinfectar las manos.

Con un protocolo así un error humano no sería equivalente a una infección. El protocolo no es infalible, todo el mundo debe seguirlo rigurosamente para que sirva de algo, y cuando alguien lleva meses haciendo algo sin ninguna consecuencia negativa acaba relajando las medidas de seguridad. Hay que ir rotando al personal para que tengan destreza pero sin perderle el respeto a la enfermedad.

Todos los que critican el error humano que ha llevado a esta infección no son capaces ni de diseñar ni de criticar un protocolo.

Saturday, September 14, 2013

El vendedor de tomates

Hace un par de años estuve de viaje en Cuba. Una de las cosas que más me sorprendieron es la cantidad de cosas que son ilegales. Si eres cubano, no puedes comprar gambas. No puedes matar vacas. Si tienes un negocio, solo puedes vender aquello que hayas creado tú mismo. No puedes salir a pescar con bote. Los impuestos que se pagan por tener una habitación en alquiler son de cerca del 90% de lo que se gana en temporada alta, con lo que es inviable tener habitaciones en alquiler en temporada bajas.

Un día en Cienfuegos un hombre de unos 40 años da unos golpecitos discretamente en la ventanilla de nuestro coche. Miraba nervioso a derecha e izquierda, y ocultaba algo bajo la chaqueta. Cuando le preguntamos qué quería saca de la chaqueta una lata de tomate, y nos dice que la vendía. Nuestro guía llegó rápidamente a un acuerdo con él y le compró la lata.

Las prohibiciones son tales, que el pueblo se veía obligado a violar la ley si quería sobrevivir: las casas tenían una habitación declarada en alquiler y otra más sin declarar, a las vacas les daba por cruzar las vías del tren cuando iba a pasar un mercancías, para que un guía te llevase en coche te daban un documento dónde asegurabas ser un biólogo español que venía a colaborar con la Universidad de la Havana y que ellos te llevaban gratuitamente en coche para poder desempeñar tu labor (?!).

La situación para un estado represor es ideal. Pueden detener a quién quieran, ya que todo el mundo está haciendo algo ilegal.

Hoy los que nos dedicamos al mundo de internet en España estamos en esa situación. Desde el 1 de abril 2012 es obligatorio obtener el consentimiento informado del usuario antes de instalarle una cookie (la definición legal de cookie de la normativa europea es amplia, va más allá de la definición tradicional de una cookie y cubre cualquier cosa almacenada en el disco duro del usuario).

Repito, el consentimiento va ANTES que la cookie. No basta con poner un texto diciendo "Al utilizar nuestros servicios, aceptas el uso de las cookies" si al mismo tiempo que pones ese texto sirves automáticamente una cookie. De pasada, me gustaría saber que piensa el legislador que entiende una persona de la calle cuando ve ese mensaje.

La lista de páginas web que incumplen la normativa es amplia, empezando por Google (sirve las cookies PREF y NID antes de obtener el consentimiento), Microsoft (sirve las cookies A, ACOOKIE, MC0, MC1, MS0, MUID, WT_FPC, ...), La Moncloa, la página del Ministerio del Interior, y un larguísimo etc.

¿Has decidido abrir una página para tu negocio en Facebook, Blogger, Wordpress, Google+? Estás incumpliendo la normativa y te pueden multar con hasta 30.000€ si la infracción es leve, hasta 150.000€ si la infracción es grave. Este punto lo dejó clarísimo D. Jesús Rubí Navarrete, Adjunto al Director de la Agencia Española de Protección de Datos (AEPD) (podéis ver el interesante artículo de Pablo Fernández Burgueño al respecto). Por cierto, mientras el Sr. Rubí nos dejaba bien clarito que si poníamos una página en Facebook nos exponíamos a las consecuencias de la ley, la misma AEPD tenía una página en Facebook. Ahora la han quitado, no sé si tuvieron que auto denunciarse o si arreglaron este problemilla por las buenas...

Hoy estamos todos en la situación de aquel vendedor de tomates: nos han colocado en una situación ilegal, cuando lo único que intentamos es ganarnos la vida creando algo útil.

Monday, July 26, 2010

Got back from holidays, and after copying my photos to my Windows I see (again) that the default Windows picture viewer doesn't respect the orientation flag in the EXIF tags.

I fired mogrify from imagemagick to rotate the pixels in the photos according to the EXIF orientation, so that stupid Windows can show them correctly

> mogrify -auto-orient *.JPG

So far so good, but then I suddenly I thought, is ImageMagick rotating JPEG photos loss less? For the non initiated, JPEG compress your photos very well, but at the expense of not storing exactly the color of each pixel. If you read and store again the photo, you're doing it probably with slightly different settings, so the program that stores the photo creates a new JPEG that stores the photo with more incorrect pixels. Even worse, it tries to reproduce the bad pixels the first encoding created, so the result is worse than what you expect. After a few transformations, what used to be a "slight invisible difference" between the original and the first JPEG, becomes a big drop in quality in the last JPEG. Most people regularly rotate several times the photos, resize them, and crop them. If after each step you save the photo and reload it, you're degrading the photo.

I did a quick test to check if ImageMagick rotates lossless-ly the photos, putting the rotated photo over the original in GIMP, choosing the difference filter. If everything is pitch black then it's lossless. I got something that looked black, but looking at the histogram showed that not everything was completely black. I then switched solid black to solid white, and the you can see the result here:


Arggh, so I lost a bit of quality on all my photos from my last trip to Paris. I then tried to make something constructive out of this, looked up this issue and the ImageMagick guys recommend to use jhead & jpegtrans to do lossless rotations, crop, resizes in JPEG images. These tools don't read the pixels and try to encode them again, they know how JPEG works are thus able to do these operations lossless-ly if you don't try to modify any of the JPEG blocks in the image (of 8x8 pixels).

So I tried it, and this time I tested it before I apply it to my new photos from Switzerland:

> jhead -autorot IMG_3778.JPG

And tried to see the difference again the GIMP. This time I got a much better histogram, but still not pitch black. Again, changing solid black but white showed some noise, more or less uniformly distributed over the image. Zooming in to 1:1 I got (cropped a 200x200 part of the image):


I don't understand this time why is there any difference at all. Anybody has any clue?

P.S.: To my girlfriend, that's why I'm so slow copying & selecting the photos from our trips...

Thursday, July 01, 2010

Performance bug in AppStats

I was trying to upload a CSV file with 100 entries to my local dev_appserver, but it was way too slow (30s). I first thought it was due to the datastore file implementation, and tried using the sqlite one, but it still took 30s to complete.

After a few hours putting time.clock all around the place, it turns out the problem was in AppStats. When AppStats collects all the info about a request, it stores in memcache the trace of every AppEngine API call. My code is like this:

@local_or_admin_required
def post(self):
# csv_data comes from FieldStorage, and these fields are not converted to
# Unicode by AppEngine / WebOb. So csv_data is a utf-8 bytestring.
csv_data = self.request.get('file_to_convert')
read_pos = csv.DictReader(csv_data.split('\n'))
generated_pos = [self.convert_press_office(po) for po in read_pos]
po_with_errors = []

if self.request.get('import_file_after_conversion') == 'true':
# Write the result to the datastore
for po in generated_pos:
name = po.name
del po.name

try:
update.add_press_office(name, **po)
except update.DuplicatedPressOfficeName:
...

add_press_office makes 3 API calls, so in total I'm making 300 API calls. For each call AppStats records the entire stack frame, with local variables. This includes csv_data, generated_pos, that are 50K and around 100K respectively. Total more than ~45M.

The 300 stack traces with pointers to local variables (csv_data, generated_pos, ...) are sharing the 50K and 100K of these variables, but as soon as you try to serialize it in a protocol buffer, it copies this information for each stack. And this happends in appstats/recording.py@356 in the function Recorder::get_both_protos_encoded. This function first encodes the full protocol buffer, and wastes 30s doing it. Then it discovers that "oh no! this thing is too big to keep it in memcache!" and it deletes the local variables from the stack traces. But at this point is too late and you already paid the 30s.

If you have this problem, add "appstats_MAX_LOCALS = 0" to your appengine_config.py, you will lose the value of local variables in AppStats but it will be faster.


Tuesday, May 04, 2010

Barcamp Málaga 2010

Al final me he dejado convencer y voy a organizar junto con Jose una Barcamp en Málaga. Sabemos seguro que habrá buen tiempo y bares para salir después de la barcamp, lo que pase en la barcamp ya es más incierto, pero parece que tendremos la asistencia de varios "startaperos", y que todos compartiremos nuestros secretillos (eso el que aún tenga alguno).

Si quieres saber lo que empresas que se crearon con dos duros en el bolsillo pero mucha ilusión y trabajo hicieron, entonces eres bienvenido, para más datos mira la web de la Barcamp en Málaga.

Sunday, April 06, 2008

Escalabilidad en el servidor

Al principio fue Apache, con un modo de funcionamiento extremadamente robusto. Cada petición se sirve en un proceso hijo distinto. Si algo va mal, el proceso hijo se colgaría, pero no afectaría en absoluto al resto de peticiones servidas por los otros procesos. Método lento, ya que crear un nuevo proceso es lento, pero seguro. Para mejorar el rendimiento se suele usar un "pool" (grupo) de procesos latentes, para no tener que crear un nuevo proceso con cada petición. Así nos ahorramos la creación de un nuevo proceso por petición.

Si queremos servir páginas web dinámicas y usamos Apache tenemos principalmente dos posibilidades, usar un "pool" de procesos PHP, qué comunicarán con Apache usando FastCGI, o usar ejecutar PHP directamente desde Apache con mod_php (en lugar de PHP/fastCGI podemos usar Python/SCGI o cualquier otro lenguaje). En el segundo caso, la comunicación entre Apache y el intérprete será más rápida ya que no necesita una comunicación entre procesos (Apache - PHP) por cada petición. Para servir una petición, Apache comunicará con uno de sus procesos libres del pool, y si es una petición dinámica y ese proceso aún no ha cargado mod_php, lo cargará y ejecutará el script correspondiente.

El numero máximo de recursos por segundo que podremos servir antes de agotar la memoría será:

nb_procesos_max = mem_total / mem_proc
recursos_seg = nb_procesos_max / seg_recurso

Donde mem_proc es la memoría que ocupa cada proceso Apache y seg_recurso es el tiempo medio que tardamos en servir un recurso.

Por ejemplo, si tenemos 2 GB de memoria, cada proceso consume 20 MB, y de media tardamos 0.1 segundos en servir una petición, podremos servir un máximo de 2000 / (20 * 0.1) = 1000 peticiones por segundo. Con este modelo, si queremos activar KeepAlives (ver el artículo sobre cómo acelerar la descarga de páginas web) durante 10 segundos, tendremos un límite de 2000 / (20 * 10) = 20 peticiones por segundo. ¡Hemos dividido por 100 nuestro máximo!

Para poder activar KeepAlives sin matar nuestro servidor, o simplemente si queremos consumir menos memoria, tenemos que cambiar de modelo. Sabemos que la mayoría de recursos son simples ficheros estáticos (js, css, png, jpg, etc.). No necesitamos un intérprete PHP para servirlos. De hecho, es tan sencillo que podemos hacerlo directamente desde el proceso principal del servidor web, sin necesidad de usar un proceso secundario ya que la posibilidad de que el proceso se cuelgue haciendo algo tan simple es prácticamente nula. Apache (<= 2.1) no puede funcionar en este modo, y tendremos que usar otro servidor Web, como lighttpd, nginx o cherokee.

Para los recursos dinámicos, podemos mantener un pool de procesos PHP y comunicar con ellos usando fastCGI, o simplemente seguir usando Apache y comunicar con Apache por HTTP (ideal para una migración rápida, ya que podemos seguir usando la configuración previa). La comunicacion entre servidor y PHP será algo más lenta, pero como ventaja desacoplamos el tiempo de vida de la conexión TCP con el del proceso PHP. Lo único que hay que hacer es pedirle a Apache que escuche en otro puerto (Listen 8080) y pedirle al servidor de recursos estáticos que actue de reverse proxy hacia localhost:8080 para todas las peticiones dinámicas.

La mayoría de servidores que permiten este modo de funcionamiento, permiten también usar un pool de procesos para servir los recursos estáticos, pero esta vez un pool pequeño y fijo (por ejemplo de 4 procesos). De este modo podemos aprovechar varios procesadores o un procesador multicore, y paralelizar las peticiones en caso de que alguna llamada al sistema bloquee (en teoría todas las llamadas que se usan son asíncronas, en la práctica a veces bloquean).

Consejos para acelar las páginas web - Parte II

Ya hemos hablado de la importancia de la latencia en el tiempo de descarga de las páginas web. Reducir el número de "three-way handshakes" activando KeepAlives, el número de ficheros a descargar, paralelizarlos sobre un máximo de 4 dominios, etc. son consejos importantes para reducir el impacto de la latencia, pero hay algo que sobrepasa a cualquier otra técnica para reducir el tiempo de espera cuando queremos usar un recurso (imágen, hoja de estilo, javascript, etc.), y es no mandarle nada al navegador, siempre que este tenga en caché una copia del recurso en cuestión.

El navegador divide los recursos que tiene en su caché en dos tipos, los "frescos" y los "dudosos". Los recursos frescos pueden usarse sin preguntarle nada al servidor. Los recursos dudosos se pueden usar, pero antes hay que preguntarle al servidor que nos lo envió si sigue siendo válido, o si hay que tirarlo a la basura y usar una nueva versión que el servidor le mandará al navegador.

¿Cómo controlar si un recurso entra en la caché, y si lo hace el tiempo qué permanece fresco? Con las cabeceras Cache-Control o Expires. La cabecera Expires contiene una fecha a partir de la cual el recurso debe ser considerado como dudoso. Si la fecha está en el pasado, o si no es una fecha (como "0"), se considerará inmediatamente al recurso como dudoso. La cabecera Cache-Control ofrece muchas más opciones, permitiendo por ejemplo especificar si un proxy que se encuentre entre el usuario y el servidor puede guardar el recurso en caché o si sólo el navegador puede hacerlo. Podéis leer la especificación para ver todas las subopciones de Cache-Control, siendo una de las más importantes max-age, que especifica durante cuantos segundos se tiene que considerar el recurso como fresco. Si se usa Cache-Control, toma prioridad sobre Expires.

Recordemos que si un recurso es fresco, el navegador lo usa directamente sin decirle nada al servidor. Más rápido imposible.

Si el recurso es dudoso, ¿cómo puede el navegador comprobar si ha cambiado en el servidor o no? La pregunta, en prácticamente todos los casos es completamente irrelevante. Es mejor que vuestros recursos siempre se mantengan frescos, poniendo un Expires o un Cache-Control max-age de muchos años. O están en la caché y pueden usarse directamente, o no están y hay que pedírselos al servidor.

¿Por qué muchas páginas los dejan pasar al estado "dudoso" (poniendo pequeños tiempos de expiración)? Porque quieren que si cambian el recurso, el navegador se baje la nueva versión. Esto es extremadamente problemático, porque en la práctica es imposible predecir cuando se va a cambiar un recurso, asi que no le podemos decir al navegador hasta cuando lo puede mantener fresco, o nos arriesgamos a que el navegador use una versión antigua de algunas de nuestras imágenes, páginas de estilo, javascript, etc.

Tenemos tres alternativas para atacar este problema:


  1. Ignorarlo. Es lo que hacen todos los que ponen un tiempo corto en Expires. Es incorrecto porque el cliente puede ver una nueva versión de nuestra página web con una mezcla de recursos antiguos y nuevos. Es ineficiente, porque hace que el cliente le haga una petición al servidor para comprobar la validez del recurso una vez caduca, requiriendo un mínimo de una ida y vuelta al servidor, y más típicamente dos idas y vueltas (ver el artículo anterior sobre la importancia de la latencia)

  2. Poner el Expires a 0. Es correcto, ya que el cliente siempre ve la última versión del recurso. Es ineficiente, ya que siempre es necesario comprobar la validez del recurso.

  3. Nunca cambiar un recurso. Más especificamente, cuando quieras crear una versión nueva de un recurso, no le des el mismo nombre que otro recurso ya existente en tu página web. Es correcto, ya que la versión en la caché y en el servidor de un recurso siempre es la misma. Es eficiente, ya que si se sigue usando ese recurso y está en la caché, el navegador nunca le pregunta al servidor.



La última opción es obviamente la ideal, y la que más trabajo necesita por parte del desarrollador, ergo es la opción menos usada. En realidad no es tan dificil de implementar. En lugar de poner en el HTML enlaces como "imagen.png", pondremos un enlace llamado "imagen.png?v=1". El servidor evidentemente ignorará el "?v=1", pero el navegador eso no lo sabe. Cuando actualicemos "imagen.png", cambiamos el número de versión, y en el HTML ponemos "imagen.png?v=2". Asi pues, en el mismo instante en que tengamos una versión nueva de "imagen.png" el navegador tendrá que pedírsela al servidor (él no tiene en su caché nada sobre "imagen.png?v=2"). Si queremos aumentar la legibilidad, o evitar que algún proxy no cachée nuestra imagen por usar un ? en la URL, podemos escribir imagen.v2.png, y eliminar el .v2 con mod_rewrite en Apache. Con un poco de esfuerzo, todo el proceso puede automatizarse. En Panoramio por ejemplo tenemos una tabla con el MD5 de todos los recursos estáticos qué usamos, y su número de versión (excepto para las imágenes que suben los usuarios). Cuándo subimos una nueva versión de Panoramio, un script comprueba el MD5 de todos los recursos, y si este cambia, aumentamos su número de versión. Todas las URLs a nuestros recursos las generamos a través de una función que le añade a la URL el número de versión que hemos calculado anteriormente. Todo esto se basa en que nuestro HTML no debe de estar cacheado, o los clientes puede recibir una versión antigua del HTML con una versión nueva de algunos recursos.

Si teneis algún recurso para el que este tipo de solución no es práctica, entonces tendreis que dejar que vuestros recursos pasen al estado dudoso (o no cachearlo en absoluto). Para comprobar si un recurso sigue siendo válido, el navegador le pregunta al servidor si ese recurso ha cambiado desde que se metió en la caché, usando la cabecera If-Modified-Since. Si no ha cambiado, el servidor responde con HTTP/304 Not Modified, y si ha cambiado responde enviando el nuevo contenido.

Si nuestro recurso no tiene una fecha de modificación clara (por ejemplo, si se ha generado dinámicamente), este método de verificación no nos servirá. Si ese es nuestro caso, al enviar el recurso la primera vez tendremos que añadir la cabecera ETag, que contendrá un identificador único que deberá cambiar cuando cambie el recurso. Los ETag que tanto Apache como IIS generan no dependen exclusivamente del contenido del fichero, si no que también dependen de datos especificos del servidor (por ejemplo, el inode donde se encuentra el fichero). Si usamos varios servidores, este ETag será inutilizable. En la práctica hay muy pocas razones para usar un ETag, siendo la validación por fecha suficiente en la mayoría de los casos.

En el anterior artículo de esta serie preguntasteis por herramientas para poder medir el tiempo de descarga de páginas web. Una formidable es Fiddler, desgraciadamente sólo disponible para Windows. Especificamente para Firefox tenemos firebug, sin duda el plugin de desarrollo más útil que existe para Firefox, y que incluye un panel donde podemos ver el tiempo que tarda cada petición al servidor, pero ¡cuidado al interpretar los resultados!, firebug muestra cuando las peticiones entran en la cola de peticiones de Firefox, no cuando se despacha la petición realmente al servidor.

El autor de Fiddler tiene un artículo donde habla también sobre el rendimiento de páginas web, y donde explica con algo más de detalle las diferentes cabeceras involucradas en todo el proceso de cacheo.

Queda bastante que hablar de este tema, sobre la compresión de los recursos en el servidor, y sobre todo de los distintos bugs en servidores y clientes que le ponen un poco de pimienta a la vida, pero ya es tarde. ¡Hasta la próxima!

Consejos para acelerar las páginas web - Parte I

La velocidad de carga de las páginas web viene determinada por muchos factores, pero los más importantes son la combinación de la latencia y el número de recursos a descargar, y por otro lado el ancho de banda y el tamaño de estos recursos.

Como diseñadores web, lo único que podemos hacer para disminuir la latencia entre nuestros servidores y nuestros usuarios es colocar los servidores lo más cerca posible de nuestros usuarios, o distribuirlos por todo el planeta si nuestra página web no tiene un uso mayoritario en un solo país. Pero están soluciones suelen ser muy caras y no están al alcance de todo el mundo.

En la práctica, la latencia y el ancho de banda del usuario lo tendremos que tomar como elementos incontrolables. Ya que el ancho de banda ha ido creciendo exponencialmente desde hace unos años, este elemento ya no tiene la importancia tan crítica que tenía hace tan solo unos años, y el tiempo de espera debido a la latencia y el número de recursos a descargar ha cobrado más importancia.

Veamos pues en detalle el proceso de descarga de una página web, y por qué afecta tanto la latencia al tiempo total de descarga. Cuando el navegador tiene que mostrar una página web nueva, digamos http://www.ejemplo.com/, primero tiene que convertir el nombre del dominio a una dirección IP con la que poder conectar, asi que contacta con los DNS de nuestro ISP para conseguir esta dirección IP.

Con la dirección IP en su mano, intenta establecer una conexión TCP con ese servidor, siguiendo el proceso conocido como “three-way handshake”. El navegador envía un paquete SYN y espera pacientemente la respuesta del servidor, que será un paquete SYN + ACK. Cuando el navegador reciba el paquete respuesta del servidor ya puede mandar un paquete ACK de respuesta e inmediatamente lanzar una petición HTTP GET del recurso que nos interesa (en nuestro ejemplo, “/”), a lo que el servidor responderá por ejemplo, mandando el contenido del fichero “index.html”.

Si el tiempo que tarda un paquete en llegar de nuestro navegador al servidor es de 100 ms, necesitaremos 400 ms debido a la latencia antes de que el navegador consiga el primer byte de contenido real, y ya podemos tener una flamante conexión a 20 Mb/s que no reducirá nada este tiempo de espera. A partir de aquí, y hasta que el navegador termina de recibir “index.html” pasará un tiempo dominado por el tamaño de este fichero y el ancho de banda de que dispongamos. Si “index.html” pesa 100 KB y tenemos una conexión de 4 Mb/s tardará unos (100 * 8) Kb / (4 * 1024) Kb/s * 1.2 = 235 ms. El factor “mágico” de 1.2 tiene cuenta de la sobrecarga típica de datos a enviar debido a la encapsulación de estos datos en paquetes.

Una vez con el contenido del HTML, el navegador procede a interpretarlo, y típicamente encontrará dentro del HTML referencias a otros recursos, como hojas de estilo CSS, Javascript, imágenes, etc. Para cada uno de estos recursos extra se repite todo el proceso anterior, asi pues si hacemos referencia a 10 recursos, un número muy conservador hoy en día, perderíamos 10 * 400 ms = 4 s extra debido a la latencia.

Afortunadamente el navegador paraleliza estas peticiones, pero para no sobrecargar al servidor no hace más de dos peticiones a la vez al mismo dominio. Mención especial merecen los ficheros Javascript, que no se descargan en paralelo. Si en nuestro HTML no usamos ningún fichero Javascript, los 10 recursos añadirán entonces 10 * 400 ms / 2 = 2 s extra.

¿Cómo podemos “engañar” al navegador para que descargue más ficheros en paralelo de nuestro servidor? Hay un truco muy sencillo, y es crear varios subdominios que apunten al mismo servidor, y dividir el enlace en el HTML a nuestros recursos entre estos subdominios. El navegador empezar a descargar en paralelo un máximo de 2 recursos por subdominio, sobre un máximo de 4 dominios simultáneamente. Haciendo esto el tiempo perdido estableciendo las conexiones TCP y empezando a descargar los recursos será de 0.8 s (400 ms para establecer las primeras 8 conexiones + 400 ms para las 2 últimas conexiones).

Hay una forma aún más sencilla de reducir este tiempo de espera. Si el servidor no cierra la conexión TCP con el cliente en cada petición HTTP, usando KeepAlives, dividimos por dos el efecto de la latencia en cada recurso que se descargue en una conexión ya abierta. El efecto de KeepAlives depende del número de subdominios que tengamos, si lo servimos todo desde el mismo dominio, en lugar de los 2 s anteriores tardaremos 400 ms para establecer las 2 primeras conexiones en paralelo + 200 ms por cada uno de los otros 8 recursos a bajar / 2 (número de recursos que se descargan en paralelo) = 400 + 200 * 4 = 1.2 s. Sobre 4 subdominios tendremos una espera usando KeepAlives de 0.6 s, ya que los 2 últimos recursos se descargan sin necesidad de establecer otra vez la conexión TCP.

Hay que tener en cuenta que en algunos servidores web la activación de KeepAlives tiene un impacto sobre el rendimiento muy significativo. Os recomiendo utilizar un servidor web alternativo, como lighttpd, nginx o cherokee al menos para vuestro contenido estático, con KeepAlives activado.

Un cambio que puede tener un efecto significativo en el tiempo de descarga es concatenar todos los ficheros CSS a descargar en un solo fichero (o 2, uno general al sitio y otro particular a la página), concatenar los ficheros Javascript, e incluso concatenar las imágenes, seleccionando la subimagen a mostrar usando CSS Sprites. Así podremos reducir significativamente el número de recursos a descargar.

Otro de los factores que sorprendentemente puede influir en el tiempo de descarga es el tamaño de las peticiones que hace el navegador. Cuando el navegador le pide al servidor una página, le envía también información extra, como el nombre del navegador, el idioma preferido por el usuario, las cookies, etc. Muchos de estos datos (como las cookies) no suelen influir en el resultado devuelto por el servidor para los ficheros CSS, JS o imágenes, y si nuestras cookies son cross-subdomain podemos evitar que el navegador tenga que mandarlas si colocamos los ficheros estáticos en otro dominio. Yahoo! por ejemplo usa el dominio yimg.com para servir este tipo de ficheros. A pesar de que las cookies son relativamente pequeñas, hemos de recordar que típicamente los usuarios navegan con conexiones asimétricas, donde la velocidad de subida puede ser 50 veces inferior a la velocidad de bajada de datos.

Una vez aplicados estos cambios en nuestras páginas, podremos concentrarnos en la segunda parte de mayor influencia en la velocidad, el tamaño de los recursos a descargar, y las distintas formas en que podemos influenciar la caché del navegador para evitar tener que descargar todo cada vez que un usuario llegue a nuestra página. Pero esa es otra historia de la que hablaremos otro día.

Thursday, August 18, 2005

Re: Ctrl+N in IE

Havoc comments on Internet Explorer showing the same page you're already viewing when you open a new window:

I'm pretty sure this is the only page on the Internet that I could not possibly want to see in the new window, since I already had it open.


Havoc is a smart guy, and usually he's right in the money, but he is soooo wrong on this one.

First, it's not always the case that you do not want to see the same page on the new window. You may want to check the bottom of the page to have a window with the footnotes, but still retain a window on the current reading position. But granted it's extremelly rare.

The thing that is invaluable is having the whole history of previous / next links on the new window. When you create a new window it's usually just because you want to fork your lecture. You have found two interesting links to follow on the same page, for instance. In no way is one of the windows more important than the other one to the user. Both are part of the current lecture “tree”.

All the Mozilla derivated I've seen handles new windows (or tabs) from a programmer point of view. They are just new, virgin, history-less windows. A pity.