Escalando Rails con NeverBlock y DBSlayer

Actualmente se está hablando mucho de los problemas de escalabilidad de Rails, dado el caso de Twitter en primera instancia, y las posibles soluciones. Los detractores afirman que Rails es un corset en este sentido y que su arquitectura interna es bastante inflexible, si bien el principal problema no es del framework sino del lenguaje en si, ya que MRI 1.8.x no incorpora la gestión eficiente de multithreading.

Mientras muchos esperan ansiosos la salida de fase beta del fabuloso y prometedor Maglev, que como un mesías quitará el dolor de la lentitud del mundo Ruby y guiará a la comunidad al top ten de rendimiento de aplicaciones, recientemente han surgido dos soluciones que prometen tirar por la borda los malos comentarios, otorgando excelente rendimiento y escalabilidad en nuestras aplicaciones. Este es el caso de Neverblock y de una reimplementación sobre Cherokee de DBSlayer.

Neverblock es una librería Ruby que permite a los desarrolladores escribir código concurrente y sin bloqueos de manera transparente. Esto significa que uno puede mantener la forma de codificar de forma tradicional obteniendo los beneficiones de las operaciones IO non blocking.

Tradicionalmente, una aplicación Ruby realizaría un bloqueo cada vez que realizace un pedido a una operación IO. Esto puede ser resuelto corriendo código en múltiples hilos pero esto tiene costos, principalmente:

  1. La alta sobrecarga de hilos en tiempo de ejecución.
  2. Las complejidades de la seguridad de los hilos y los requerimientos de sincronización.

Otro intento fue usar un modelo basado en evento para lograr la concurrencia. Mientras estos se ejecutaron bien sufrieron otros problemas, principalmente:

  1. El retorcido modelo de flujo de programa donde todo el código esta separado por callbacks.
  2. La complejidad de mapear una aplicación existente para calzarla en el modelo en cuestion.

NeverBlock toma ventaja de estas deficiencias proveyendo un sistema que tiene:

  1. Mucha menos sobrecarga de CPU y memoria.
  2. Complejidades de sincronización muy reducidas.
  3. Un modelo de flujo de programa normal.
  4. La habilidad de adaptarse a aplicaciones existentes con mínimo esfuerzo.

Neveblock se construye sobre dos conceptos principales, los Fibers de Ruby 1.9,y los non-blocking IO. Se ubica sobre estos para proveer a los desarrolladores librerias de acceso IO que pueden esconder totalmente las complejidades de los modelos basados en eventos y agregar soporte de concurrencia a las aplicaciones.

como benchmark se midió el tiempo de ejecución de varias combinaciones de consultas rápidas y lentas. Para simular las consultas rápidas se usó "select 1" y para las consultas lentas "select sleep(1)"

Se categorizaron las combinaciones como sigue:

Very light work load : 005 slow queries, 1000 fast queries
Light work load : 010 slow queries, 1000 fast queries
Moderate work load : 020 slow queries, 1000 fast queries
Heavy work load : 050 slow queries, 1000 fast queries
Very heavy work load : 100 slow queries, 1000 fast queries

 

Se testearon una conexión de bloqueo simple (es el caso normal de cualquier aplicación Rails) contra un pool neverblock de 12 conexiones.

Tiempo de ejecución de consultas, Bloqueo versus NeverBlock (menos es mejor)

 

Balanceo asíncrono de MySql con HTTP+JSON

Ilya Grigorik en su blog comenta:

"Teniendo en cuenta que uno de los grandes cuellos de botella de las aplicaciones Rails es el acceso a las DB, se han buscado soluciones alternativas a este problema. El conflicto es que los onerosos pedidos dinámicos atraviesan el caché y requieren una consulta a DB con bloqueo, la cual tiene efectos colaterales desafortunados al bloquear tanto  los recursos de la aplicación como del servidor. Sería bueno si pudiésemos obtener la informacion de modo asincrónico, ¿no?.

"A traves de los años, hubo muchos intentos de desarrollar drivers de databases asíncronas, con Asymy
como la mas reciente variante Ruby por Thomas Ptacek. Sin embargo, cada vez que uno inicia otro de estos proyectos, debemos preguntarnos: ¿Porqué duplicar esfuerzos? Despues de todo hemos batallado y testeado un protocolo que es familiar para todos nosotros: HTTP. Por esta razón, cuando vi el anuncio, y mas tarde asistí a la presentación de
DBSlayer en MySQL Conf '08, supe que los chicos de NYT  estaban en algo grande."

En vez de elegir un lenguaje específico o plataforma, DBSlayer habla y entiende JSON - algo que cualguier otro lenguaje puede producir y consumir fácilmente. Con soporte a fallas, pooling de conexiones, distribucion de esclavos round-robin, y una interface MySQL, me sorprende que no haya recibido mayor atención. Realicemos una prueba:

# connect to localhost database, expose DBSlayer on port 81
dbslayer -s localhost -c mysql.conf -p 81 -l db.log

# Check if it's live
curl http://192.168.0.198:81/stats

{"current_time" : 1218011845 , "hits" : [8 , 1] , "slices" :
[1218011724 , 1218011784] , "start_time" : 1218011724 ,
"total_requests" : 9}

Ahora que DBSlayer esta corriendo, acceder a la DB es tan simple como realizar una llamada HTTP:

require 'rubygems'
require 'net/http'
require 'json'
require 'cgi'
# instead of querying the database, send a HTTP call to DBSlayer
def http_request(query)
Net::HTTP.start('localhost', 81) { |http|
req = Net::HTTP::Get.new('/db?'+ CGI.escape(query.to_json))
response = http.request(req)
return JSON.parse(response.body)
}
end
 
# select the database. Output:
# >> {"HOST_INFO" : "Localhost via UNIX socket" , "RESULT" : {"SUCCESS" : true} , "SERVER" : "localhost"}
http_request({'SQL' => 'use dbslayer'})
 
# issue the real sql query! Output:
# >> ["bobblehead", 5]
# >> ["toy", 2]
# >> ["gadget", 3]
http_request({'SQL' => 'select * from widgets'})['RESULT']['ROWS'].each do |row|
p row
end

Por otra parte, es posible mejorar aún más el rendimiento de DBSlayer utilizando un adaptador que corre sobre Cherokee.

Del blog de Alvaro Lopez Ortega leemos:

El benchmark consistió de 50.000 requests con 50 hilos concurrentes. Cada uno de ellos enviando una consulta muy básica al mismo servidor MySQL. Los pedidos enviados tanto a DBSlayer como a Cherokee fueron sutilmente diferentes debido a una decisión que tomé. Enviamos la consulta SQL como el cuerpo de un POST en vez de encodearla en un request, y las opciones son enviadas como encabezados HTTP en vez de encodearlos con JSON y reencoderlos como parte del request:

Este fue el request a DBSlayer:

GET /db?%7B%22SQL%22:%22SELECT%20*%20FROM%20example;%22%7D HTTP/1.1
User-Agent: DBSlayer-tester/0.1
Host: localhost:9090
Connection: Keep-Alive
Accept: */*

Y este otro a Cherokee + handler_dbslayer:

POST / HTTP/1.1
User-Agent: DBSlayer-tester/0.1
Host: localhost:9999
Content-Length: 22
Connection: Keep-Alive
Accept: */*
X-Beautify: 0
SELECT * FROM example;

Ve los resultados!

 

Le tomó a Cherokee 2.4 segundos responder todos los requests (4,186
reqs/sec), mientras que DBSlayer necesitó 8.01 segundos para hacer el mismo trabajo
(1,255 reqs/sec).

Sin embargo, las cosas no terminan aquí. Tan pronto como el Cherokee's handler_dbslayer fue implementado como un módulo de respuesta ('handler' en el proyecto) este pudo tomar ventaja del resto de las características del servidor. Por ejemplo, usted puede configurarlo para usar cualquiero módulo de balance de carga que ya poseeemos, de modo que pueda dividir la carga entre un número de servidores MySQL.

A la hora de escribir este artículo, no he tenido noticia respecto de si a alguien se le ocurrió fusionar estas dos tecnologías. NeverBlock es muy interesante para desarrollar ya que nos permite seguir escribiendo nuestras aplicaciones al mejor estilo Ruby sin tener que deformar el código para realizar SysRequest para logra Threading eficiente, y el balanceo asincrónico de MySQL mediante HTTP+JSON tanto con DBSlayer como conCherokee abren nuevas puertas al ansiado rendimiento. Pero no todo es color de rosas, ya que NeverBlock utiliza Ruby 1.9 el cual puede ser muy  insidioso a la hora de reimplementar nuestras aplicaciones basadas en 1.8, ya que tiene varias modificaciones de funcionamiento y sintaxis importantes. Por otra parte, para lograr el máximo rendimiento en balanceo asíncrono es necesario experimentar con Cherokee, lo cual rompe con el estandar Apache/mongrel/passenger/nginx, pero esto ya es cuestión de gustos. Todas estas contras quizás sean el precio a pagar en pos del rendimiento y la escalabilidad que deseamos.

Otros lenguajes

Excelente Post, me quede enganchado con DBSlayer, sobre cherokee ya habia leido estos dias, donde muestran un benchmark que los muestra mucho mas rapido que cualquiera de los web servers mas conocidos en el mundo open source (seria barbara una comparacion con IIS).

Cito de la web de DBSlayer:
"As long as your programming language has HTTP request and JSON parsing libraries, calling the DBSlayer should be rather straightforward."

No soy muy adepto a Ruby, no te voy a mentir, pero si esta misma tecnologia se puede aplicar a otros lenguajes, no pondria esto, otra vez, la balanza a favor de los lenguajes/frameworks actualmente mas rapidos? La respuesta esta en Neverblock.

Me encanto el post. Saludos.