Archivo
Android, iOS, tiempos de respuestas y por qué nada es gratis en sistemas informáticos
Hace unas pocas horas escribí esta respuesta sobre Por qué iOS es más fluido que Android (con buen criterio, eliminaron la entrada). Obviamente, por cuestiones de longitud y la “respuesta rápida” que requiere un comentario, no me quedó todo lo completo que requiere el tema. Lo que me gustaría explicar daría para muchas horas de charlas. De hecho, enseño estos temas en mi asignatura de Sistemas Operativos (II), dedico al menos unas 12 hs de clase, y aún así no entramos en muchos detalles importantes. Pero intentaré resumirlo en este apunte, fundamentalmente para que se entiendan los problemas de arquitectura, y de cómo toda decisión que se tome en una arquitectura, lenguaje o programa tiene implicaciones positivas y negativas, siempre.
Almacenar y leer de base de datos propiedades arbitrarias de objetos de forma transparente
Voy a comentar un truco muy sencillo, pero que no recuerdo haberlo visto implementado (si no lo provee ya el framework que se use), a pesar de lo que simplifica la programación a la hora de almacenar en base de datos relacionales propiedades arbitrarias de un objeto. El siguiente es parte del código de profile.php de Menéame que almacena la “bio” de un usuario.
if(!empty($_POST['bio']) || $user->bio) {
...
if ($bio != $user->bio) $user->bio = $bio;
}
Este otro de libs/user.php, y almacena en la base de datos las actualizaciones del karma de un usuario (y su hora de modificación).
if (! empty($log) && mb_strlen($log) > 5) {
$this->karma_log .= "$log: $inc, " . _('nuevo karma') . ": $this->karma\n";
$this->karma_calculated = time();
}
Nota: “.=” es una concatenación, implica dos operaciones, “get” y luego “set”.
Las propiedades bio, karma_log y karma_calculated se almacenan en la base de datos, pero no están en la base de datos de usuarios como las demás. Como no son parte del fastpath (no se consultan ni visualizan en las páginas más vistas) estos datos se almacenan por separado, en la tabla genérica annotations.
Sin embargo, a la hora de acceder o almacenar esos datos, no tenemos que preocuparnos de leer e insertar en la base de datos, se hace de forma “automática”. Explico cómo hacerlo, es un buen patrón cuando se requieran almacenar y acceder a propiedades arbitrarias de un objeto. Además sirve para cualquier lenguaje orientado a objetos.
Optimizando obsesivamente las consultas al MySQL
Para ir al grano y que no perdáis el tiempo los que no estéis interesados en esta frikada de optimización de consultas a base de datos [MySQL]: el código siguiente es el código SQL que selecciona y obtiene los datos de los enlaces que aparece en la portada del Menéame.
SELECT link_id AS id, link_author AS author, link_blog AS blog, /* muchas columnas adicionales del objeto */ FROM links INNER JOIN users ON (user_id = link_author) LEFT JOIN (categories AS cat, categories AS meta) ON (cat.category_id = links.link_category AND meta.category_id = cat.category_parent) LEFT JOIN votes ON (link_date > @enabled_votes AND vote_type='links' AND vote_link_id = links.link_id AND vote_user_id = @user_id AND ( @user_id > 0 OR vote_ip_int = @ip_int ) ) LEFT JOIN favorites ON (@user_id > 0 AND favorite_user_id = @user_id and favorite_type = 'link' AND favorite_link_id = links.link_id) LEFT JOIN link_clicks AS clicks ON (clicks.id = links.link_id) INNER JOIN (SELECT link_id FROM links $from WHERE $where $order_by LIMIT $offset,$page_size) AS id USING (link_id)
Nota: Las variables que comienzan con $, como $from o $where son variables asignadas desde el PHP para definir las condiciones (publicadas o pendientes, ordenadas por fecha o por karma, etc). Las que comienzan con @, como @user_id o @ip_int, son variables de sesión del MySQL.
Un podcast para sysadmins
Hay personas de tu “quinta” a las que conoces hace más de una década por su actividad en Internet y el software libre. Algunas de ellas son personas que casi nadie conoce, salvo un grupo de frikis informáticos (o fotógrafos) que le tienen un respeto casi reverencial. Algunas de ellas las aprecias como amigos de verdad aunque nunca tengas contacto directo, y sólo algún que otro comentario cruzado en algún foro.
Así pueden pasar años sin saber nada de esa persona, y un día, estando en apuros, esa persona te llama para apoyarte, ofrecer ayuda y darte información importante. Así, con el paso de los años descubres que el “mundo virtual” sí crea amistades reales, aunque nunca has podido desvirtualizarlas. Es eso lo que me pasó con David Hernandez, más conocido como Dabo.
Hace una semana por fin pude hacer algo, una tontería, por él (aunque no sé si es al revés). Dabo me pidió hacer un podcast sobre temas para sysadmins donde contase mi experiencia con Menéame, Amazon AWS, MySQL, servidores webs, etc. etc. Por supuesto le dije que sí. Dabo se preparó un enorme guión, lo trabajamos, nos enviamos decenas de correos, grabamos un día (más de tres horas), decenas más de correos, se tomó muchísimo curro para revisar la grabación, editarla y generar un índice de contenidos.
Finalmente acaba de publicarlo, un enorme podcast de 2 hs y 37 min: Kernel Panic, especial SysAdmin.
Yo no puedo escuchar mi voz, me descompone físicamente, así que no sé si es “digerible”, tampoco soy un gran sysadmin –lo hago porque es inevitable, aunque también tiene su punto de diversión y desafío– pero espero que sea útil. Para mi no fue ningún esfuerzo: hice lo que me divierte, hablar de lo que hago. El enorme trabajo es de Dabo.
Disfrutadlo, padecedlo, o pasad de él. Pero no echéis la culpas a Dabo, hizo lo que pudo
Piensa un poco en los lectores de tu sitio web: optimiza lo fundamental, intercala y elimina
¿Cansado de esperar más de 5 segundos para que se visualice la página de un blog? ¿agobiado de esos sitios con decenas de widgets, gadgets, efectos AJAX y mashups que tardan horas en cargarse? ¿amargado porque has desarrollado un programa muy eficiente para el último framework de moda y “va lento”? Yo también, y me molesta mucho, considero que son chapuzas increíbles que no tienen en cuenta lo básico de la usabilidad e interfaces humanas: el tiempo de respuesta que percibe el usuario.
De Menéame nos podrán criticar todo, salvo que sea lento o que no hayamos tenido en cuenta este aspecto tan importante, por eso cuento unas pocas reglas que hemos seguido muy estrictamente. Algunas las conocíamos, otras hemos aprendido durante estos casi cinco años de desarrollo.
Hay muchos parámetros a tener en cuenta para desarrollar sitios web que sean “rápidos”. No sólo hay que tener en cuenta la velocidad de los servidores, o el tiempo que el servidor en generar un HTML dinámico, hay muchos otros parámetros que afectan directamente al navegador y la percepción de los usuarios.
En julio de 2001 escribí un artículo en Bulma donde expliqué, basado en la experiencia y mediciones durante el desarrollo de los primeros sitios de Diari de Balears y Última Hora (en los años 1997-1998), los parámetros o mediciones técnicas fundamentales a tener en cuenta: tiempo de respuesta, tiempo de retorno, tiempo de descarga y el “tiempo para la visualización”. Este último parámetro, el de visualización, es el que más efecto tiene para el usuario. Éste espera una respuesta rápida, esa “respuesta” se suele percibir por el tiempo que tarda en empezar mostrarse la página en el navegador.
En el artículo comento con ejemplos como se puede hacer que el tiempo de visualización sea mucho menor que el tiempo total necesario para generar y descargar el HTML completo mediante una maquetación optimizada. Así, una página maquetada sin tener en cuenta estos parámetros toma, de acuerdo con las mediciones de esos años, seis segundos para ser visualizadas.
Tvis = Tdescarga = 0.5 seg + 40 KB/7.5KBseg = 0.5 seg + 5.33 seg =~ 6 segundos
En cambio la misma página, pero formateada adecuadamente puede reducir el tiempo de visualización a un segundo.
Tvis = 0.5 seg + 4 KB/7.5KBseg = 0.5 seg + 0.53 seg =~ 1 segundo
Esas mediciones se hicieron cuando reinaban las tablas, casi no se usaba CSS ni Javascript. Aún así las cosas han empeorado, incluso con el espectacular aumento de ancho de banda (lo mejor que había en 2001 era el RDSI a 64 kbps y muy pocas conexiones de 128 o 256 kbps), navegadores y potencia de cálculo hay muchos sitios populares que tardan más de seis segundos en visualizar la página.
Los problemas siguen siendo los mismos, aunque más complejos por esa influencia de los CSS, Javascripts, publicidad, librerías, efectos etc. Sería largo de explicar todas las recetas, pero aquí van algunas de las reglas que seguimos estrictamente en Menéame:
1. Minimizar las inclusión de CSS y Javascript
Los CSS y la mayoría de ficheros de Javascript se indican al principio del HTML y son bloqueantes, i.e. hacen que el navegador detenga todo el proceso de “dibujado” hasta que se hayan bajados todos los ficheros. Piensa mucho bien antes de incluir alegremente tantos ficheros CSS y Javascript en tu página, especialmente estos últimos. No es gratuito, todo lo contrario, estarás molestando mucho a tus lectores o usuarios. La regla fundamental es: minimiza el número de CSS y Javascript, no incluyas librerías que no necesitarás, minimiza el número de imágenes dentro del CSS (que generan conexiones adicionales).
Es muy común que blogs y sitios webs incluyan todo tipo de javascript externo (los widgets) sin tener en cuenta la penalización temporal que ejercen sobre su página. Estos javascripts bloquean completamente y se suele notar, especialmente si no fueron desarrollados cuidadosamente para evitar que el bloqueo sea prologando. Los buenos scripts suelen ser un pequeño código que sólo define un iframe de tamaño fijo y hace que éste cargue a posteriori el resto de javascript (con las consultas necesarias). Un ejemplo de buenos scripts en este sentido son los de Google AdSense.
Así que si no tienes más remedio que incluir javascript externo fíjate cómo está desarrollado, descarta aquellos que bloquean el dibujado de la página mientras hacen sus consultas a servidores externos. Si no te queda más remedio que utilizar esos script bloqueantes y lentos, tómate el trabajo para evitar el problema. Es lo que hicimos en el Menéame en los dos bloques de publicidad que se ven.
La publicidad se genera a partir de ficheros javascript del servidor de Social Media S.L. Este servidor verifica primero si hay alguna publicidad contratada para nuestro sitio de sus propias bases de datos y de bases de datos de terceros. Si no es así redirecciona para que se carguen los anuncios de AdSense. Todo este proceso es bastante lento y el API del servidor no está optimizado. La solución que usamos y se puede aplicar para cualquier sitio con un problema similar:
No se carga directamente al Javascript, sino que definimos un iframe de tamaño fijo y un html independiente que se carga de nuestros servidores. El html de ese iframe es el que incluye los javascript de publicidad. La carga del contenido del iframe no bloquea la página y tiene menos prioridad. Esta es la razón de que si váis al Menéame la publicidad se suele muestra después que se haya dibujado la página completa (probadlo y veréis el efecto).
Ya sabéis el truco que hay que hacer para solucionar esos widgets lentos, tómate el trabajo, tus lectores te lo agradecerán. Pero aún más importante: si eres desarrollador de widgets o plugins haz lo mismo en tu scripts. Aquí tienes un ejemplo de cómo lo hacemos para el “contador de votos” de Menéame check_url.js o este otro similar pero más elaborado.
Nota: está el atributo defer de la etiqueta script, pero usadlo con cuidado, tuve muchos problemas cuando lo intenté usar.
2. Retrasar la carga de Javascript que no sea necesaria para mostrar la página
Como comentaba en el punto anterior, hay que intentar minimizar el número de ficheros javascript que se incluyen al principio de la página. Cuando hay que cargar ficheros adicionales lo mejor es ponerlos al final del html, justo antes de la etiqueta </body>.
A veces no es simple porque esos javascript se pueden necesitar para mostrar contenido en la misma página. Eso se puede solucionar, como hacemos en el caso de noticias que tienen geolocalización y el mapa de Google Maps se muestra en la barra lateral, por ejemplo en esta noticia. La intención es retrasar lo máximo posible la carga del Javascript adicional de Menéame y el de Google Maps.
Podéis ver que el código que lo hace está al final del html, justo antes del </body> (el primer script incluye el API de Google, el segundo las funciones propias de Menéame):
<script type="text/javascript" src="http://maps.google.com/maps?file=api&v=2&key=ABQIAAAAocztgtKfY7lNdoKmmVwCrRTm04PVUmrAy_OEGIhbf1bFTbg4wRQr-dfRhZoi0UaKDoqFRXUZXOgLuw"> </script> <script type="text/javascript" src="http://aws.mnmstatic.net/js/geo.js"> </script>
Al principio del HTML insertamos una llamada a geo_coder_load (definida en geo.js que se cargará al final) pero que se llamará después que se haya cargado todo el contenido usando la función $.() de Jquery:
<script type="text/javascript">
$(function(){geo_coder_load(34.0104402,-118.4983353, 5, 'published');})
</script>
En el lugar donde queremos que se inserte el mapa sólo definimos un div con las medidas deseadas:
<div id="map" style="width:300px;height:200px;margin-bottom:25px;"> </div>
Lo que pasará es que la página se cargará y dibujará completamente, sólo entonces se llamará a la función geo_coder_load que llamará a las funciones del API de Google Maps para que dibuje el map en el div con id “map”. Así evitamos que se bloquee la página mientras se carga el “inmenso” mapas (por eso, al igual que la publicidad, el mapa se muestra con “retraso”).
3. Maquetar de forma que se pueda “dibujar” parte de la página aunque no se haya bajado todo el HTML
En el artículo de Bulma de hace casi diez años comentaba el mismo tema, aunque orientado más a tablas los principios son los mismos:
- Separar en módulos que puedan ser dibujados inmediatamente, por ejemplo la cabecera.
- Definir todas las dimensiones de imágenes, áreas (por ejemplo el alto de la cabecera, el ancho de las columnas, etc.) y espacios:
En Menéame el ancho de completo ya se define en el CSS:
#wrap {
min-width: 952px;
max-width: 1200px;
margin: 0 auto;
...
}
#header{
height: 36px;
...
}
#naviwrap li{
width: 120px;
height: 20px;
..
}
#sidebar {
float: right;
width: 300px;
...
}
En cuanto el navegador interpreta el CSS de esas tres divs ya puede calcular las dimensiones necesarias para poder dibujar rápidamente: el ancho que tendrá la parte visible, el alto de la cabecera superior, el alto del menú de la izquierda y el ancho de la columna de la derecha (y por lo tanto ya calcula el ancho de la columna principal de contenido).
La parte que toma más tiempo de CPU en la portada o una noticia de Menéame es la consulta a los enlaces o los comentarios. Para disminuir aún más el tiempo de visualización lo primero que se envía al navegador es el contenido de la columna derecha (el #sidebar), como el ancho ya está especificado puede mostrarla aunque no se haya recibido el HTML de la columna principal. Por eso en conexiones u ordenadores lentos veréis que se empieza dibujando por la columna de la derecha.
4. Utilizar dominios diferentes para el contenido estático
Los navegadores (o al menos la mayoría) paralelizan las descargas de sitios diferentes al original, por eso es mejor que dichos ficheros se bajen de un dominio diferente.
<link rel="stylesheet" type="text/css" media="all" href="http://aws.mnmstatic.net/css/es/mnm65.css"/> ... <link rel="shortcut icon" href="http://aws.mnmstatic.net/img/favicons/favicon4.ico" type="image/x-icon"/> ... <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js" type="text/javascript"></script> ... <meta name="thumbnail_url" content="http://aws.mnmstatic.net/img/mnm/eli_thumbnail.png"/> ... <img src="http://aws.mnmstatic.net/img/common/search-left-04.png" width="6" height="22" alt=""/> ... <img src="http://aws.mnmstatic.net/cache/00/01/1-1280021590-20.jpg" width="20" height="20" alt="gallir"/>
La línea 3 es interesante. Hay librerías, como las JQuery, que son usadas por muchísimos sitios. Aunque se trata siempre del mismo código cada sitio incluye su propio fichero, lo que obliga al navegador a bajar una y otra vez lo mismo. Para evitarlo Google puso a disposición el servidor Google Apis, donde mantiene una copia de esas librerías para que puedan ser usadas por cualquier sitio. Si todos los sitios importantes los usasen se ahorraría mucho tiempo de los usuarios (además los servidores de Google suelen estar distribuidos y se bajan de sitios con la menor demora).
5. Usar dominios diferentes, no subdominios, para contenido estático
Esta apartado podría ser un subapartado del anterior, incluso el mismo, pero lo puse por separado ya que es un truco muy bueno para sitios complejos (con uso de cookies) y muchas imágenes o ficheros estáticos.
En el ejemplo anterior se puede ver que no usamos un subdominio de meneame.net (por ejemplo static.meneame.net) para servir el contenido estático, sino uno completamente diferente. El objetivo evitar agregar tráfico adicional por los cookies que se usan.
Cada cookie que se define en un dominio implica que por cada conexión el navegador envíe al servidor el valor de esos cookies. Esto pasa incluso para bajarse unas imágenes pequeñas de pocos bytes. Además el ancho de banda de subida suele ser bastante inferior al de bajada. La situación se agrava aún más con los cookies que definen las herramientas de estadísticas como Google Analytics.
La forma de evitar ese tráfico inútil para el contenido estático, lo mejor es usar un dominio completamente diferente. Cuando implementamos esto en Menéame hicimos las pruebas y ahorrábamos 14 KB de tráfico para un navegador con la cache vacía.
6. Definir tiempo de caducidad para el contenido estático
Para evitar que los navegadores se conecten cada vez para verificar si un fichero estático fue modificado es mejor definirles un tiempo de caducidad bastante elevado. De esta forma el navegador no volverá a verificar ese fichero hasta que haya pasado ese tiempo.
En menéame usamos 10 días, en el nginx:
location ~* \.(gif|jpg|png|js|css|html|ico)$ {
expires 10d;
...
}
Por ejemplo para una imagen generará las siguientes cabeceras HTTP:
Cache-Control: max-age=864000 Expires: Mon, 06 Sep 2010 16:12:01 GMT
7. Comprimir el contenido de texto
Si un sitio está razonablemente bien programado, lo que más tiempo consume es la transmisión del HTML por la red, por eso los navegadores aceptan la versión comprimida. Con la potencia actual de los procesadores es mejor comprirlos antes de enviarlos, los servidores pueden hacerlo automáticamente y on-the-fly si el navegador lo acepta.
Por ejemplo la configuración del nginx para Menéame para comprimir en gzip:
gzip on;
gzip_comp_level 4;
gzip_proxied any;
gzip_min_length 512;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
gzip_http_version 1.0;
gzip_vary on;
gzip_types text/css text/xml application/x-javascript application/atom+xml text/plain application/json application/xml+rss text/javascript;
Hay más reglas y trucos para minimizar los tiempos, por ejemplo poner todas las imágenes del CSS en una sola y tratarlas como “sprites” [*], pero si respetas la mayoría de estas 7 reglas la mejora de velocidad de tu sitio será muy notable.
[*] Particularmente no me gusta esta opción para sitios donde el diseño es dinámico con el Menéame. Cada vez que se agregan o modifican iconos obliga a generar una nueva imagen, que además debe ser compatible en sus coordenadas con la anterior, caso contrario hay que redefinirlas en todo el CSS. Es mucho tabajo de “administración” y sujeta a errores. Pero sí la veo como buena medida para los plugins o widgets que incluyen iconos y serán usados por muchos sitios (por lo que la “estabilidad” es mucho más prolongada). Más o menos lo mismo pienso sobre minimizar los javascripts.
¡No necesitas servidores más rápidos! debes intercalar
Esta sección es más friki-informática, y es la que en realidad me motivó a escribir este apunte. Pero por esas cosas de “completitud” quedó última y la más breve.
La tendencia actual de programación web es de usar plantillas cada vez más sofisticadas y complejas. Ejemplos típicos son la de generar todas las consultas a la base de datos y luego generar los resultados HTML en un bucle FOR dentro de la plantilla. Ejemplos más sofisticados son la de incluir plantillas dentro de otras o usar las características de herencia (o bloques). Algo similar ocurre con casos más sencillos, como usar el output buffer del PHP.
La mitología informática dice que así se mejora la negociación TCP/IP y se minimizan los paquetes enviados. Posiblemente sea verdad para sitios muy sencillos por con contenido tan simple que se genera muy rápido. Pero la realidad es que los sitios actuales generan páginas de varias decenas de KB (el HTML de la portada de Menéame tiene una media de unos 50-60 KB, una noticia con muchos comentarios puede llegar fácilmente a los 200 KB).
Hace pocas semanas liberamos Haanga, el sistema de plantillas de Menéame. Aunque éste permite la herencia no usamos esa características. Ni usamos el output buffer de PHP. Tampoco usamos los grandes bucles FOR, sino que el contenido se genera progresivamente para cada noticia, comentario o nota. Esta generación progresiva del HTML se usa para todos las partes de la página, para la cabecera, el pie, etc.
La razón es muy sencilla, así reducimos el tiempo de respuesta, el de retorno… y si se respetan las reglas comentadas anterioremente, el de visualización.
Un programa web tiene varias partes bien diferentes: consultas a la base de datos, lógica, generación de HTML, envío por la red. Para simplificar podemos considerar:
B: Consultas a base de datos y lógica:
H: Generación de HTML.
E: Envío a través de la red.
A: Ficheros adicionales (CSS, Javascript) que necesita bajar el navegador.
Así un patrón típico cuando se usa buffer del PHP, o se genera el HTML vía un template al final de la lógica sería (en azúl y sólo como referencia el momento en el que el navegador puede empezar a mostrar contenido):
BBBBBBBBBBHHHHHEEAAAEEEEEEEEEEEEEEEEEE
En la figura tenemos:
- diez “unidades de tiempo” (llamémosles “tics”) para la lógica y consultas a la base de datos,
- cinco tics para la generación del HTML,
- veinte tics para la compresión (si está habilitada) y descarga por la red y
- tres tics para descargar ficheros adicionales.
Por supuesto es sólo un dibujo aproximado, si se usan plantillas más complicadas la generación de HTML podría tomar más tiempo que la lógica y consulta de base de datos. O viceversa si el HTML es simple pero las consultas son complicadas.
En el ejemplo el tiempo total que toma para bajar todo el HTML es de 38 tics, con suerte el usuario podrá empezar a visualizar la página web a los 20 tics.
Si el web es muy lento, la mayoría de programadores decidirán que lo mejor es poner un servidor de base de datos o web que sea más rápido, o que permita disminuir la carga y por lo tanto se reducirían los tiempos de base de datos y generación de HTML. Pero no es tan fácil, sobre todo porque la conexión al servidor de base de datos involucra –al menos para sitios grandes– conexiones vía red. Aunque se aumente la velocidad del procesador, el tiempo de latencia de la red sigue siendo el mismo.
Aún así, supongamos que cambia por servidores y redes más potentes y logra reducir casi a la mitad [*] el tiempo de base de datos y generación de HTML:
BBBBBHHHEEAAAEEEEEEEEEEEEEEEEEE
Después de la inversión para conseguir semejante mejor y el trabajo de migrar a una nueva infraestructura habrá reducido el tiempo total a 31 tics (18 % de mejora) y el tiempo de visualización a 13 tics (35% de mejora).
[*] Habitualmente la programación web es de un sólo “hilo”, por lo que poner más procesadores o con más núcleos no acelera una ejecución. La única solución es aumentar los MHz, pero doblar los MHz tampoco implica que se reduzca el tiempo a la mitad (ni mucho menos) por lo que el coste para lograrlo suele ser muy elevado.
Pero este programador se olvidó de algo importante que aprendió en sus estudios y que es fundamental es informática, sistemas operativos y multiprogramación en general: la intercalación.
Si no la hubiese tenido en cuenta se habría dado cuenta que lo que tocaba era “adelantar” la generación del HTML para hacerlo progresivo y empezar la compresión y el envío mientras se hacen las demás consultas a la base de datos. Así el dibujo original, con los “servidores más lentos” le hubiese quedado algo como lo siguiente:
EEBBBBBBBBBBHHHHH
AAAEEEEEEEEEEEEEEEEEE
En se empieza a enviar inmediatamente el contenido que es inmediato o necesita muy pocas consultas (como la cabecera y la barra lateral del Méneame) para que el navegador pueda empezar a interpretarlo. Con estos números el tiempo total es de 23 tics (40% de mejora) y 5 tics para la visualización (75% de mejora). Los resultados son mucho más espectaculares sin cambiar nada de equipamiento, sólo “adelantando” la evaluación de los templates (y/o deshabilitando el output buffer).
Por supuesto esta “espectacular” mejora depende mucho de la aplicación. Menéame es un caso más o menos extremo en sus tiempos, la lógica y consultas a la base de datos están muy optimizadas y por lo tanto consumen muy poco tiempo relativo. Los siguientes son tiempos resultantes de 100 mediciones sobre los servidores en producción a una hora de mucho tráfico (hoy viernes entre las 12:00 a 12:30 hs)
- La lógica más base de datos (B): media 0.03348 seg (desviación estándar 0.02155)
- Lógica más base de datos (B) + generación HTML (H): 0.04663 seg (desviación estándar 0.02903)
- De #1 y #2 se obtiene que la media de H es: 0.01315 seg
Como daría mucho trabajo rehacer los programas y plantillas hice la prueba habilitando y deshabilitando el buffering y la compresión. La siguiente tabla muestra el tiempo total medio (en segundos, contando el establecimiento de una nueva conexión TCP/IP) que tarda en bajar el HTML de la portada (14 KB comprimidos, 57 KB sin comprimir, aproximadamente):
| Sin compresión | Con compresión | |||
| Sin buffer | Con buffer | Sin buffer | Con buffer | |
| ONO 12 Mbps | 0.52 | 0.61 | 0.32 | 0.34 |
| 3G Vodafone | 1.09 | 1.12 | 0.86 | 0.89 |
Se puede observar que sin buffering es algo más rápido pero la diferencia es mínima. Sí se nota la diferencia con la compresión habilitada, se llega hasta el 38% de reducción del tiempo.
Conclusiones
No uses el buffering de salida si no estás seguro de lo que haces y/o has hecho mediciones de tiempo. En general irá mejor sin buffer, y consumirá menos recursos del servidor (fundamentalmente memoria RAM).
Si tu aplicación con plantillas es compleja y el tiempo de procesamiento y consultas a la base de datos es relativamente grande, serializa, no generes todo a partir de un sólo template que llamas al final. Además ahorrarás memoria RAM y ciclos de CPU. En caso que no puedas serializar la generación de cada “objeto”, al menos genera una plantilla independiente con la cabecera HTML y envíala lo antes posible, así el navegador podrá bajar en paralelo los CSS y javascripts.
Nota final sobre los dispositivos móviles e iPads
Ten en cuenta que muchos usuarios usan sus móviles o iPads para navegar, haz que sean más felices con sus cacharros cuando naveguen por tu sitio, que para eso se gastaron un pastón. Hay algunos trucos muy fáciles, como no mostrar contenido secundario, no cargar APIs “pesados”, y usar el selector @media del CSS para ajustar tamaños, márgenes o no mostrar secciones completas.
Menéame tiene su sitio optimizado para móviles desde hace tiempo. Pocos lo conocían, así que hemos implementado la redirección automática al sitio de móviles si se detecta que el cliente es de móviles (teníamos más de 10.000 visitas diarias). Mucha gente se quejaba de esta redirección, así que la restringimos a que sólo se haga si se accede directamente a una noticia desde un sitio externo, o si se usa el enlace corto tipo http://m.menea.me/m5za (lo usamos por ejemplo en los envíos automáticos a Twitter).
Para aligerar la navegación por el sitio principal a esos usuarios hemos hecho varias cosas:
- No se genera el contenido de la barra lateral derecha (ni la publicidad superior).
- Se cambia el tamaño de la fuente, algunos márgenes y marca en el CSS como “oculta” (display: none) aprovechando el selector @media de los CSS:
/* Definitions for mobile, iPad and TVs */
@media print,tv,handheld,all and (max-device-width: 780px) {
body {
font-size: medium;
}
.sneaker {
font-size: small;
}
#wrap {
min-width: 320px;
}
#sidebar {
display:none;
}
#singlewrap, #newswrap {
margin: 5px 5px 0px 5px;
}
#footwrap {
margin: 5em 5px 0 5px;
clear: both;
}
.banner-top {
width: 470px;
}
}
- No se muestran los mapas ni se carga el API de Google Maps.
- No se muestra el megabanner sino un anuncio de AdSense más pequeño.
Haanga vs Django vs H2O
En mi apunte anterior conté que habíamos desarrollado y liberado Haanga, el sistema de plantillas de Menéame. Ahora acabo de hacer unos benchmarks comparándolo con Django y H2O. La intención fue medir el rendimiento del motor de plantillas de cada uno.
Los resultados se muestran en la tabla a continuación. La primera columna es el tipo de benchmark. La segunda, tercera y cuarta son el número de conexiones por segundo (más altas son mejores) de cada uno de los sistemas. Las dos últimas columnas indican cuantas veces más conexiones por segundo sirvió Haanga.
| Bench | Haanga | Django | H2O | vs Django | vs H2O |
| b1 (sin plantillas) | 3214.84 | 769.48 | 1447.68 | 4.2 | 2.2 |
| b1_tpl | 1031.57 | 203.28 | 273.24 | 5.1 | 3.8 |
| b2_tpl | 1944.41 | 270.96 | 240.55 | 7.2 | 8.1 |
| b2_filters | 1011.10 | 65.14 | 156.68 | 15.5 | 6.5 |
| header | 2365.59 | 185.69 | 273.61 | 12.7 | 8.6 |
Haanga saca mucha diferencia a los dos sistemas en cualquiera de las pruebas.
Explicación de los benchmarks
- b1 sin plantillas: Este es un programa simple que sólo ejecuta un bucle para imprimir la hora cien veces. No se usan templates, pero sirve para dar una referencia y notar la diferencia entre la inclusión de librerías entre Haanga y H2O (en la vistas de Django ya se incluyen).
- b1_tpl: El mismo resultado que el anterior pero llamándose al generador de plantilla para cada iteración.
- b2_tpl: Este es un caso más habitual. En vez de llamar cien veces al generador se guardan los resultados en un array y al final se genera el resultado desde el template (con el {% for o in objects %}
- b2_filters: Es el mismo programa que el anterior pero al template se le agregaron un par de filtros (La hora ({{ o.i|escape }}) en epochs es: {{o.epoch}} ({{ o.epoch|date:”r” }}).
- header: Este es un caso más real. Tomé una de las plantillas más complejas que usamos en Menéame, la que genera la cabecera HTML, logo y menú superior (header.html). Eliminé las etiquetas no compatibles con los otros dos (los exec y trans), se llama tres veces al generador de plantillas (me aseguré que el HTML era similar en los tres –había diferencia en los espacios en blanco que genera cada uno–).
Los códigos fuentes completos lo podéis bajar en un tgz.
Ejecución
Todas las pruebas fueron hechas sobre un Intel Quad de 2.40GHz con 4GB de RAM. No había otros procesos consumiendo CPU.
Los números de conexiones por segundo fueron obtenidos con el Apache Benchmark contra el servidor local (localhost). El comando fue «ab -q -S -c 2 -t 30 $url», podéis ver el script benchs.sh en el tgz mencionado antes.
Los PHP se ejecutaron en un Apache2 estándar de Ubuntu 10.4, con el PHP5 también estándar, el xcache (con su configuración por defecto). El único cambio que hice fue en la configuración del PHP5 (php.ini) para que sólo avise de errores (i.e. «error_reporting = E_ERROR»).
El Apache con el mod_python me daba resultados muy malos con el Apache 2, así que consulté a Antoni Aloy (un expertísimo en Django) y me recomendó hacerlo con el gunicorn. Este es el comando que utilicé: «python manage.py run_gunicorn -w 5» (el DEBUG del Django estaba deshabilitado).
Actualización: hice las pruebas como me indicó @Rolando con el tornado y su sistema de templates. Estos son los resultados.
| Bench | Tornado |
| b1 (sin plantillas) | 2988.29 |
| b1_tpl | 1111.82 |
| b2_tpl | 1802.91 |
| b2_filters | 613.54 |
| header | 2425.91 |
Cambiamos de Apache y lighttpd a nginx
Cómo montamos Menéame en Amazon EC2
La primera vez que alquilamos servidores dedicados para Menéame fue en ThePlanet.com. Aunque funcionaba muy bien y no tuvimos problemas decidimos traerlos a España para mejorar el ping y apostar por hosting español. Así nos fuimos a Ferca, que nos atendieron muy bien y no tuvimos problemas hasta que fueron absorbidos por Acens. Allí cambió, a peor, con caidas frecuentes de la red, problemas con sus firewalls que bloqueaban conexiones, desaparición del servicio de emergencias, imposibilidad de ver nuestras estadísticas… y lo que colmó nuestra paciencia es que a pesar que nos cobraron religiosamente los meses de noviembre y diciembre no nos enviaron las facturas (aún esperamos respuesta).
El tema del hosting suele ser un dolor de cabeza, y si además necesitas una arquitectura sofisticada, o escalable debes sacar el talonario y prepararte a gastar dinero que desde mi punto de vista no tiene justificación. Como llevaba probando y “jugando” con Amazon EC2 desde hace tiempo decidí hacer pruebas. Después de horas de cálculadora y viendo que nos costaría sólo un poco más que Acens (pero menos que otras ofertas que recibimos) pero nos daría mucha más flexibilidad y fiabilidad decidimos hacer la migración.
La arquitectura básica es la que se muestra en la siguiente imagen (prohido reirse de mi dibujo a mano con la mejor voluntad y dedicación):
Actualización: Britxardo me envió un par de parches con los esquemas:
Tenemos dos servidores (instancias) permanentes, aws0 y aws1, y un número variable de instancias que sólo atienden a las peticiones web. Todos los servidores ejecutan Ubuntu Karmic, de las imágenes (AMI) oficiales de Ubuntu para EC2.
El kernel que se ejecuta en cada instancia debe ser compatible con el “anfitrión” de Amazon, Canonical tiene un convenio con Amazon para validar su kernel, que son mucho más modernos que otras distribuciones (estas imágenes –AMI– se encuentran en “Community AMIs” de EC2). También nos interesaba Ubuntu Karmic porque en la migración cambiaríamos el tipo de motor de la base de datos a MyISAM a InnoDB y la versión de InnoDB del MySQL 5.1 está muy mejorada con respecto a la 5.0 de la última Ubuntu LTS (08.04).
Servidor principal: aws0
Es el “servidor central”, del tamaño large (m1.large) de 64 bits, cuesta $ 0.38 por hora, con 7.5GB de RAM y además la imagen está montada y arranca de un EBS (Elastic Block Service, un “disco persistente” con mejor rendimiento). Además de las ventajas obvias de persistencia de datos permite que en cualquier momento detengamos la instancia y la volvamos a iniciar con otros parámetros, por ejemplo más CPU y memoria RAM.
Los servicios que da este servidor son:
- Servidor MySQL: Aprovechando la migración hemos pasado a usar InnoDB y usar las transacciones/commit donde más hacían falta.
- Servidor NIS y NFS: Todo el software del Menéame y las cuentas de usuario están exportadas por NFS que son usadas por aws1 y las instancias web. Para centralizar la gestión de cuentas usamos el NIS.
- Servidor principal DNS: Es el primario de la zona meneame.net y notame.net.
- Servidor memcache: Usado por todas las instancias web.
- Servidor SVN para la gestión del software.
- Servidor SSH
- Servidor SMTP: Recibe todos los correos desde las otras instancias. Debido a las restricciones de Amazon EC2 para el correo saliente, todos los correos lo enviamos vía AuthSMTP (más adelante lo explicamos)
Teníamos muchas dudas de cómo se iba a comportar con carga real a la base de datos, y estamos gratamente sorprendidos, durante el funcionamiento normal no supera el 50% de uso de la CPU como se puede ver en la imagen (los picos de madrugadas son por el cálculo del karma y otras tareas como generación de “summaries”)
Mysql
Es fundamental la configuración mínimamente correcta del servidor MySQL, en caso de InnoDB es muy crítico el tamaño del buffer, como tenemos un total de 7.5 GB le asignamos 5 GB de RAM.
[mysqld] default-character-set=utf8 server-id=1 set-variable=max_connections=1000 set-variable=query_cache_wlock_invalidate=True table_cache=1024 innodb_buffer_pool_size = 5G innodb_flush_method = O_DIRECT innodb_flush_log_at_trx_commit = 0 innodb_additional_mem_pool_size = 16M innodb_log_buffer_size = 8M innodb_file_per_table = True join_buffer_size = 1M sort_buffer_size = 1M read_buffer_size = 1M read_rnd_buffer_size = 2M thread_cache_size = 64 thread_concurrency = 8 query_cache_size = 96M query_cache_limit = 16K query_cache_type = 2 tmp_table_size = 64M max_heap_table_size = 64M wait_timeout = 60
Servidor secundario y de backups: aws1
Esta es una instancia m1.small, de 32 bits, cuesta $0.095 por hora, con 1.7 GB de RAM, un sistema raiz de 10 GB y otro adicional (en /mnt) de 150 GB (ninguno de los dos es persistente)
- Secundario DNS
- Réplica en tiempo real de la base de datos (slave)
- Sphinx para las búsquedas
Esta instancia ya la teníamos funcionando desde noviembre como secundario DNS, ahora simplemente la reaprovechamos para hacer más cosas. Las fundamental es la réplica en tiempo real de la base de datos, que nos sirve para hacer backups a discreción sin afectar al servicio web, también lo usamos para muchos scripts de control que son “pesados” en consultas (en MyISAM era imprescindible hacerlo así porque bloquea todos los updates, con el InnoDB ya no se nota, de todas formas seguimo usando el slave para estas tareas).
También estamos usando (no acabamos de configurar todo) el S3Sync para hacer backups diarios a S3. El S3Sync emula de alguna forma el tradicional rsync, pero para trabajar sobre el sistema de S3.
Réplicas con balanceo de carga y escalado automático
Decidimos poner todo el servicio web en un sistema que se autoescale automáticamente, no sólo por la cuestión de precios sino por la tranquilidad que en momentos de mucha carga no habrá problemas de saturación (como nos ocurrió durante la Eurocopa, que tuvimos que montar un servidor adicional en EC2, pero al hacer réplica en EEUU de un servidor en España no iba muy fino por las enormes latencias).
La decisión fue poner todos los servicios web en una misma imagen:
- Apache con PHP para HTTP y HTTPS (puerto 80 y 443)
- Lighttpd para los ficheros estáticos (puerto 81)
Desde hace tiempo usamos mnmstatic.net como dominio para los ficheros estáticos, esto mejora la carga desde el navegador y evita que el navegador envíe los cookies de meneame.net [*] cada vez que se baja una imagen, en las pruebas que habíamos hecho se ahorra hasta 14KB de tráfico de subida en cada página.
[*] No son sólo los nuestros, que son sólo dos, sino también todos los que definen vía javascript los medidores como Google Analytics, podéis verificarlo en vuestros navegadores.
Cada instancia al arrancarse monta por NFS el directorio donde tiene acceso a todo el software y las imágenes (estáticas y dinámicas). Esto se hace el el fstab, pero hay que tener cuidado con las opciones, fundamentalmente la opción bg para que que bloqueada al arranque, y otras para aumentar la eficiencia y tolerancia a errores.
# /etc/fstab: static file system information. proc /proc proc defaults 0 0 /dev/sda3 None swap defaults 0 0 /dev/sda1 / ext3 defaults 0 0 /dev/sda2 /mnt ext3 defaults 0 0 x.x.x.amazonaws.com:/home /home nfs rw,bg,noatime,soft,nolock,intr,async,nodev,nocto,ac,actimeo=15 0 0
Balanceo (Load Balancer)
Como tenemos que balancear del puerto 80 dos tráficos diferentes (al Apache y al Lighttpd) tuvimos que definir dos baleanceadores diferentes (web-balancer y static-balancer). Cada balanceador te da una dirección IP y nombre DNS. Uno de los balanceadores –web-balancer– redirecciona a los puertos 80 y 443 y el otro –static-balancer– redirecciona del puerto 80 al 81 del lighttpd.
Para cada una de las réplicas elegimos primero el tamaño pequeño (m1.small), pero el primer día de funcionamiento (el lunes) y a pesar que durante las fiestas el tráfico es bastante menor que lo habitual vimos que el AutoScaler creaba cinco instancias para poder atender a la demanda, y durante las horas de menor tráfico no bajaba de dos. Me soprendió bastante, no esperaba que el consumo del Apache fuese tan elevado.
Me puse a estudiar y comparar con el vmstat, ví que las estadísticas del monitor de EC2 (CloudWatch) no se correspondían con estas. Investigando más me di cuenta del problema, el vmstat –al menos con el kernel de Ubuntu– no toma en cuenta la “capacidad” real de CPU asignada sino la del procesador. La asignada es un 40% de la total de la CPU (fácil de comprobar, se pone un proceso que consuma el 100% de cpu –por ejemplo yes > /dev/null– y se mira la columna idle del vmstat, en este caso el idle indicaba 60%).
Entonces tenía que buscar una solución más barata, y encontré una solución muy buena, bonita, potente y barata, usar una instancia orientada a CPU para el Apache. EC2 ofrece este tipo de instancias, las High-CPU Instances. Con la c1.medium y por sólo el doble de precio ($0.19/hora) podía tener una máquina con la misma memoria y algo menos de prioridad de E/S (que es muy poca, sólo los logs del Apache) pero con 5 veces la capacidad de CPU.
Así que reconfiguré el AutoScaler para que cree las instancias del tipo c1.medium y problema solucionado: desde que lo pusimos en marcha no ha creado ninguna instancia adicional, nunca superó al 80% de CPU:
AutoScaler
La configuración del escalador automático es casi lo último que se hace, necesitas tener definido los balanceadores y la imagen (AMI) que se usará para arrancar las réplicas. A diferencia de lo anterior, que se puede crear y modificar con la consola web del Amazon AWS, todavía no existe esa opción para el AutoScaler (aunque seguro que la agregan en las próximas semanas).
Por ahora toca hacer con los scripts, se necesitan definir tres cosas que están mostradas en el código de abajo:
# Crea la configuración para lanzar las instancias as-create-launch-config web01cpu --image-id ami-ami-d3fad1a7 --instance-type m1.small --group default,www # Define el grupo de autoscale llamado web-group # Se "conecta" a los dos balanceadores # Después de tomar una decisión no hace nada durante 180 segundos as-create-auto-scaling-group web-group --launch-configuration web01cpu --availability-zones eu-west-1a --min-size 1 --max-size 6 --load-balancers web-balancer,static-balancer --cooldown 180 # Define los parámetros para crear o destruir # Crea cuando consume más del 80% de cpu durante dos minutos # Destruye cuando baja del 30% as-create-or-update-trigger web-trigger --auto-scaling-group web-group --namespace "AWS/EC2" --measure CPUUtilization --statistic Average --dimensions "AutoScalingGroupName=web-group" --period 60 --lower-threshold 30 --upper-threshold 80 --lower-breach-increment=-1 --upper-breach-increment 1 --breach-duration 120
Con todo lo comentado anteriormente, esta es la situación habitual de funcionamiento del Menéame:
Para mostrar el funcionamiento del escalador puse la CPU de la instancia web al 100% (ejecutando dos yes > /dev/null para poner al 100% cada una de las CPU) para obligar a crear otra instancia.
Aquí se puede ver como el trigger indica HighBreaching, es decir que se superó el límite superior
$ as-describe-triggers web-group --headers TRIGGER TRIGGER-NAME GROUP STATUS NAMESPACE MEASURE STATISTIC PERIOD TRIGGER web-trigger web-group HighBreaching AWS/EC2 CPUUtilization Average 60
Aquí se oberva como la “capacidad deseada” es de 2 y que inició la creación y arranque de una nueva instancia:
$ as-describe-auto-scaling-groups web-group --headers AUTO-SCALING-GROUP GROUP-NAME LAUNCH-CONFIG AVAILABILITY-ZONES LOAD-BALANCERS MIN-SIZE MAX-SIZE DESIRED-CAPACITY AUTO-SCALING-GROUP web-group web01cpu eu-west-1a static-balancer,web-balancer 1 6 2 INSTANCE INSTANCE-ID AUTO-SCALING-GROUP AVAILABILITY-ZONE STATE INSTANCE i-04ef2c73 web-group eu-west-1a InService INSTANCE i-4e7dbe39 web-group eu-west-1a Pending
Finalmente las dos instancias ya están activas y accesibles:
$ as-describe-auto-scaling-groups web-group --headers AUTO-SCALING-GROUP GROUP-NAME LAUNCH-CONFIG AVAILABILITY-ZONES LOAD-BALANCERS MIN-SIZE MAX-SIZE DESIRED-CAPACITY AUTO-SCALING-GROUP web-group web01cpu eu-west-1a static-balancer,web-balancer 1 6 2 INSTANCE INSTANCE-ID AUTO-SCALING-GROUP AVAILABILITY-ZONE STATE INSTANCE i-04ef2c73 web-group eu-west-1a InService INSTANCE i-4e7dbe39 web-group eu-west-1a InService
Así es como se ve en la consola:
Problemas encontrados
Tuvimos dos problemas fundamentales.
HTTP SSL detrás del balanceador
El primero es que las instancias sólo “ven” la IP privada del balanceador y envía una cabecera HTTP para indicar la original, esto no ocasionó gran problema porque el software del Menéame ya estaba preparado. Pero con el SSL (o HTTPS) la cosa es muy distinta, en estos caso la conexión entre el balanceador y la instancia no es vía HTTP, sino TCP (por razones obvias, la comunicación va cifrada) por lo que no se recibe nada de información de la IP del cliente.
Esto obligó a modificaciones y agregar campos de control en los formularios de registro y login vía el servidor seguro. No fue nada importante, pero me obligó a pensar bastante en una solución razonablemente segura (digo razonable porque seguro que se le pueden encontrar problemas si se le dedica tiempo).
Envío de correo por AuthSMTP
El segundo problema fue inesperado y el que nos retrasó la migración del día sábado al domingo. El viernes recibimos un email de Amazon indicándones que estábamos enviando correo saliente (nuestras pruebas de envío de validación de cuenta y recuperación de contraseña) y que nos iban a bloquear la salida. Además vimos que agregan automáticamente nuestras IPs al Spamhause.
Aunque te envían el URL de un formulario para pedir la autorización decían que hay que esperar por lo menos dos días hábiles (hoy nos enviaron un email diciendo que ya estudiaron el caso y nos autorizan, pero las IPs siguen apareciendo en Spamhause). Fue un momento de depresión, además ví que es un problema en Amazon por las medidas que tomaron para evitar spammers en su servicio.
Pero encontré una solución buena, bonita y barata: AuthSMTP.
Por un precio muy razonable ellos se encargan de enviar el correo de tus usuarios o todo el dominio. Funciona muy bien y muy rápido, te olvidas del problema que las IPs de tus servidores de correo aparezcas en esas demoníacas bases de datos de spammers.
En nuestro caso la configuración del Postfix de aws0 fue muy sencilla, sólo tuvimos que agregar las siguientes líneas al main.cf:
relayhost = mail.authsmtp.com:2525 smtp_connection_cache_destinations = mail.authsmtp.com smtp_sasl_auth_enable = yes smtp_sasl_password_maps = static:USUARIO:PASSWORD smtp_sasl_security_options = noanonymous default_destination_concurrency_limit = 4 soft_bounce = yes
Con esto se solucionó el problema más grave que encontramos y pudimos hacer la migración.
Costes mensuales
- 1 m1.large: $ 273
- 1 m1.small: $ 64.8
- 1, 2 instancias c1.medium web de media (máxima): $ 164.16
- 2 TB transferencia de datos: $ 340
- 1 EBS de 50GB almacenamiento + aproximadamente 50 Mops= $ 11
- 3 instancias medias medidas con Cloudwatch: $32.4
- 2 Balanceadores: $40.32 + $16 de 2 TB de tráfico, $56.32
Coste total estimado: $ 941.68, al cambio actual del euro queda en 649 € mensuales
Fin
Sí, se pueden encontrar ofertas más baratas, y es un poco más de lo que pagamos a Acens (pero mucho menos que otras ofertas que recibimos), pero te olvidas de estar atado por una arquitectura, de tener que hacer engorrosas y largas negociaciones si necesitas un nuevo servidor, de abrir averías por la red (en Amazon está todo monitorizado y es público, una caída afecta a muchos servidores, así que no tardan nada en resolver).
Y lo más importante, es el futuro, mucho más sofisticado, pero también más divertido (y creo que además más ecológico, el hardware está mucho mejor aprovechado) y como administrador de sistemas/programador/CTO tienes una libertad y flexibilidad impensable hace pocos años.
Como conclusión, por ahora muy positivo. Ya veremos como evoluciona, conociendo la historia de EC2 no dudo que agregarán cada vez más servicios y que bajarán los precios una vez al año.
PS: Son las cinco de la mañana, lo publico como está, disculpad los errores que haya, ya los corregiré luego.
El bug marciano
El día 4 de julio de 1997 el Mars Pathfinder aterriza en Marte y a las pocas horas empieza a enviar las fotos de Marte en alta calidad que todos conocemos. La misión hasta ese momento de calificó de éxito. Se despliega la nave que sirvió para el viaje y para el aterrizaje –el SpaceCraft/Lander (diagrama inferior)– para dejar salir al Sojounder Rover (la conocida imagen de la derecha).
A los pocos días se detectan continuos reinicios del ordenador del Lander cuando intentaba enviar datos metereológicos y científicos a la Tierra. Los reinicios del ordenador estaban ocasionados por una tarea (bc_sched) que verifica si otras han acabado, de no ser así reinicia todo el software (sin pérdidas de los datos obtenidos, que se mantienen en la RAM).
El hardware
El ordenador de la nave era un Power1/RS6000 de IBM, conectada a un bus VME con interfaces para la cámara, la radio y un bus 1553. El bus 1553 tenía dos partes, una usada para navegación espacial (aceleradores, válvulas, sensor solar y scanner de estrellas) y otra para el aterrizaje con interfaz para el acelerómetro, radar de altitud y lo sinstrumentos meteorológicos-científicos: el ASI/MET. El hardware del 1553 fue heredado de la sonda Cassini y tenía un modo de funcionamiento simple y obligatorio, el software controlador y toma de datos debía planificarse exactamente cada 0.125 segundos (u 8 Hz).
El software
El sistema operativo usado era el VxWorks, adaptado específicamente al procesador RS600. VxWorks es un Unix de tiempo real desarrollado por Wind River y comprada completamente por Intel hace pocos días (el 17 de julio). La arquitectura del software era la siguiente:
- bc_sched: Era la tarea con máxima prioridad y se encargaba de preparar las “transacciones” para el siguiente ciclo de 0.125 segs sobre el bus 1553.
- entry+landing: La tarea con la segunda prioridad, ya inactiva.
- bc_dist: La tarea de tercera prioridad, toma datos del 1553 y las coloca en un doble buffer circular desde donde extraen información las otras tareas, salvo las ASI/MET.
- Otras tareas de baja prioridad de la nave.
- ASI/MET: es la tarea de menor prioridad junto con las otras tareas científicas (generación y compresión de imágenes, etc.). A diferencia de las otras tareas, la ASI/MET toma datos al 1553 a través de un mecanismo de comunicación entre procesos usando el pipe() de vxWorks (es estándard de Unix).
El problema
Luego de detectado el problema se obtuvieron datos de debug del Mars Pathfinder y se simularon condiciones similares con un sistema gemelo en la Tierra trabajando junto con desarrolladores de VxWorks. Luego de 18 horas de simulaciones descubrieron lo que estaba pasando: por sobrecarga de tomas de datos –era mejor de lo que se esperaban– el sistema estaba más cargado que el peor caso probado en Tierra y ocasionaba la inversión de prioridades que impedía que la tarea bc_dist pudiese acabar… por culpa de la ASI/MET, aunque esta era de menor prioridad.
Inversión de prioridades
Este es un tema que ya se conocía en la “culturilla” informática, aunque no había nada forma ni soluciones hasta 1980 que se presentó una solución en el artículo Experience with processes and monitors in Mesa. Actualmente es un tema muy estudiado en las asignaturas de sistemas operativo o concurrencia, pero sobre la cuál no hay un consenso unánime sobre la mejor solución, aunque la más usada y simple es conocida como herencia de prioridades.
La inversión de prioridades es un problema que se pueden presentar en todos los sistema de multiprogramación cuando se usan mecanismos para controlar la exclusión mutua al acceso de recursos compartidos –como los semáforos mutex–. Antes que un proceso pueda acceder a esa sección crítica debe hacer una operación de wait sobre el semáforo, esta operación puede generar que el sistema operativo bloquee a ese proceso hasta que el proceso que está en la sección crítica en ese momento salga de ella.
Pero puede ocurrir lo siguiente.
Supongamos que A sea el proceso de mayor prioridad, B una de prioridad intermedia y C la de menor prioridad. C entra a una sección crítica, mientras está en ella es “quitada” –preempted– de la CPU para ejecutar el proceso B que tiene mayor prioridad. En ese momento A se ejecuta e intenta entrar en la misma sección crítica que C, como C está en ella A es bloqueado y el planificador del –el scheduler– sistema operativo vuelve a ejecutar el proceso B.
Es decir, C nunca sale de la sección crítica porque hay un proceso B con mayor prioridad, A que tiene mayor prioridad que los demás tampoco se ejecuta porque está esperando que C salga de la sección crítica… pero ésta no sale porque B tiene preferencia.
Este efecto es el conocido como inversión de prioridades y es lo que pasó en el Mars Pathfinder, bc_dist es equivalente a A, las otras tareas de baja prioridad de la nave son B y ASI/MET es C. ASI/MET fue preempted durante la llamada a la función select() (también estándar de Unix) que daba “acceso” al descriptor del pipe. Esta llama entra a una sección crítica mediante un semáforo mutex que es el que ocasionó la espera infinita de bc_dist.
El diagrama siguiente (obtenido de este PDF) muestra gráficamente lo que acabo de describir:
Cuando se ejecutaba bc_sched, la de mayor prioridad de todas las tareas, ésta detectaba que bc_dist no había acabado su ciclo y reiniciaba el sistema.
Nota: la terminología más apropiada quizás sea “proceso” en vez de “tarea” (no conozco más detalles de cómo están implementados), pero preferí respetar la terminología de los informes oficiales.
La solución
VxWorks sí tenía la opción de habilitar la herencia de prioridades, pero el JPL de la NASA había decido no habilitarla ya que era una variable global que afectaba a todos los semáforos. Una vez confirmado que se trataba de un problema de inversión de prioridades pidieron a VxWorks que analice el efecto global, estos respondieron que afectaría muy poco al rendimiento y que el sistema se comportaría de la misma forma siempre que hubiese un sólo proceso en las colas de espera de los semáforos. Como éste era el caso decidieron que habilitarían la herencia de prioridades de forma global.
Pero no tenían forma de cambiar esa variable directamente en el Mars Pathfinder, el procedimiento era que se debía enviar un parche binario con las diferencias entre el software de Marte con el que tenían en Tierra. Así que tuvieron que seguir este procedimiento, generar el parche y enviarlo por radio a Marte donde un programa preparado lo valida estrictamente y luego lo aplica (es similar por ejemplo al sistema de parches que se usa para actualizar los teléfonos con Android).
Este “parchado” quizás debería figurar como la actualización remota más lejana realizada a un sistema operativo.
Más información:
[*] Este es el “resumen oficial” de Glenn Reeves, director del equipo de software de Mars Pathfinder en el JPL de la NASA. Fue una respuesta con correcciones a un resumen de Mike Jones (de Microsoft Research) sobre una keynote de Dave Wilner (CTO de Wind River).
Curiosidad: Como le han “colado” la herencia de prioridades a Linus Torvalds
Linus Torvalds fue siempre reacio a implementar herencia de prioridades en Linux. Lo dejó bastante claro en un email del 2005:
“Los amigos no dejan que los amigos usen herencia de prioridades”
No lo hagas. Si realmente lo necesitas de todas formas tu sistema ya está roto.
Pero resulta que Linux sí soporta la herencia de prioridades desde la versión 2.6.18. ¿Quién lo logró? El fantástico Ingo Molnar. Éste había desarrollado la nueva interfaz de semáforos futex, era una optimización importante ya que casi todo el código se ejecutaba en modo usuario sin necesidad de hacer “costosas” llamadas de sistema en cada operación de semáforos. Estas últimas sólo se hacen en caso de contención entre varios procesos.
Este nuevo método era mucho más eficiente y fue aceptado para el kernel. Años después Ingo introdujo sin demasiada fanfarria –pero muy bien explicado y argumentado– el PI-futex, o lightweight userspace priority inheritance. Así tenemos en Linux un sistema de semáforos con rendimiento similar a los sistemas de tiempo real, además con herencia de prioridades… a pesar que Linus lo había prohibido expresamente tres meses antes. Ingo, genial como siempre
¿”Ingeniería” del software? Ahora vienen los mea culpa
Los que leen habitualmente mi blog conocen mi opinión sobre la “ingeniería del software tradicional”, muchas veces hablé de ello y del “mal” que hacen en general a la profesión, y hasta como limitan la innovación.
Nunca me gustó que se llame “ingeniería” a un proceso que tiene poco que ver con los de las ingenierías tradicionales, entre ellas que las tradicionales poseen una serie de herramientas analíticas y cuantitativas para evaluar y estimar riesgos y tolerancias. En el desarrollo de software no tenemos esas herramientas, quizás nunca las tengamos: no es lo mismo trabajar en la construcción de elementos físicos, limitados por leyes fundamentales de la física, con pocas combinaciones posibles, con la construcción de programas informáticos que no tienen esas limitaciones físicas, ni en las combinaciones posibles.
Este tipo de visión propuesta desde el mismo nombre “ingeniería” nos lleva a errores muy importantes, por ejemplo el de suponer que las construcciones físicas y las matemáticas [los programas] son equivalentes. No sólo hace “daño” a los proyectos que nunca pueden cumplir plazos y presupuestos, sino también a la propia “ciencia” y profesión informática. Es un engaño, fundamentalmente a los propios profesionales. Así se cometen errores derivados como reclamar colegios, regulaciones y normas y procedimientos similares a otras ingenierías: las soluciones equivocadas son más perjudiciales que los problemas abiertos. Además es ponernos una venda en los ojos para no observar el verdadero potencial de la informática, ni para darnos cuenta de dónde está la innovación (que no es precisamente seguir haciendo el mismo software de siempre).
Escribí bastante sobre estos temas, fundamentalmente en apuntes críticos con la creación de los colegios oficiales informáticos. El núcleo de la argumentación es la que comentaba antes, incluso de crear una “pseudociencia” (p.e. en Recursividad, punteros, estadísticas y pseudociencia del software, Diseño, ingeniería, ágiles… y frameworks, Redescubriendo al Dijkstra provocador 18 años después), además enseñado en la universidad como si de leyes científicas se tratasen –quizás lo que más daño hace–.
También critiqué al establishment de la “ingenieria del software”, dominado por tecnológos pocos “científicos” –en su mayoría empleados de grandes corporaciones de desarrollo de software para empresa de Fortune 500– que en ninguna de sus publicaciones científicas presentan los datos para que puedan ser contrastados (algo inherente del software privativo) y sometidos al peer review (que no es aceptable en ninguna otra rama científica). Este mismo establishment ha ignorado durante muchos años al software libre con sus métodos de diseño y desarrollo completamente contradictorios con los propuestos por la “ingeniería tradicional” (muy bien expuesto en al “Catedral y el Bazar” de Eric Raymond).
Muchas de las respuestas que tuve –incluso entre supuestos reputados profesionales– es que “hago daño a la profesión” [sic].
Pero ahora está surgiendo el mea culpa de los gurús incondicionales de la “ingeniería del software”. El reconocido gurú Tom DeMarco (gracias Paco) que escribió el artículo de opinión Software Engineering: An Idea Whose Time Has Come and Gone? (algo así como “Ingeniería del software: ¿una idea de tiempos pasados?”) que es en gran parte una autocrítica a sus ideas y libro de referencia Controlling Software Projects: Management, Measurement, and Estimation.
Resumo sus ideas fundamentales con algunas de sus frases traducidas literalmente:
- Hoy en día todos comprendemos que las métricas de software cuestan dinero y tiempo, y que deben ser usadas con moderación.
- El desarrollo de software es inherentemente diferente de las ciencias naturales tales como las física, por lo que sus métricas son muchas menos precisas para capturar lo que deben describir.
- La frase más citada del libre es «No puedes controlar lo que no puedes medir». Esta frase contiene una verdad real, pero cada vez me sentía más incómodo con el uso que hice de ella. Está implícita en la frase (y en título del libro) que el control es un aspecto importante, quizás el más importante, de cualquier proyecto de software. Pero no lo es.
- Muchos proyectos se han realizado sin demasiado control pero han generado productos maravillosos tales como Google Earth o la Wikipedia.
- Esto nos lleva a la desagradable conclusión que el control estricto es algo que importa mucho en proyectos relativamente inútiles y mucho menos en proyectos útiles. Sugiere que mientras más te enfoques en el control aumenta la probabilidad de que estás trabajando en un proyecto que se esfuerza por generar algo de valor relativamente menor.
- ¿Estoy diciendo que está bien ejecutar proyecto sin control o con un control relativamente menor? Casi. Estoy sugiriendo que deberíamos seleccionar primero a los proyectos cuyo control preciso no importe demasiado.
- Estoy llegando gradualmente a la conclusión que el momento de la ingeniería del software vino y se marchó.
- En los últimos 40 años nos hemos torturado por nuestra ineptitud en acabar proyectos a tiempo y con el presupuesto previsto. Pero como sugerí antes, no debería haber sido el objetivo supremo.
- El objetivo más importante es la transformación, crear software que cambie el mundo, o que transforme una empresa, o la forma en que hace negocios.
- El desarrollo de software es y será siempre algo experimental. Lo construcción real de software no es necesariamente experimental, pero sí lo es su concepción. Allí deberíamos enfocar nuestros esfuerzos. Allí es donde deberíamos haberlo hecho siempre.
Está muy bien que por fin los gurús de la “ingeniería [tradicional] de software” comiencen a reconocer errores y dejar de ignorar a los métodos y filosofías tan habituales del software libre y de proyectos que han sido realmente innovadores desde el punto de vista de gestión de proyectos de software. Todavía se queda corto y esta opinión me deja la misma sensación de dejè vu que me produjo hace unos diez años la moda de lo métodos ágiles y la programación extrema: era algo que ya se veía hace tiempo en el software libre y de hecho era mucho más radical que las “radicales” nuevas metodologías y métricas. Por ejemplo.
Tom DeMarco cita a dos proyectos que le parecen notables para mostrar la contradicción con las ideas tradicionales: Wikipedia y Google Earth. Pero resulta que ambos tienen muy poco que ver y no han sido “seminales” en sus aŕeas.
El software de la Wikipedia [un wiki colaborativo] es muy sencillo y no fue el primero en hacer un programa de este estilo. el éxito e importancia social de al Wikipedia es el proyecto global, y cómo ha conseguido formar una enorme comunidad de gente que comparte valores de fomentar la divulgación libre del conocimiento. Poco tiene nada que ver con “desarrollo de software”, sino con valores y conceptos mucho más amplios.
Google Earth es un programa mucho más complejo pero que ya eran comunes desde principios de la década de los ’90, con el auge de las estaciones 3D (el líder indiscutible era Silicon Graphics, SGI) y la realidad virtual. De hecho Google Earth surge de la compra de una empresa (Keyhole) que a su vez se basó en otras ideas e implementaciones previas de 1994. El logro fundamental de Google Earth poder acceder a esa cantidad de datos y dejarlos a disposición de todo el mundo, algo que antes sólo podían hacer muy pocas empresas. (En todo caso yo hubiese mencionada a Google Maps, porque mostrar esa información usando sólo HTML y Javascript sí fue una innovación importante, junto con Gmail mostraron el camino del desarrollo de aplicaciones Web/Ajax complejas).
Si yo tuviese que elegir proyectos de software realmente indicativo de las contradicciones con las “métricas tradiconales”, por ejemplo:
- Núcleo Linux: Es muy complejo desarrollaro un núcleo de sistema operativo, la variedad de hardware es enorme, los bugs tienen consecuencias graves, se necesitan mucho conocimientos, tiene una enorme cantidad de elementos independientes, las combinaciones de caminos de ejecución de casi infinitas. Aún así se desarrolló uno que funciona desde los superordenadores más grandes del mundo a los teléfonos móviles, con el aporte de miles de programadores sin ningún tipo de reuniones, coordinación, diseño previo o siquiera documentación básica (ahora ya hay bastantes libros y documentos).
- KDE/Gnome: Dos proyectos complejos, independientes pero competidores e incluso “con esfuerzos inaceptablemente duplicados”, pero que cada uno aportó innovaciones en distintas áreas y se integran o usan en entornos diversos como escritorios completos a dispositivos móviles o multimedias (por ejemplo el Sfari/Webkit que se ejcuta en Mac, iPhones, Androids y Nokias es derivado directo del KHTML de KDE).
- Sistema GNU/Linux o BSD (+ X.org, +GNOME/KDE, +multitud de software): Ejemplo radical de cómo se pueden construir sistemas complejos muy completos basados sólo en una coordinación o consenso mínimó para las interfaces entre ellos. Estas interfaces además son muy simples y no han dejado de evolucionar sin necesidad de “cambios drásticos”.
Cualquiera de los tres proyectos es –en código fuente– órdenes de magnitud más grande que la Wikipedia o Google Earth, además ninguno de ellos dependió de una empresa o fundación, y tienen una complejidad y diversad de uso mucho mayor.
Por eso, está muy bien que empiecen a descubrir todo un mundo allí fuera, que creció y se desarrolló sin seguir las métricas y métodos “tradicionales”. Es aún una mejor noticia que los gurús de la ingeniería del software empiecen la autocrítica de las ideas erróneas que han fomentado durante 40 años.
Estaría ahora muy bien que los fanboys de esa “ingeniería” empiecen a aceptarlo. Pero sería mucho mejor que los responsables de planes de estudio y asignaturas de “ingeniería del software” en las carreras informáticas sean conscientes del problema, al menos ya tienen a uno de sus gurús poniendo en dudas los cimientos de su [pseudo] ciencia. Quizás así dejaremos de leer y escuchar falacias tales como “para ser buen director de proyecto no hace falta saber programar”, “un proyecto es malo si no tiene un diseño previo bien especificado y documentado”, o el mantra:
[...] podemos y/o debemos gestionar un proyecto de software de la misma forma que un proyecto de arquitectura.
Futurología: en unos pocos años veremos que el hot topic en las publicaciones de ingeniería del software serán estudios de caso de grandes proyectos de software libre. Nos hablarán de la radicalidad de los métodos ágiles usados, la coordinación debil, el software evolutivo, la complejidad de las relaciones sociales entre programadores y usuarios, la influencia del uso real de los usuarios para el diseño del software, la importancia del release often, del peer review, los casos reales de uso no previstos por los programadores… y volveré a sentir una sensación de dejà vu.












![Blog [no] premiado](https://gallir.files.wordpress.com/2013/03/premio-no-premio.jpg?w=200&h=261)

Comentarios recientes