Ruby on Rails, Apache y cache para multiples dominios

Visita este artí­culo en http://www.estadobeta.com/2007/04/27/ruby-on-rails-apache-y-cache-para-multiples-dominios/

Por Ismael en Ruby & Rails, artículos, tips

Implementando cache de páginas para aplicaciones que soportan varios dominios o subdominios.

El problema

Ok. El título es largo y necesita una introducción.

En Aardvark estamos construyendo una aplicación basada en web que permite crear varios “sitios”. Aunque cada sitio puede tener su propio nombre de dominio (por ejemplo “www.mi-propio-dominio.com”), todos estan dirigidos a la misma aplicación. Ésta usa el nombre de dominio - o subdominio - para determinar qué sitio mostrar, desde una base de datos.

El problema es cómo implementar un cache para las páginas estáticas de cada sitio. Aunque Ruby on Rails es una delicia para desarrollar, no es exactamente un bólido. Cuando una sola aplicación debe encargarse de varios sitios a la vez, necesitas generar versiones estáticas, html, de cada página. De esta manera es el servidor web quien sirve las páginas directamente, evitando el overhead de consultar la base de datos cada vez.

¿Qué hacer?

Rails, Apache, mod_rewrite

En Rails esto es extremádamente fácil. Sin entrar en detalles de cómo se organiza una aplicación Rails (eso es arena de otro costal), basta con definir qué acciones (métodos) de tus controladores debieran generar una versión estática.

caches_page :index, :lista, :detalle

Dependiendo de cómo estructures tus URL’s en la configuración de Rails (arena de otro costal más!), eso hace que, la primera vez que visites las páginas index, lista o detalle, Rails hará lo que tenga que hacer para construir esas páginas (consultar una base dedatos, otros procesos dinámicos). Acto seguido, creará copias estáticas de esas páginas en el directorio público de la aplicación (index.html, lista.html, detalle.html). La próxima vez que un usuario visite esas páginas, estas serán servidas directamente de los archivos estáticos. Mucho más rápido y eficiente.

Para esto último, toda aplicación Rails viene con un archivo de configuración local de Apache (llamado .htaccess) con las siguientes instrucciones:

RewriteEngine On
RewriteRule ^$ index.html [QSA]
RewriteRule ^([^.]+)$ $1.html [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

La primera línea activa el poderoso módulo mod_rewrite de Apache, que se encarga de parsear y reescribir URL’s. La segunda línea le dice a Apache que, si la URL viene vacía (”www.mi-sitio.com/”), debe agregar “index.html”.
Si no viene vacía, la tercera línea agrega “.html” a la URL. Esto convierte “www.mi-sitio.com/contacto” en “www.mi-sitio.com/contacto.html” (siempre que la URL no contenga ya una extensión de archivo, como imágenes o CSS’s).
Finalmente, la cuarta línea revisa que la URL procesada no pertenezca a un archivo existente en el servidor. Si no existe, el archivo .htaccess envía la URL completa al framework Rails (dispatch.fcgi), que descompone la URL para saber qué acciones ejecutar en la aplicación.

Sin embargo, si la URL entrante si corresponde a un archivo real en la raiz del sitio (”www.mi-sitio.com/logo.jpg”), Rails no es invocado y Apache sirve directamente el archivo, con mínimo esfuerzo.

Esta es la forma en que Rails aprovecha el poder del módulo mod_rewrite de Apache para sus sistema de cache. La primera vez que solicite la URL “www.mi-sitio.com/contacto”, .htaccess reescribirá la dirección como “www.mi-sitio.com/contacto.html” y preguntará si ese archivo existe. Al no encontrarlo, enviará la URL a Rails, quien generará dinámicamente la página y la guardará como “contacto.html”. La próxima vez la página si existirá y Rails no será necesario.

Multiples dominios, un sólo directorio

Excelente. Para una aplicación simple, unas pocas líneas de código en Rails y tenemos el asunto del cache resuelto. ¿Pero qué pasa cuando la misma aplicación soporta múltimples sitios? El problema es que, dado que todos los sitios comparten el mismo directorio público, qué pasa si dos o más sitios tienen una página llamada “contacto”? Con el sistema tradicional, el primer sitio visitado generará la página “/contacto.html”. Para los otros sitios que tengan una página del mismo nombre, Apache usará la primera página guardada en el cache, perteneciente a otro sitio!

Rails hace su magia

La solución es decirle a Rails que cree carpetas diferentes para guardar los archivos estáticos de cada sitio. Por suerte, tras las convenciones a que nos acostumbra Rails se esconde un sistema altamente configurable.

En el controlador principal de nuestra aplicación (generalmente app/controllers/application.rb), usamos el nombre de dominio como nombre de directorio para el cache.

before_filter :set_cache_directory # esto se ejecutará antes que nada


  def set_cache_directory
    # recupero el dominio completo del objeto request
    dominio = request.subdomains.join(”.”) + “.” + request.domain
    # directorio de cache para cada dominio
     self.page_cache_directory = RAILS_ROOT+”/public/cache/” + dominio
  end

Con esto, luego de generar las páginas para el caché de la aplicación, el directorio público se vería algo como así:

/
  cache/
          www.mi-sitio.com/
              index.html
              contacto.html
          www.otro-sitio.com/
              index.html
              contacto.html
              logo.jpg

Perfecto. Ahora Rails genera un cache independiente para cada sitio soportado por la aplicación, pero ¿cómo sabrá Apache dónde buscar los archivos estáticos?

.htaccess al rescate (de nuevo)

El módulo mod_rewrite nos salva de nuevo. Con él, podemos reescribir las URL originales para que Apache busque en el lugar apropiado. Esto lo hacemos agregando unas líneas de código en el archivo .htaccess en la raiz pública de la aplicación.

Decirlo es fácil, pero mi increíble torpeza con las Expresiones Regulares me tuvo varias horas probando, sin éxito. Por suerte mi amigo y socio Tomás “Bootlog” Pollak estaba cerca con su manejo del idioma .htaccess y su fluído acento en Expresiones Regulares.

A continuación, el .htaccess completo.


RewriteEngine On

# sacar trailing slash
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ /$1 [R=301,L]

# estan pidiendo un archivo estatico?
RewriteCond /www/super_aplicacion/public/cache/%{HTTP_HOST}/%{REQUEST_URI} -f
RewriteRule ^(.+)$ /cache/%{HTTP_HOST}/$1 [QSA,L]

# revisar si estan pidiendo la portada, y entregarla (index.html)
RewriteCond %{REQUEST_URI} ^/$
RewriteCond /www/super_aplicacion/public/cache/%{HTTP_HOST}/index.html -f
RewriteRule ^$ /cache/%{HTTP_HOST}/index.html [QSA,L]

# ver si esta el .html cacheado, y servirlo si esta
RewriteCond /www/super_aplicacion/public/cache/%{HTTP_HOST}%{REQUEST_URI}.html -f
RewriteRule ^([^.]+)$ /cache/%{HTTP_HOST}/$1.html [QSA,L]

#y si no hay un archivo estatico, pasarselo a rails
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

Ahí está. Lo que hace esta maravilla es lo mismo que el .htaccess original de una aplicación Rails, pero además usa el nombre de dominio del sitio (la variable %{HTTP_HOST} de Apache para revisar la existencia de archivos estáticos en el cache de cada dominio (por ejemplo ./public/cache/www.mi-dominio.com/). Finalmente, si ni uno de los archivos buscados existe, la URL completa es pasada a Rails para que éste la procese y genere el nuevo cache (RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]).

Este artículo introduce a Ruby on Rails como invitado de honor en EstadoBeta. Aunque lo he estado usando hace más de un año, por alguna razón no se había aparecido con frecuencia por aquí. Es tímido.

11 comentarios para “Ruby on Rails, Apache y cache para multiples dominios”

  1. GravatarJose Rodriguez Dice:

    Como complemento al uso de técnicas de cache, aprovecho de recomendarles una biblioteca llamada “memcache” que llevo utilizando por algún tiempo con exelentes resultados. Utiliza cache en memoria RAM por lo que es volátil y restringida solo a algunos gigas, pero tiene la gran ventaja de ser muy rápido, distribuido y altamente escalable. Varios portales grandes ya lo están utilizando.

    Para más información pueden buscar en Google.

  2. GravatarIsmael Dice:

    Gracias José. Hace tempo que tengo ganas de hincarle el diente a memcache.

  3. GravatarNico Orellana Dice:

    Excelente articulo Ismael, si bien nos cuentas como solucionar el tema del cacheo. Yo actualmente tengo un problema que no me deja dormir comodo en las noches, tambien estoy trabajando en una aplicación que la idea es que sirva otros sitios siendo llamados con otros dominios.

    El problema es que no estoy usando fastcgi , a cambio, el ligero mongrel.

    No se si tienes experiencia tratando de hacerlo con mongrel, pero necesito que si llega un dominio x invoque el controlador x emulando un vhost y si llega el dominio y, controlador y.

    No se si me explico, espero.
    Saludos.

  4. GravatarIsmael Dice:

    Nico, nosotros en Aardvark también usamos Mongrel, junto con Mongrel Manager, un administrador de procesos Mongrel desarrollado por Hector Vergara (sitio offline por ahora).

    No soy experto en servidores, pero los vhost son algo que yo haría en el servidor web (Apache o Lighttpd). El servidor web configura los vhosts y dirije el request a la aplicación y controlador que quieras. Mongrel sólo se encarga de gestionar cada aplicación.

    Otra opción es que dirijas cada dominio a controladores diferentes a nivel de DNS.

    ¿Qué quieres hacer exactamente?

  5. GravatarNico Orellana Dice:

    Ismael te explico.

    1. Tengo dos dominios apuntando al mismo servidor.

    2. Y tengo una aplicación en donde llamo a un controlador, por ejemplo … www.xxx.cl/nico -> que llama al controlador DIRIGIR con parametro nico. Entonces este controlador despliega todo lo referente a Nico.
    Puedes ver tus fotos (por ejemplo), en www.xxx.cl/nico/fotos

    La idea es que www.nico.cl despliegue www.xxx.cl/nico y que si quiero ver sus fotos sea www.nico.cl/fotos no www.xxx.cl/nico/fotos

    3. He echo esta pregunta 2 veces en la lista de rails y/o me explico MUY MUY MAL, o esta dificil.

    A nivel de dns podria pasarle el controlador pero no se como hacerlo como vhost.

  6. GravatarIsmael Dice:

    Nico, creo que entiendo lo que quieres hacer, pero ya que me preguntas, yo lo haría de otra forma:

    1). Todos los dominios apuntan a la misma aplicación y al mismo controlador.

    2). La aplicación maneja Cuentas de usuario (un modelo Cuenta, por ejemplo). Cada Cuenta tiene un campo “dominio”, o “url”.

    3). El controlador usa el dominio para rescatar de la base de datos la Cuenta apropiada, y con ellas los datos y fotos correpondientes a esa cuenta.

    Por ejemplo, en el controlador:


    brefore_filter :get_account


    def get_account
    dominio = request.subdomains.join(”.”) + “.” + request.domain
    @account = Cuenta.find_by_dominio( dominio )
    @fotos = @account.fotos
    end

    Se entiende? Así no tienes que crear un nuevo controlador para cada usuario. Los usuarios (Cuentas) estan en la base de datos. Un solo controlador obtiene el usuario correcto de acuerdo al dominio que hace el request.

  7. GravatarNico Orellana Dice:

    Ismael, nunca se me hubiese ocurrido, no te conteste altiro, porque lo queria ver funcionando y funciono de maravillas.

    Muchas gracias,

  8. GravatarIsmael Dice:

    Me alegro. Por curiosidad, puedo saber qué estas haciendo? Me gustaría estar al tanto de lo que se está haciendo con Rails en Chile.

  9. GravatarNico Orellana Dice:

    Estoy en fase de desarrollo por lo que no puedo dar muchos detalles porque todavia ni yo los manejo, pero es basicamente un sistema generador de comunidades para centros de alumnos de Universides.

    Por otro lado, te veo prendido con el tema… Que te parece un cafe en algun lado con los interesados, con Hector (tu colega) habiamos quedado en juntarnos pero no me he podido comunicar con él, y otro chico tambien esta con ganas de juntarse (el dueño de chileonrails.cl), que te parece?

    Cualquier cosa dispara a mi correo
    Saludos.

  10. GravatarIsmael Dice:

    Te disparé a tu correo :)

  11. Gravatarivagew Dice:

    Hi My Name Is ivaakz.

Deja un comentario

XHTML: puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>