6 de Junio, 2010

Redis, NoSQL y esas hierbas (I)

Ya hablaba por aquí hace meses de NoSQL (en mi opinión, la forma corta de Not Only SQL), y en su momento me pareció bastante interesante, pero solo para los casos en los que ayuden a resolver el problema que tenemos entre manos.

Es decir, no creo que la idea de no usar un sistema de bases de datos relacional sea buena per se, a no ser que queramos experimentar con nueva tecnología (como ha sido mi caso). Hay problemas muy acotados para los que una solución NoSQL encajará muy bien, pero en general lo que ganamos con NoSQL es a costa de sacrificar la versatilidad de un sistema relacional.

Para mi proyecto he elegido Redis, que está escrito en C y no tiene grandes requerimientos para empezar a trabajar. Como cliente he usado redis-py, que expone el sencillo API de Redis de una forma adecuada (salvo algún detalle, no documentado además, que casi me vuelve loco :D, como intercambiar el orden de los parámetros en LREM).

Redis es un almacén clave/valor muy optimizado, que nos proporciona además algunos tipos de datos complejos. Por ejemplo, Redis nos da soporte para listas, conjuntos, conjuntos ordenados y hashes.

En el problema acotado de un gestor de contenidos para un blog, estas características extra nos serán de gran utilidad a la hora de diseñar cómo será el almacenamiento, pero ya hablaré del modelo de datos más adelante :P.

Lo que voy a comentar ahora son los distintos puntos fuertes de un almacenamiento NoSQL, después de la experiencia de escribir un pequeño gestor de contenidos.

Esquema de datos flexible

Esto no me ha resultado excesivamente útil, porque a fin de cuentas he hecho un análisis, un desarrollo, y cuando todo estaba acabado lo he puesto en producción.

Como no he hecho cambios sobre la marcha, en realidad la flexibilidad del esquema de datos no me ha supuesto una diferencia, sino al contrario: cuando añadía una nueva propiedad a un documento, tenía que tener cuidado con los documentos viejos que no disponían de esa propiedad.

En mi caso he utilizado JSON para serializar los distintos tipos de documentos (anotaciones, comentarios y categorías), y recuperarlos así cómodamente del almacenamiento, aunque podría haber sacado mucho más partido de Redis.

Conclusión: podemos cambiar el modelo sobre la marcha, lo que nos permite refactorizar fácilmente, pero como el modelo es flexible, hay que llevar mucho cuidado con cómo lo manejamos.

No hay relaciones

Esto es definitivamente un problema :), pero usando índices empleando los tipos de datos de Redis (como las listas ordenadas), es fácil de solucionar. A cambio de un poco más de complejidad, ganamos velocidad, porque en lugar de recurrir por ejemplo a un ORDER BY, los datos ya los tenemos ordenados como queremos de forma natural.

En el caso de una bitácora, su definición nos ayuda a entender qué necesitamos:

Una bitácora es una página web que consiste en una serie de artículos ordenados de forma cronológica inversa, de manera que el más reciente se muestra primero, seguido de los más antiguos.

Conclusión: no hay relaciones, pero en realidad cualquier sistema NoSQL nos dará algunas primitivas para implementar las relaciones que necesitamos para solucionar nuestro problema.

Escalabilidad y rendimiento

La parte de la escalabilidad la tengo pendiente, por ahora solo he implementado backups, pero en cuanto al rendimiento ya tengo algunos resultados.

Ahora mismo la base de datos de la bitácora ocupa en disco 3.4MB, y Redis en ejecución unos 7.4MB. No está mal, un consumo muy ajustado.

¿Qué rendimiento da Redis? Es difícil de decir, y mucho más complicado de demostrar, pero aún así he hecho algunas cutre-pruebas.

Es importante restaltar que la comparación es a nivel de aplicación, y no solo en almacenamiento. Las condiciones de la prueba han sido:

  Modelo anterior Modelo nuevo
Frontal Cherokee Cherokee
Aplicación A medida (PHP, sin framework) A medida (Python, con Tornado)
Interfaz FastCGI Proxy HTTP
Almacenamiento MySQL Redis

En ambos casos he atacado a mi portátil (1GB de RAM, 1.7GHz) con Apache benchmarking tool desde otra máquina de la red local (con potencia suficiente :P), empleando los siguientes parámetros:

$ ab -c 10 -t 10 http://servidor.lan:8080/

Además he tenido en cuenta:

  • Tornado es muy eficiente sirviendo páginas web, pero fue diseñado para servir páginas detrás de un servidor de páginas web y no sería seguro tenerlo expuesto directamente en producción, así que lo he puesto detrás de Cherokee. He hecho pruebas sin frontal, y tampoco ha supuesto una gran penalización.
  • En el caso de PHP he configurado el interfaz FastCGI sobre TCP, con PHP_FCGI_MAX_REQUESTS=250 y PHP_FCGI_CHILDREN=4 (que es como lo tenía en producción).
  • Para MySQL he configurado el query caché, y el acceso vía socket (en mi experiencia, es más rápido que con TCP). Las tablas van con MyISAM.
  • Las páginas no pesan exactamente lo mismo, por motivos evidentes: la aplicación es distinta, el resultado HTML también.

Las siguientes cifras hay que tomarlas como lo que son, y hay que llevar cuidado con las conclusiones:

Portada PHP + MySQL

Concurrency Level:      10
Time taken for tests:   10.026 seconds
Complete requests:      161
Failed requests:        0
Write errors:           0
Total transferred:      8456919 bytes
HTML transferred:       8431165 bytes
Requests per second:    16.06 [#/sec] (mean)
Time per request:       622.716 [ms] (mean)
Time per request:       62.272 [ms] (mean, across all concurrent requests)
Transfer rate:          823.75 [Kbytes/sec] received

Portada Python (Tornado) + Redis

Concurrency Level:      10
Time taken for tests:   10.127 seconds
Complete requests:      178
Failed requests:        0
Write errors:           0
Total transferred:      8027099 bytes
HTML transferred:       7993243 bytes
Requests per second:    17.58 [#/sec] (mean)
Time per request:       568.941 [ms] (mean)
Time per request:       56.894 [ms] (mean, across all concurrent requests)
Transfer rate:          774.05 [Kbytes/sec] received

Con estos resultados (actualizados 10/06/10), la nueva versión nos proporciona apenas un 8,64% de respuestas más por segundo.

Conclusión: tampoco hay tanta diferencia :D. Mi código en PHP está muy optimizado y, a la vez, es muy poco mantenible (sin capas), mientras que la nueva aplicación tiene un diseño mucho más limpio.

Es cierto que hemos dejado pendiente la escalabilidad, que es una parte muy importante de este capítulo.

Un API sencilla

La verdad es que me resulta muy cómodo trabajar con un ORM, pero en tal caso sospecho que la diferencia en el rendimiento sería mayor. Un ORM no generará SQL tan optimizado como mi código tan poco mantenible ;).

El API de Redis es lo suficientemente simple como para que no suponga un esfuerzo usarla, algo que es muy importante cuando hay que ser disciplinado para manejar un modelo de datos con esquema flexible:

import redis
import simplejson as json

r = redis.Redis(host='localhost', port=6379, db=0)

# recuperar comentario con ID 1, serializado con JSON
comentario = r.get('comment:1:store')
comentario = json.JSONDecoder().decode(comentario)

print comentario['author']
print comentario['body']

Tenemos muchos comandos para interactuar con los tipos de datos de Redis, aunque yo he usado solo unos pocos, los que he necesitado para mi diseño del almacenamiento.

Conclusión: un API sencilla es esencial, porque tendemos que trabajar con ella para hacer que nuestro almacenamiento sea consistente y cumpla con nuestros requisitos.

Siguiente entrega

Más o menos esto es todo lo que quería comentar hoy, en la próxima entrega explicaré cómo es el almacenamiento que he diseñado para gestionar la bitácora.

Actualización 10/06/10: las pruebas con AB

Esta es la segunda vez que publico los resultados, después de descubrir gracias al amigo MarcosBL que ab tiene un bug cuando lo ejecutamos con -w, que hace que el resultado sea absurdo.

Como no tengo experiencia con esta herramienta, no me había dado cuenta del problema. Gracias de nuevo a MarcosBL por su ayuda (porque alguien en Internet está equivocado :D).

Como extra, y ya que he repetido las pruebas, he lanzado ab activando los keep-alive, que está soportado en la configuración de Tornado, y los resultados son algo mejores:

Concurrency Level:      10
Time taken for tests:   10.016 seconds
Complete requests:      211
Failed requests:        0
Write errors:           0
Keep-Alive requests:    211
Total transferred:      9377262 bytes
HTML transferred:       9332319 bytes
Requests per second:    21.07 [#/sec] (mean)
Time per request:       474.686 [ms] (mean)
Time per request:       47.469 [ms] (mean, across all concurrent requests)
Transfer rate:          914.30 [Kbytes/sec] received

Esto ya es un 23,77% más que la versión en PHP, pero sigue sin ser una ventaja impresionante.

Anotación por Juan J. Martínez, clasificada en: nosql, redis, cherokee, blog.

Hay 4 comentarios

Gravatar

Deseando ver el modelo de almacenamiento del blog, supongo que despejará muchas dudas que me vienen a la cabeza.

por r0sk, en 2010-06-08 12:59:54

Gravatar

No sé si te puede ayudar, pero en mi experiencia simplejson es especialmente lento, te recomiendo que lo cambies por alguna otra implementación json de python.

Mirate este artículo: Choosing a Python JSON Translator

por Oriol Rius, en 2010-06-09 09:22:35

Gravatar

Muy interesante Oriol, gracias!

simplejson es el segundo en tiempos en esa comparativa, aunque si python-cjson es unas 20 veces más rápido en el decode, podría mejorar bastante los tiempos de respuesta del blog.

Haré algunas pruebas, aunque sea por curiosidad :D

por Juanjo, en 2010-06-09 09:35:44

Gravatar

Vaya, pues no… python-cjson esta abandonado y recomiendan precisamente usar simplejson :(

por Juanjo, en 2010-06-10 09:02:14

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: