O el más explícitamente cinematográfico "La magia tenía un precio" (y a mí me tocaba pagarlo tarde o temprano) que posiblemente describe mejor lo que pretendo contar en esta disculpa con forma de post. Sin embargo me identifico bastante más con el título que he elegido (quizá porque aquel vinilo de Pink Floyd de similar nombre vio la luz pocos meses antes que yo) y además me sirve de excusa para hacerme por fin eco de un "blog grupal" de desarrollo del que disfruto desde hace ya más de un año: La Cara Oscura del Desarrollo de Software.
En lacaraoscura.com trece aparentemente-latinos-(quizá-venezolanos)-y-bilingües amantes de los objetos y sus clases escriben en español e inglés posts sin desperdicio para aquellos que estamos interesados en conocer los rincones ocultos (o no tan visibles) del desarrollo y mundos aledaños.
Rincones ocultos cómo el que quizá podría haber intuido el pasado jueves antes de pasar a producción en La Coctelera uno de los fallos más graves y peligrosos que he cometido en su ya no tan corta historia.
Antecedentes (o background)
No pocos hemos sido los que nos hemos quedado (y seguimos quedando) maravillados más de una vez con Rails (y Ruby) al ver como con apenas un par de líneas de código, como por arte de magia y con una elegancia casi insultante, teníamos resuelto algo que en principio pensábamos que nos iba a costar mucho más conseguir.
Una de las cosas que habitualmente facilita cualquier framework que se precie es la conexión con la base de datos: en algún lugar de la aplicación indicamos la información necesaria para conectar con la base de datos y después la utilizamos sin preocuparnos por establecer la comunicación con la misma.
En su equipamiento de serie Rails trae tres entornos de ejecución distintos (desarrollo, test y producción) y permite definir para cada uno de ellos su propia base de datos. Con dicho equipamiento nos hemos venido apañando pero desde un principio hemos sido conscientes de que era una de las partes pendientes de escalar en nuestra arquitectura.
En más de una ocasión el acceso a la base de datos nos ha permitido verle las orejas al lobo (orejas con forma de cuello... de botella, ¡pero qué lobo más feo por Ford!) y hemos tenido que ir sacando cartuchos para alejarlo lo más posible de nuestro rebaño: primero fue una máquina dedicada (maitai...), después una inyección de memoria (... que de 2 gigas paso a tener 10) y uno de los últimos ha sido la separación de la base de datos de La Coctelera de la del resto los sites que alberga the-shaker-club.
Pero en realidad los cartuchos se nos comenzaban a agotar. Sin duda, comprar una mirilla de precisión (con forma de cluster) y acertar con alguno para darle digna sepultura al lobo era uno de nuestros mayores deseos. Pero por desgracia a día de hoy no las fabrican con la suficiente fiabilidad en ningún laboratorio conocido.
En la conferencia Rails que se celebró en Londres el pasado mes de septiembre un par de ponencias o tres sobre base de datos y mysql parecían que nos podian aportar algunos cartuchos más en nuestra lucha contra el lobo (todos contral el lobo, todos contra el lobo, laaaaalalala la!). Desafortunadamente la munición que allí encontramos era la que ya habíamos empleado (más memoria, optimización de las consultas, etc.) y nos volvimos sin poder iluminar mejor el tunel de la escalabilidad de nuestras bases de datos.
Sin embargo nosotros, optimistas siempre, para evitar que el lobo volviese a acercarse decidimos salir en su búsqueda, por aquello de que no hay mejor defensa que un buen ataque.
El ataque (en forma de código)
Comenzamos a investigar formas de aumentar el número de servidores que pudiesen atender la peticiones de lectura (esclavos) manteniendo uno para las de escritura (maestro). Sin demasiadas dificultades encontramos en la red código Rails que hacía lo que queriamos utilizando la llamada al establish_connection del ActiveRecord.
Lo añadimos casi sin aliñar como filtro previo (before_filter) al código de the-shaker incorporando la definición de nueva base de datos de "sólo lectura" para el entorno de producción (production_ro) y utilizando el controlador y la accion para determinar si la petición debía hacer uso de la misma. El código era simplificando algo como lo siguiente:
if readonly_action
establish_readonly_connection
else
establish_normal_connection
end
Bien, esto funcionaba de maravilla pero, teniendo en cuenta que en una aplicación Rails nunca tienes que establecer la conexión con la base de datos del entorno en ejecución, aparentemente se podía optimizar evitantado establecer dos veces la "normal_connection", quedando la cosa de la siguente guisa:
establish_readonly_connection if readonly_action
Y efectivamente, si la acción no era de "sólo lectura" la petición se resolvía atacando sin problemas a la base de datos maestra. Perfecto, toda una señora refactorización... pero envenenada.
El terreno de juego (con forma de fastcgi)
En los sites de producción de the-shaker-club utilizamos FastCGIs para resolver las peticiones que llegan a la aplicación desde los servidores web.
Resumiendo los FastCGIs lo que consiguen es reducir el tiempo de respuesta de una petición "preparando su entorno" antes de que esta llegue. Dicha preparación se basa en el hecho de que la ejecución de todas las peticiones es la misma en su fase inicial.
De esta forma cuando llega la petición todo está listo para comenzar a ejecutar sus tareas específicas. Al finalizar su ejecución el entorno inicialmente preparado para ella se reutiliza para la siguiente petición, sin volver a preparalo desde cero (¡viva el reciclaje!).
La llamada al establish_connection del ActiveRecord se encuentra dentro de dicha preparación inicial.
El resultado (en forma de desastre)
Al establecer la conexión contra la base de datos esclava en una petición de sólo lectura, su entorno se quedaba con dicho entorno para las siguientes peticiones... ¡incluidas las de escritura!
El caos reinó en el reino coctelero durante algunas horas y, aunque posteriormente se pudo volver a la normalidad sin tener que lamentar grandes pérdidas, desde entonces no he dejado de pensar en disculparme por semenjante metedura de pata. Espero que mis compañeros finalmente no me corten las zarpas.
Tenía intención de contar aquí también la solución final (el remate final y ¡gol!) pero tengo la sensación de que si siguo más tiempo con este post al final se querará sin publicar enterrado entre mis borradores.
Termi-nando: lo siento cocteler@s, prometo estar mucho más atento a la magia indómita de Rails de ahora en adelante. Bien lo merece. Bien lo merecéis.