27 mar 2011

No uses variables globales

Este post explica por qué mejor no usar variables globales en entornos Web y está dirigido a personas que aún no lo ven tan claro. Para este ejemplo emplearé Apache, mod_python/mod_wsgi, Django y Memcached pero cualquier otro entorno Web también sirve.

Apache


Apache tiene 2 modos de funcionamiento:
1. Prefork: Mediante procesos
2. Worker: Mediante hebras y procesos
Más frecuentemente se suele usar el modo 1 porque es más seguro ya que no todas las librerías usadas están escritos teniendo en cuenta que pueden ser empleadas por varias hebras a la vez. Se dice que no son thread-safe.

En las variantes de Unix (Linux, FreeBSD, NetBSD, Mac OS X, Unix, Solaris, etc.), el modo 1 se basa en una llamada al sistema operativo fork que divide un proceso en dos, uno considerado "padre" y otro "hijo". Ambos procesos tienen una copia idéntica de todos los datos. Pero una vez divididos, los cambios en memoria realizados por un proceso ya no afectan al otro.

En caso de Apache, existe un proceso maestro responsable de crear los hijos, controlar el número de procesos disponibles, etc. Los procesos hijos son los que atienden a las peticiones. En caso de los intérpretes de lenguajes o en general cualquier módulo, se carga después de realizar el fork.

mod_python y mod_wsgi


mod_python y mod_wsgi permiten el uso de más de un intérprete si Apache está sirviendo varias aplicaciones Web y pueda haber interferencias entre ellas. Si, por ejemplo, usamos dos intérpretes y tenemos diez procesos de Apache, podemos tener hasta 2 x 10 = 20 intérpretes de Python activos a la vez. Al mostrar los procesos con ps o top no aparecen como Python sino como Apache porque se ha invocado el intérprete desde una llamada a una librería. Por tanto, se ejecuta en el espacio de memoria y con los permisos del proceso Apache.

Por otra parte, todo lo que un proceso almacena en variables globales, por ejemplo datos de una base de datos, datos pre-calculados o páginas web completas, no puede ser aprovechado por otro proceso. Supongamos que haya que cargar un conjunto de datos de tamaño considerable y lo almacenamos en variables globales. En caso de Python, por ejemplo, podría ser un diccionario asignado a una variable con ámbito de módulo durante el inicio de la aplicación o cuando el usuario realice una determinada petición. Cada proceso tendría entonces que seguir los mismos pasos duplicando así los datos en memoria y realizando las mismas consultas a la base de datos. Esto no es demasiado óptimo y hará que cada proceso de Apache ocupe mucha memoria.

¿Qué podemos hacer al respecto?

Memcached


La solución es usar una caché compartida entre los procesos e incluso entre varias máquinas. Siempre que queramos aprovechar un dato elaborado, tal como los resultados de una consulta de base de datos, una página completa, un cálculo estadístico, etc. lo almacenamos en la caché compartida para que cualquiera de los procesos Apache pueda aprovecharlo.

Memcached es una caché de este tipo. Básicamente, se trata de un software que permite almacenar y recuperar conjuntos de datos desde cualquier ubicación de nuestra red.

La lógica podría ser siempre la misma:
1. Intentar obtener el valor requerido desde la caché
2. En caso de no existir, lo calculamos y lo almacenamos en caché.
3. Hacer lo oportuno con el valor.

Un fragmento Python podría ser:

value = cache.get(key)
if value == None:
# calc value
value = do_calc_value_here()
cache.set(key, value)
# do something with value
render_template(template, value)


Todos los sistemas de caché emplean una clave para almacenar y poder recuperar con posterioridad el valor. A la hora de establecer la clave es importante indicar si el valor cacheado puede compartirse entre todos los usuarios o no. Por otra parte, es importante que se actualice la caché cuando algún usuario provoque un cambio, es decir, que la caché no ofrezca valores inconsistentes.

Django & Memcached


Tomemos como ejemplo Django. Podemos almacenar un valor recuperado de la base de datos siempre que la petición de otra persona no lo actualice ni que exista otra aplicación que actualice los mismos datos directamente sobre la base de datos. Para no tener que lidiar con diferentes APIs de los sistemas de caché, Django ofrece una abstracción:

from django.core.cache import cache

cache.set(clave, valor, tiempo) # almacenar un valor en cache
valor = cache.get(clave) # recuperar un valor de cache

Para facilitar el uso con página completas, puede emplearse el decorador cache_page:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
...

Esto almacena la página generada durante 900 segundos y emplea la versión cacheada si está disponible. La función cache_page hace toda la mágica.

Supongamos, sin embargo que la página dependa de la persona que realiza la consulta:

from django.core.cache import cache

def my_view(request):
key = "my_view" + request.user.username
page = cache.get(key)
if not page:
page = render....
cache.set(key, page, 60 * 15)
return page

El valor a almacenar en caché ha de ser "persistible". El cliente de memcached para python (OJO: existen varios clientes en la actualidad: python-memcachedmemcached, python-libmemcached y pylibmc usa pickle, siempre que no se trate de una cadena de caracteres. Esto significa que no podemos cachear objetos tipo conexión a base de datos, objeto sesión, etc.

Es importante dimensionar el tamaño de RAM asignado a memcached, así como el número de posibles conexiones. Los valores por defecto son 64 MBytes (parámetro -m) y 1024 (parámetro -m) respectivamente. Mire la ayuda de memcached para ver cómo modificar estos parámetros (memcached -h).

Django ofrece incluso la posibilidad de cachear trozos de una plantilla:

{% load cache %}
{% cache 500 topmenu %}
.. topmenu ..
{% endcache %}

En caso que el menú dependa del usuario podría usarse el siguiente fragmento:

{% load cache %}
{% cache 500 topmenu request.user.username %}
.. topmenu ..
{% endcache %}

Es decir, todos los parámetros a partir del segundo de la etiqueta de plantilla "cache" son usados para formar la clave de caché.

Finalmente, pueden almacenarse también las sesiones en memcached. Para ello indicamos lo siguiente en settings.py:

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

Si deseamos que las sesiones sobrevivan el reinicio de memcached a costa de un muy pequeña reducción de rendimiento debemos usar siguiente línea:

SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"

Los detalles sobre el uso de caché en Django los podemos encontrar en la Web de Django.

Conclusión


En vez de usar variables globales podemos emplear una caché compartida para ahorrar recursos y aumentar la escalabilidad. El uso de la API de caché de Django es extremadamente simple.

18 mar 2011

Alfresco Export Tool


I recently found the problem that Alfresco doesn't provide a shell script to export repository contents anymore. So I rewrote one of them found on Alfresco forums. One of the problems that I encountered is that it tries to start the VTI server module (if installed), colliding with the running Alfresco instance. Setting port to 0 disabled it.



See my post to Alfresco forums.