Developing Frogtek

El blog del Departamento de Tecnología

Categoría: gae

Google App Engine: Lanzar un MapReduce desde un Cron (Segunda Parte)

Hace más de un año, Alberto nos explicaba cómo lanzar un MapReduce desde un Cron en Google App Engine. Desde entonces ha llovido mucho. Google App Engine ha evolucionado y madurado, trayéndonos mejor documentación y una comunidad más activa.

Gracias a ello he podido encontrar una solución más sencilla que la que os ofrecímos en nuestro último post al respecto. Digamos que queda mucho más sencillo 😛

from mapreduce import control
 
mapreduce_id = control.start_map(
"My Mapper",
"main.my_mapper",
"mapreduce.input_readers.DatastoreInputReader",
{"entity_kind": "models.MyEntity"},
shard_count=10)

Cuidado con el shard_count. Aseguraos que corresponde con el que tenéis configurado en vuestra aplicación.

 

BDD en Google App Engine con Lettuce y Splinter (I)

Hemos de reconocerlo: tenemos mucho que aprender en lo que se refiere a programación de pruebas automáticas en la web. Hace ya bastante tiempo que veníamos usando Selenium , primero grabando las pruebas con su IDE, más tarde programándolas con Web Driver. Pero los tests resultantes eran frágiles y aburridos de programar, por lo que sentíamos bastante envidia de los developers Ruby con su flamante Cucumber y sus elegantes tests BDD.

Decidimos remediarlo y Julio se puso con la instalación de  Cucumber + Capybara en el servidor, creando un test de demo y animándonos a montar el entorno en nuestras máquinas para comenzar a darle poquito a poco. Teníamos que aprender Ruby y conocer cómo funcionan sus versiones y conceptos nuevos como RubyGems, por lo que la cosa iba a ser divertida pero no rápida y por ello se nos ocurrió darle una oportunidad a una herramienta que nos sonaba de oídas: Lettuce, combinándola con otra herramienta bastante nueva: Splinter, de manera que, en lugar de pepinos y capibaras:
Siempre es verano con el pepino en la mano

tendremos lechugas y poder mutante. ¡Cowabunga!

Lettuce permite definir test de aceptación automáticos para proyectos Python, y Splinter proporciona un API sencilla y potente para implementar los pasos de los tests usando diferentes webDrivers.

De momento parece que aún falta que ambos proyectos mejoren un poco ya que son relativamente recientes. En el caso de Splinter, si no llega a darnos todo el potencial que necesitamos, siempre podemos sustituirlo por twill, selenium, webdriver o windmill.

¿Por qué usar estas herramientas quizás no tan maduras como sus homólogas del mundo Ruby? Una razón discutible – aprender un nuevo lenguaje siempre es divertido y enriquecedor – es que no nos obliga a aprender Ruby y nos permite definir los pasos en Python con el que estamos mucho más cómodos. Pero la ventaja principal es que tenemos una librería Python usada en nuestros tests de integración para crear diferentes escenarios de test en GAE y esta solución nos permite aprovechar todo su potencial instantáneamente. Bueno, eso si conseguimos instalar correctamente todo lo necesario para rular estas herramientas con GAE.

En Frogtek ninguno tenemos experiencia con Django más allá de su integración con Google App Engine, por lo que seguramente nos falte algo de familiaridad con ciertos conceptos que no aplican dentro de la plataforma de Google. Es quizás por esto por lo que nos encontramos con bastantes problemas de instalación y configuración que nos gustaría compartir con todos aquí. Eso será en el próximo post.

Lanzar un Job de Jenkins desde Google App Engine

La semana pasada estuve dándole vueltas a este problema, después de varios intentos fallidos llegué, con la ayuda de Julio, a la solución que planteo en el título del post.

Para empezar tenemos que ir al job en Jenkins que queramos lanzar desde fuera y activar la opción de “Lanzar ejecuciones remotas”

Por motivos de seguridad, Jenkins nos obliga a usar un Token que será usado al llamar al Job desde fuera.

El código que usaremos en GAE (en Python) será el siguiente:

import urllib2, base64, httplib
from urllib2 import URLError, HTTPError
from django.http import HttpResponse

def call_mlp_generator(request):

    theurl = 'JENKINS_JOB_URL?token=TOKEN'
    username = 'USERNAME'
    password = 'PASSWORD'

    request = urllib2.Request(theurl, None)
    base64string = base64.encodestring('%s:%s' % (username, password)).replace('n', '')
    request.add_header("Authorization", "Basic %s" % base64string)
    try:
        urllib2.urlopen(request)
        response = str(httplib.OK)
    except HTTPError, e:
        response = str(e.code)
    except URLError, e:
        response = str(e.code)

    return HttpResponse(response)

Bastante simple, sólo había que saber tocar las teclas adecuadas.

Entorno de desarrollo de Google App Engine en Fedora 15

Me parece interesante contar la experiencia que tuve ayer al configurar el entorno de desarrollo de GAE en una máquina Linux (Fedora 15 en este caso), sobre todo por si puede ayudar a otros desarrolladores y evitar algunos dolores de cabeza.

Aquellos que trabajen con GAE han de saber que hasta nueva orden, el sistema necesita Python 2.5 para poder funcionar correctamente. Google así lo recomienda, aunque recuerdo haberlo hecho funcionar sobre Python 2.6 en mi anterior vida (cuando era Ubuntu).

Voy a intentar reconstruir el camino correcto a seguir para configurar el entorno de desarrollo de GAE sobre un Fedora 15 (ya que di muchas idas y venidas).

La última versión de Fedora tiene por defecto Python 2.7, y si intentaba arrancar el proyecto en local usando esta versión de Python, tenía serios problemas de incompatibilidades de módulos, por ello me bajé Python 2.5.

Por otro lado instalé un par de librerías que necesitaría más adelante (esto tardé varias horas en hacerlo funcionar, por el orden en que hacía las cosas):

sudo yum install openssl openssl-devel -y
sudo yum install sqlite-devel

Una vez instaladas estas librerías era el momento de compilar el código de Python 2.5 e instalarlo. Antes de nada, hay que modificar el fichero <python-folder>/Modules/Setup.dist y descomentar las siguientes líneas:

# Socket module helper for SSL support; you must comment out the other
# socket line above, and possibly edit the SSL variable:
SSL=/usr/local/ssl
_ssl _ssl.c 
	-DUSE_SSL -I$(SSL)/include -I$(SSL)/include/openssl 
	-L$(SSL)/lib -lssl -lcrypto

(Líneas 206-209)

De esta forma habilitamos el módulo de SSL cuando construyamos Python.

Es hora de construir Python:

su
./configure
make
make altinstall

Con el make altinstall lo que hacemos es instalar Python en una ruta diferente y no machacar la versión actual que tengamos instalada en el sistema.

Si todo ha ido bien, ahora podremos usar

python2.5 dev_appserver.py --port 8080 RutaDeTuAplicación

Si compilamos Python 2.5 sin haber instalado las librerías arriba mencionadas o sin haber habilitado los módulos de SSL podríamos encontrarnos problemas como estos a la hora de arrancar el proyecto:

AttributeError: 'module' object has no attribute 'HTTPSHandler'

o

ImportError: No module named _sqlite3

(Dependiendo de lo complejo que sea vuestro proyecto, claro.)

Espero que esta pequeña guía sirva de ayuda para aquellos que tengan los mismos problemas que tuve en su momento.

Compartir es poder.

VirtualEnv para Google App Engine en local

Por defecto en Google App Engine , hasta hace bien poco, solo se podía usar hasta la versión 1.1 de Django. Esto generaba problemas si tenías alguna otra aplicación web que requería una versión más reciente de Django, como por ejemplo nuestra herramienta de revisión de código. Obtenías una serie de conflictos que eran una molestia para el desarrollo y uso en local del servidor, impidiendo un desarrollo fluido y una configuración estable.

La solución por la que optamos fue usar virtualenv. Con esta herramienta podríamos separar el entorno de ejecución de Google App Engine, del resto de aplicaciones instaladas, tanto en el servidor de integración continua como en los equipos de los desarrolladores.

Después de consultar varios sitios web al respecto, el proceso de configuración quedo algo así:


aptitude install python-virtualenv python-pip

Para instalar tanto el propio virtualenv como pip, una herramienta para instalar paquetes en dicho entorno.


virtualenv --no-site-packages --python=python2.5 ENV

Para configurar en ENV el entorno. –no-site-packages indica que no herede ningún package de la instalación de la cual se toma python y le indicamos que queremos la versión 2.5 pues GAE se ejecuta en con la 2.5 en los servidores de Google.

pip install -E ENV Django==1.1

Añadimos la versión de Django que utilizar con pip.

Para finalizar ya solo falta copiar la carpeta de Google App Engine dentro de ENV. Activar el entorno utilizando un par de scripts que hay en ENV/bin/ y lanzar el servidor de desarrollo.

Ahora mismo toda esta parafernalia tiene algo menos de sentido pues en la última versión del SDK del GAE han añadido la versión 1.2 de Django. Pero aún así puede ser útil para conseguir un entorno controlado y separado del resto de la máquina. Ya sea un servidor de integración continua o nuestra propia máquina de trabajo.

Google App Engine: Lanzar un MapReduce desde un cron

Con las antiguas limitaciones del GAE, la ejecución de un proceso batch (generación de informes, actualización de una columna en todas las entidades de un tipo…) requería montar complejos sistemas diviendo nuestro proceso en pequeñas partes que serían procesadas por tareas enlazadas.

Desde hace poco (SDK 1.4.0.) el límite de tiempo para las peticiones en segundo plano (tareas y cron) aumentó:  ¡de 30 segundos a 10 minutos! , por lo que,  la mayoría de los procesos antes mencionados podrían ejecutarse sin mayor problema.

Sin embargo, cuando lo que necesitamos es recorrer todas las entidades de un modelo y actualizarlas/operar con ellas, mapreduce nos aporta algunas ventajas:

  • Comodidad a la hora de definir un nuevo script (mapreduce handler).
  • Eficiencia: mapreduce se ejecuta atutomáticamente con un número configurable de shards.
  • Escalabilidad infinita (aquí no tenemos la limitación de 10 minutos en caso de tener millones de entidades).

El problema es que los mapreduce están pensados para lanzarse manualmente desde la consola de administración de mapreduce (url_de_mi_aplicacion/mapreduce) pero en ocasiones nos gustaría poder lanzarlos programáticamente. Estudiando un poco el funcionamiento de esta consola y el código de mapreduce es sencillo modificarlo para conseguir nuestro propósito. Por ejemplo, lanzar un mapreduce desde un cron que se ejecute todos los lunes a la misma hora.

Lo primero es definir la entrada en nuestro cron.yaml, por ejemplo:

cron:
- description: purge tokens
 url: /admin/cron/execute_mapreduce
 schedule: every monday 00:00
 timezone: Europe/Madrid

Nosotros usamos Django, así que definiríamos una entrada en urls.py para mapear la url definida en el cron con nuestro método python encargado de llamar al mapreduce:

(r'^admin/cron/execute_mapreduce$', 'ruta_del_modulo.run_map_reduce'),

Y ahora viene la duda. ¿Cómo llamamos al mapreduce desde dicho método si sólo se puede a través de la consola? Esstudiando el comportamiento de la consola con una herramienta tipo firebug, vemos que cuando se pulsa en el botón “run” se ejecuta una llamada AJAX con una serie de parámetros. Si replicamos dicha llamada en nuestro código ya tenemos el mismo comportamiento.

Si  la definición de nuestro mapreduce (en el mapreduce.yaml) es ésta:

- name: execute_something_from_cron
 mapper:
 input_reader: mapreduce.input_readers.DatastoreInputReader
 handler: map_reduce_handlers.executed_from_cron.execute_something_from_cron
 params:
 - name: entity_kind
 default: points_of_sale.models.pos_model.PointOfSale

Entonces la llamada por código (en localhost) sería esta:

 form_fields = {
 "mapper_handler": "map_reduce_handlers.executed_from_cron.execute_something_from_cron",
 "mapper_input_reader": "mapreduce.input_readers.DatastoreInputReader",
 "mapper_params.entity_kind": "points_of_sale.models.pos_model.PointOfSale",
 "name": "calculate_aggregates_of_aggregates"
 }

 form_data = urllib.urlencode(form_fields)
 result = urlfetch.fetch(url="http://localhost:8080/mapreduce/command/start_job",
 payload=form_data,
 method=urlfetch.POST,
 headers={'Content-Type': 'application/x-www-form-urlencoded'},
 deadline=10)

Por último, para probar que todo funciona ya sólo queda ejecutar el cron. En local se puede hacer a través de la url:

http://localhost:8080/_ah/admin/cron

Pero cuidado, para poder ejecutarlo en local necesitamos un pequeño cambio. Lo normal es que mapreduce requiera permisos de administrador, lo cual, se indica en app.yaml:

- url: /mapreduce(/.*)?
 script: mapreduce/main.py
 login: admin  

Si ejecutamos el cron en la nube no hay problema porque tiene permisos de admin, sin embargo, el test local se ejecuta con permisos de usuario y, por ello, es necesario comentar la línea “login: admin” para las pruebas locales.

Ya parece que tenemos todos lo necesario, así que, lanzamos el test del cron desde el nagegador…obteniendo el siguiente resultado:

ApplicationError: 2 timed out

¿Por qué ocurre esto? La explicación se encuentra en el fichero base_handler.py de la libería mapreduce. Concretamente en el método _handle_wrapper:

def _handle_wrapper(self):
 if self.request.headers.get("X-Requested-With") != "XMLHttpRequest":
 logging.error(self.request.headers)
 logging.error("Got JSON request with no X-Requested-With header")
 self.response.set_status(
 403, message="Got JSON request with no X-Requested-With header")
 return

Como vemos, se están vetando explícitamente aquellas peticiones que no sean AJAX.

Afortunadamente, existe una solución muy fácil que evita cambiar el código de la librería mapreduce (sería peligroso porque en cualquier momento puede cambiar de versión o incorporarse por defecto sin necesidad de incluirla en el despliegue): se trata de añadir la cabecera XMLHttpRequest a nuestra petición:

headers={'Content-Type': 'application/x-www-form-urlencoded',
 'X-Requested-With': "XMLHttpRequest"},

Y ahora ya podemos ejecutar el mapreduce desde el cron y consultar la consola/logs cuando queramos ver su resultado. También se podrían lanzar peticiones (al igual que lo hace la consola) para comprobar que todo ha ido bien por código, pero eso ya es otra historia…

Utilizando gzip en las comunicaciones

Como ya sabéis hace dos semanas estuvimos de ranatón. En mi equipo, uno de nuestros objetivos era optimizar las comunicaciones entre el servidor y el cliente. El objetivo era optimizar todo lo que no habíamos optimizado todavía, ya que, en Colombia (donde están la mayoría de nuestros usuarios), las tarifas de datos aún son muy restrictivas en cuanto a datos enviados/recibidos.

La comunicación actualmente se estaba realizando usando Json y en texto plano.

Tal como este:

{"D":1,"sale_code":"301","type":"sale",
"local_created":"2011-01-05 09:26:57.042","balance_sheet_code":"501",
"customer_code":"-1","products":{"generic1":{"Q":1,"P":300,"F":1}},
"final_price":300,"paid":1,"cash_operation_code":"401"}

En este ejemplo estabamos enviando una operación de tipo venta. Como primera optimización, decidimos acortar los nombres de las propiedades. De este modo nos quedó:

{"D":1,"SC":"301","T":"sale","LC":"2011-01-05 09:26:57.042",
"BS":"501","CC":"-1","PR":{"generic1":{"Q":1,"P":300,"F":1}},
"FP":300,"PD":1,"CO":"401"}

Así conseguimos reducir bastante el tráfico de datos, pero necesitabamos reducirlo todavía más, así que tomamos la decisión de utilizar la compresión gzip, y comprimir todo el texto enviado y recibido por el cliente. Y después de comprimirlo lo convertimos en una string con BASE64, de este modo formateabamos los datos sin retornos de carro y todo quedaba, si cabe, mas uniforme:

H4sIAAAAAAAAADWOzQoCMQyEX0Xm3JW03SrmaMWT
aHU9KR5EiwiyygpeSt/dtCIE8s3kN+H5AuMwrK3toXCNH5GkBT
fgY8ICrBU6L66t9l7ofX5EwVVxDWndkIQb0YzNhN10TK2R8ryT
sqtDvnQ2hcIOnHCLfRzuF114Wy8EsCVSWIrKWfLfCL8PvLyDVpb
lU/4C8o/at7UAAAA=

No es el caso más optimo para comprimir, pero hemos calculado que enviamos un 50% menos de datos al enviar la información comprimida. Para poder medir esto utilizamos WireShark con un filtro tal como este (Gracias Jose):

(ip.src == 192.168.1.9  &&
           ip.dst == 192.168.1.12 &&
           tcp.port == 8080)

Para poder realizar la compresión en el cliente Android utilizamos la clase GZIPOutputStream de este modo:

public static byte[] zipStringToBytes( String input  ) throws IOException
  {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    BufferedOutputStream bufos = new BufferedOutputStream(new GZIPOutputStream(bos));
    bufos.write( input.getBytes() );
    bufos.close();
    byte[] retval= bos.toByteArray();
    bos.close();
    return retval;
  }

Y en la parte del servidor, nosotros utilizamos Google App Engine:

def decode_client_data(request_object):
    if(request_object.META.get('HTTP_CONTENT_ENCODING',None)!=None):
        gzip_string = base64.standard_b64decode(str(request_object.raw_post_data))
        url_file_handle=StringIO(gzip_string)
        gzip_file_handle = gzip.GzipFile(fileobj=url_file_handle)
        decompressed_data = gzip_file_handle.read()
        gzip_file_handle.close()
        return decompressed_data
    else:
        return request_object.raw_post_data

Gracias a la integración continua podemos asegurarnos de que ningún cliente trabajará contra una servidor que no acepte gzip, pero para evitarnos problemas y poder sacar versiones a producción del servidor sin tener que hacerlo del cliente, decidimos programar el servidor para que acepte operaciones sin comprimir y en versión de nombres de propiedades largas.

Para que el servidor nos envie la información comprimida en gzip es mucho más fácil, puesto que lo hace de manera automática siempre y cuando en las peticiones le digamos que nuestro cliente acepta compresión gzip. Nosotros lo hemos hecho de este modo:

HttpPost httppost = connectPOST(url);
   httppost.setHeader("Accept-Encoding", "gzip");
   httppost.setHeader("Content-Encoding", "GZIP");
   httppost.setHeader("User-Agent", "gzip");

Hay que tener muy presente que el gzip automático por parte de GAE no funciona en local.

Si tenéis alguna duda o sugerencia no dudéis en comentarla.

Búsquedas en Google App Engine

Ahí van algunos consejos para los que trabajen con Google App Engine y estén interesados en hacer búsquedas en el datastore que incluyan búsquedas totales o parciales de texto en campos.

Para aquellos que hemos trabajado con bases de datos relacionales, puede costar un poco cambiar el chip cuando se trabajan con sentencias usando GQL. ¿Que podemos hacer si queremos el clásico elemento de SQL “LIKE” para hacer una búsqueda parcial de un texto?, nos encontraremos que GQL no permite ese tipo de sentencias, por la eficiencia y la escalabilidad del sistema (que es lo más prioritario en GAE).

Aunque hay librerías para realizar búsquedas (no las he probado, así que poco puedo decir), vayamos por algo simple, sin librerías ni pifostios.

Hacer búsquedas de registros cuyo campo A empiece por “XXX”.

Entidad: Clientes

Campo: Nombre

Empieza por: “Jua”

query = db.Query(Clientes)
query.filter('nombre >=', 'Jua').filter('nombre <', 'Jua' + u'ufffd')

El valor u'ufffd' es como decir "el elemento unicode más grande que existe", en definitiva hacemos algo así como: "Búscame todos los registros de clientes cuyo nombre esté entre 'Jua' y 'JuaINFINITO' (o casi) "

En el caso que queramos hacer búsquedas de la forma "registros cuyo campo termine en"... la cosa está más complicada. Para ello habría que crearse un campo especial en el datastore que almacene el string a buscar volteado... y realizar la misma sentencia más arriba descrita.

Para realizar búsquedas de textos que coincidan exactamente la cosa es más simple, con usar "=" en nuestras queries.

query = db.Query(Clientes)
query.filter('nombre =', 'Juan').

Hint! OJO! Que si ponemos 'nombre=' posiblemente no nos aparezca ningún resultado. Dejad un espacio entre el campo a buscar y el operador.

Otra cosa a tener en cuenta a la hora de almacenar la información en el datastore, es la siguiente:

¿Queremos realizar búsquedas que no sean case-sensitive? En ese caso es recomendable que los campos guardados sean todos lowercase o uppercase. De esta forma, nos aseguramos que si buscamos registros que empiecen por las letras "jua" o "Jua" nos devolverán los mismos resultados. La variable de búsqueda puede ser tratada en python (o java) para convertirla a mayúsculas o minúsculas completamente antes de compararla con la información del datastore.

En el caso que ya tengamos la información almacenada tal y como llegan de los formularios/sincronizaciones/dondesea podemos hacer dos cosas:

  • Convertir todos esos textos a mayúsculas/minúsculas dentro del datastore y luego tener en cuenta a nivel de programación que dichos campos están en mayúscula/minúscula y su presentación requiera otro formato.
  • Crear nuevos campos alternativos donde se guarden copias de los campos a buscar en el formato que queramos:  name = "Juan"   name_search ="juan" y haremos las búsquedas tomando como referencia "name_search", aunque mostremos en la UI el campo "name".

Para hacer estas dos últimas tareas tenemos la herramienta mapreduce que nos puede ayudar enormemente a la hora de modificar el datastore y su información "dinámicamente", pero de esto, se hablará en otro post.

Todo esto está basado en la experiencia que poco a poco vamos adquiriendo en GAE, es posible que haya formas más eficientes y mejor de hacer las cosas. Siempre es bueno escuchar otras opiniones.

Eficiencia en Google App Engine: Appstats

Nuestra elección a la hora de subir datos a la nube fue Google App Engine, el servicio que permite ejecutar aplicaciones web en la infraestructura de Google. El desarrollo  en esta plataforma presenta ciertas características que obligan a cambiar el chip desde un primer momento y que nos fuerzan implícitamente a tener en cuenta la eficiencia y, sobretodo, la escalabilidad. Dicho esto, siempre hay momentos a lo largo del camino en los que uno se plantea revisar la eficiencia global de la aplicación para mejorar su tiempo de respuesta, reducir los consumos de cpu y, de paso, reducir la factura cobrada por Google.

La primera vez que nos enfrentamos a esta situación decidimos usar cProfile y pstats para estudiar los tiempos de nuestro código Python centrándonos en aspectos clásicos como optimización de búsquedas, mejora de la eficiencia en la manera de concatenar cadenas, reducción del número de iteraciones en un bucle…la verdad es que de poco nos sirvió, ya que, veíamos que la mayor parte del tiempo de proceso se gastaba en algo que en el profile se indicaba con líneas como esta:

_apphosting_runtime___python__apiproxy.Wait

Es decir, que la mayor parte del tiempo nuestra aplicación estaba esperando a que se completase el proceso realizado por una llamada a alguna API de Google.

Estudiando en detalle el lugar del código en el que aparecen estas líneas, se puede averiguar a qué API se están refiriendo pero, por suerte, todo ese trabajo dejó de ser necesario gracias al descubrimiento de Appstats:

Appstats es una herramienta creada por  Guido van Rossum (creador de Python y actual empleado de Google) que forma parte del SDK y nos permite ver en detalle en qué emplea realmente el tiempo nuestra aplicación mediante el estudio de las RPC, es decir, las llamadas remotas que nuestra aplicación hace a las distintas API’s de Google. Su instalación es muy sencilla y podemos instalarla incluso en producción porque consume muy pocos recursos.

En  el siguiente video, el propio Guido, nos explica las bondades de esta herramienta indispensable:

En el video se muestra cómo instalar y usar la herramienta, una breve explicación de su funcionamiento y consumo, posibilidades de configuración y algunos ejemplos de uso como, por ejemplo, detección de ciertos patrones de ineficiencia comunes a muchas aplicaciones : Patterns of Doom!