Aplicaciones escalables,distribuidas y rapidas con ZeroMQ
Zeromq
En la actualidad una aplicación no es un modulo aislado que sirve de punto de entrada o salida de datos, este se interconecta con otras aplicaciones, ya sea para recibir o enviar información, como parte de un flujo de información dentro de empresas o negocios. Existen muchas formas de interconectar aplicaciones, por lo general usando protocolos como HTTP, UDP, TCP, Websockets, etcétera; en ocasiones no solo se requiere comunicar entre distintas aplicaciones, si no modulos que se encuentran en distintos procesos hospedados en un servidor o una comunicación interproceso en la aplicación.
ZeroMQ nace en 2007 como una respuesta a estas necesidades. Su creadores lo definen como sockets con esteroides o mailboxes con enrutamiento y sistema de mensajería que tiene un rendimiento usando hardware limitado, funcionando de manera asíncrona y en multihilos, con latencia muy baja y escalable, además que no solo soportar el patrón de intercambio de mensajes de publicador-suscriptor, si no poder implementar otros mecanismos de comunicación entre los nodos involucrados.
ZeroMQ en resumen es una librería de comunicación que puede agregarse a tu aplicación y actuar también como una herramienta de concurrencia. Proporciona un mecanismo de sockets para el intercambio de mensajes entre procesos, interproceso, TCP o multicast. Es posible conectar estos sockets en patrones N-to-N como pueden ser fanout, publisher-subscriber, task distribution, request-reply o algun patrón personalizado. Es increíblemente rápido,por lo cual puede ser base para una solución cluster, es asíncrono y escalable.
Porque es popular o por que usar zeromq:
- Es código abierto y libre, esta soportado por una comunidad activa y grande, mas de 50 voluntarios contribuyen al código fuente, fuera de la compañía Imatix.
- Fue desarrollado con una API ultrasimple y basada en sockets BSD, la API es familiar, fácil de aprender e idéntica en todos los lenguajes de programación.
- Implementa patrones de mensajería como pub-sub, distribución de cargas de trabajo y request-response, por lo cual es sencillo resolver de conexión entre aplicaciones
- Funciona con una gran cantidad de lenguajes de programación (C, C++, Java, Ruby, Python, Javascript, C#, Haskell, Erlang, PHP, etc) y funciona en todas los sistemas operativos (Linux, Windows, Solaris, OSX,etc) por lo cual conectar aplicaciones o piezas de aplicaciones es posible.
- Proporciona un modelo consistente para las API en todos los lenguajes, ZeroMQ se puede aprender con un lenguaje y aplicarlo en otro sin problemas.
- Es LGPL por lo cual puede usarse en soluciones de código cerrado, aplicaciones gratuitas u open source.
- Es una librería embedida, por lo cual no existen agentes que iniciar o administrar, menos piezas que controlar y menos probabilidades que algo falle.
- El uso de CPU es muy bajo así como el de la memoria.
¿Que necesitamos para comenzar?
Un lenguaje de programación que dominemos o nos guste
- Las librerías de Zeromq que pueden descargar de esta url: http://www.zeromq.org/area:download (es preferible utilizar la versión estable, ya que los binding por lo general funcionan con esa versión y no con las últimas versiones beta)
- Compilar las librerías en el sistema operativo de su preferencia
- Instalar el binding para su lenguaje de programación, la lista de bindings y como instalarlos pueden consultarla en esta url: http://www.zeromq.org/bindings:_start
En el caso de los siguientes ejemplos, vamos a utilizar ruby en un entorno Linux, por su facilidad de instalación y para efectos de explicar cada uno de los puntos. Para lo cual se requiere tener instalado lo siguiente:
- Ruby 1.9.1 instalado en el equipo,se puede instalar con sistema de administración de paquetes de la distribución o se puede descargar de:
- La librería de ZeroMQ compilada e instalada en tu entorno Linux, se puede descargar el código fuente de:
- La gema ZMQ que se puede instalar usando el comando: gem install zmq
Show me the code!!!
Vamos a comenzar a usar ZeroMQ implementando un patrón Request-Response, es decir envió petición a una aplicación y esta debe responder. Este patrón de comunicación se ilustra de la siguiente manera:
Donde yo solicito a una aplicación la hora actual, la aplicación que envía la petición la podemos definir con el siguiente código:
require "zmq"
context =ZMQ::Context.new(1)
p "app de request"
envio=context.socket(ZMQ::REQ)
envio.connect("tcp://127.0.0.1:9000")
for i in 1..10 do
envio.send(i.to_s() +".-Que horas son?")
respuesta = envio.recv
p respuesta
p "enviado"
end
p "Termino el envio de mensajes"
En el ejemplo 1 se ve como invocamos la librería de ZeroMQ. Después se crea un objeto de contexto con el cual podemos crear nuestro socket. En este caso vamos a utilizar un socket de request(ZMQ::REQ) y nos conectamos(usando el método connect) a un puerto libre en un equipo por medio de tcp, en este caso al equipo donde estará el servidor que nos va a responder (tcp:// 127.0.0.1:9000).
A continuación se crea un ciclo donde realizamos una serie de envíos de las peticiones usando el método send. El método send nos permite enviar información ya sea una cadena de texto o un arreglo de bytes y es inmediatamente enviado al servidor que va a responder nuestras peticiones.
Como estamos usando un patrón Request-Response, cada petición espera una respuesta, por lo cual inmediatamente que se envía una petición se espera una respuesta usando el método recv, el método recv recibe la información que envía el cliente y la devuelve ya sea como una cadena de texto o arreglo de bytes.
Para poder responder a las peticiones, necesitamos un servidor por lo cual utilizaremos el siguiente código:
require "zmq"
context =ZMQ::Context.new(1)
p "app de response"
respuestas= context.socket(ZMQ::REP)
respuestas.bind("tcp://127.0.0.1:9000")
loop do
request = respuestas.recv
p request
respuestas.send(Time.now.to_s())
p "Respuesta enviada"
end
En el ejemplo 2 observamos cómo se crea el contexto de ZeroMQ y posteriormente se crea el socket, a diferencia de la aplicación de peticiones, en este caso se usa un socket de respuestas(ZMQ::REP) y se asocia a una ip y puerto del equipo (tcp:// 127.0.0.1:9000).
Creamos un ciclo donde se van a estar recibiendo las respuestas utilizando el método recv de nuestro socket, y por medio del socket enviamos la respuesta usando el método send, se esa manera implementamos un mecanismo de comunicación entre aplicaciones.
Iniciando con el siguiente comando el ejemplo 1:
ruby request.rb
Y con el siguiente comando en otra ventana de la línea de comandos el ejemplo 2:
ruby response.rb
El resultado de estos dos comandos es similar a esto:
|
Salida de Request.rb |
Salida de Response.rb |
|
"app de request" "2012-01-22 02:11:48 +0000" "enviado" "2012-01-22 02:11:48 +0000" "enviado" "2012-01-22 02:11:48 +0000" "enviado" "2012-01-22 02:11:48 +0000" "enviado" "2012-01-22 02:11:48 +0000" "enviado" "Termino el envio de mensajes" |
"app de response" "1.-Que horas son?" "Respuesta enviada" "2.-Que horas son?" "Respuesta enviada" "3.-Que horas son?" "Respuesta enviada" "4.-Que horas son?" "Respuesta enviada" "5.-Que horas son?" "Respuesta enviada" |
|
Nombre |
Compatible con |
Dirección |
Balanceo |
Patrón envio/recepción |
|
REQ |
REP |
Bidireccional |
Round-robin |
Envio,recepción,envio,recepción… |
|
REP |
REQ |
Bidireccional |
Fair-queued |
Recepcion,envio,recepción,envio |
|
DEALER |
ROUTER,REQ,REP |
Bidireccional |
Round-robin/ Fair-queued |
Sin restriccion |
|
ROUTER |
DEALER,REQ,REP |
Bidireccional |
Fair-queued |
Especial |
|
PUB |
SUB |
Unidireccional |
Fan out(envio a todos los nodos conectados) |
Envio solamente |
|
SUB |
PUB |
Unidireccional |
Fair-queued |
Recepcion solamente |
|
PUSH |
PULL |
Unidireccional |
Round-robin |
Envio solamente |
|
PULL |
PUSH |
Unidireccional |
Fair-queued |
Recepcion solamente |
|
PAIR |
PAIR |
Bidireccional |
No tiene |
Sin restricción pero solamente con un nodo a la vez |
Algunos son compatibles con otros y pueden usarse en combinación para crear patrones de distribución o balanceo de cargas, además de poder invertir el orden del servidor(binding) y de los clientes(connect) con lo cual cambiando solamente algunas partes de los códigos anteriores en cada uno de los scripts:
Request2.rb
require "zmq"
context =ZMQ::Context.new(1)
p "app de request"
envio=context.socket(ZMQ::REQ)
envio.bind("tcp://127.0.0.1:9000")
for i in 1..100 do
envio.send(i.to_s() +".-Que horas son?")
sleep(0.2)
respuesta = envio.recv
if envio.getsockopt(ZMQ::RCVMORE)
identClient=envio.recv
end
p "Enviada por:" + identClient +" Resp:" +respuesta
p "enviado"
end
p "Termino el envio de mensajes"
Response2.rb
require "zmq"
context =ZMQ::Context.new(1)
identidad=ARGV[0]
p "app de response:" +identidad
respuestas= context.socket(ZMQ::REP)
respuestas.connect("tcp://127.0.0.1:9000")
loop do
request = respuestas.recv
p request respuestas.send(Time.now.to_s(),ZMQ::SNDMORE)
respuestas.send(identidad)
p "Respuesta enviada"
end
Al invertir la manera en que nos asociamos a un puerto con nuestro sockets, podemos cambiar la funcionalidad de nuestra aplicación, en este caso vamos a crear una aplicación que hace peticiones a una serie de aplicaciones que van a recibir las tareas de manera balanceada, es decir se van a repartir una por una cada una de las peticiones de manera ordenada y equitativa.
Otro elemento que agregamos es la identidad. En este caso la aplicación que envía las peticiones, espera una repuesta que se compone de varias partes, ZeroMQ nos permite dividir un mensaje en partes, para saber si todavía quedan partes por procesar durante la recepción de un mensaje, utilizamos el método getsockopt y le mandamos de parámetro la propiedad ZMQ::RCVMORE, de esa manera si existen más partes del mensaje, devuelve un valor verdadero, y debemos seguir recibiendo datos con el método recv, en caso contrario significa que ya no hay mas partes que forman parte de ese mensaje y debemos esperar a que llegue un nuevo mensaje.
Del lado de la aplicación de respuestas, queremos enviar la identidad de nuestra aplicación y la respuesta a la petición que nos hicieron, por lo cual dividimos el mensaje en dos, enviamos primero una respuesta con el método send, pero agregamos un parámetro ZMQ::SNDMORE para que ZeroMQ le indique al receptor que está enviando una serie de partes que integran un mensaje. Cuando queremos terminar la serie de partes del mensaje, solo usamos el método send sin ningún parámetro, esto cierra la serie de partes de un mensaje.
Si iniciamos con el siguiente comando el ejemplo 3:
ruby request2.rb
Y después iniciamos el ejemplo 4 varias veces en distintas ventanas de la línea de comandos, cambiando el parámetro que enviamos, que es el nombre del nodo:
ruby response2.rb nodo1
ruby response2.rb nodo2
ruby response2.rb nodo3
Lo anterior nos muestra un escenario de balanceo de cargas, donde una aplicación reparte mensajes por medio de round-robin a los nodos que se conectan a ella, en una distribución como la siguiente:
Donde cada uno de los mensajes es repartido a cada uno de los nodos que están conectados, una característica interesante de este balanceo de cargas, es que pueden agregarse más nodos simplemente con conectarse al socket, no se requiere realizar algún otro tipo de proceso adicional en el servidor, por lo cual es relativamente fácil escalar un servicio o repartir tareas entre uno o mas nodos.
Derivado del caso anterior, observamos que nos da una salida en la línea de comandos similar a la siguiente:
|
Salida de Request2.rb |
Salida de Response2.rb nodo1 |
Salida de Response2.rb nodo2 |
Salida de Response2.rb nodo3 |
|
"app de request" "Enviada por:test01 Resp:2012-01-22 18:25:26 +0000" "enviado" "Enviada por:test02 Resp:2012-01-22 18:25:26 +0000" "enviado" "Enviada por:test03 Resp:2012-01-22 18:25:26 +0000" "enviado" "Enviada por:test01 Resp:2012-01-22 18:25:26 +0000" "enviado" "Enviada por:test02 Resp:2012-01-22 18:25:27 +0000" "enviado" "Enviada por:test03 Resp:2012-01-22 18:25:27 +0000" "enviado" "Enviada por:test01 Resp:2012-01-22 18:25:27 +0000" "enviado" "Enviada por:test02 Resp:2012-01-22 18:25:27 +0000" "enviado" "Enviada por:test03 Resp:2012-01-22 18:25:27 +0000" "enviado" "Termino el envio de mensajes" |
"app de response" "1.-Que horas son?" "Respuesta enviada" "4.-Que horas son?" "Respuesta enviada" "7.-Que horas son?" "Respuesta enviada" |
"app de response" "2.-Que horas son?" "Respuesta enviada" "5.-Que horas son?" "Respuesta enviada" "8.-Que horas son?" "Respuesta enviada" |
"app de response" "3.-Que horas son?" "Respuesta enviada" "6.-Que horas son?" "Respuesta enviada" "9.-Que horas son?" "Respuesta enviada" |
- Asignación de tareas de procesamiento complejas a varios nodos en distintos equipo y realizar procesamiento en paralelo.
- Comunicación entre procesos, el intercambio de mensajes es muy rápido y más efectivo que otros protocolos, además utilizando JSON o protocol buffers como serialización, se tiene una muy buena solución de crear medios de comunicación entre tecnologías en distintos lenguajes.
- Sistemas de comunicación de nodos descentralizado basados en directorios, puede crearse un servicio que tenga la lista de los nodos en la red y estos pueden establecer comunicación directa sin pasar por el servicio central.
- Monitoreo por heartbeat, por tener poca latencia y ser muy rápido, se puede implementar que distintas aplicaciones reporten el estatus activo a una aplicación central utilizando un patrón REQUEST-RESPONSE como el anteriormente explicado y saber cuándo una aplicación esta fuera de línea (por qué no responde el hearbeat).
- Mensajes PUSH, es posible enviar mensajes a una serie de nodos escucha y que estos lo filtren por medio de su entidad, es decir solo enviar el mensaje a un nodo receptor de entre varios que estén conectado al servidor.
- Rutas de mensajes, es posible enviar mensajes y establecer rutas de pasos entre distintos nodos para crear proxies, repartidores o embudos de procesamientos, de esta manera tener una infraestructura de procesamiento robusta para un sistema.
¿Quienes usan ZeroMQ?
Algunas de las compañías que usan actualmente ZeroMQ como parte de la infraestructura de sus aplicaciones son:
- AT&T
- Cisco
- Electronic Arts
- NASA
- Weta digital
- Zynga
- Twitter Storm, un sistema de procesamiento complejo basado en eventos
- Microsoft
- Cern
- Spotify
Al mismo tiempo de cientos de proyectos de código abierto que pueden encontrar en internet, la comunidad de ZeroMQ ha crecido mucho en los últimos años porque es una herramienta que permite integrar cómputo distribuido sin agregar complejidad al código de nuestras aplicaciones.
Conclusión
La pregunta que uno puede hacerse, es porque usar ZeroMQ de mi esquema de comunicación actual entre mis aplicaciones, las razones por las que podría justificar un cambio de este tipo seria:
- Rendimiento, procesar miles de mensajes no implica grandes costos de procesamiento o memoria.
- Velocidad, En algunas pruebas realizadas de manera aislada es capaz de procesar 32,000 mensajes por segundo en un equipo bastante modesto, por lo cual con una infraestructura adecuada puede procesar en una medida de cientos de miles de mensajes por segundo.
- Sencillez, es muy sencillo entender cómo funciona el enlace entre los sockets y en que puerto operan, así como los patrones que existen para las implementaciones.
- Escalabilidad, una aplicación que se comunica con ZeroMQ puede ser ampliarse en tiempo real, agregando nodos, los mecanismos de balanceo de cargas y atención de la cola de mensajes son manejados por la librería y facilita la implementación.
Implementar ZeroMQ en una aplicación es bastante sencillo, además de ser compatible con una gran cantidad de plataformas de desarrollo, solo es encontrar el uso adecuado a la tecnología y que uno tenga ganas que comenzar a experimentar con cómputo distribuido en la empresa o proyectos personales, además de poder usarse en conjunto con otras tecnologías o como sistema de comunicación vertebral de la empresas y los puntos finales de consumo usen otras tecnologías como HTTP, TCP, Webservices, etc.
Con infraestructura en la nube y una creciente tendencia a ofrecer servicios empresariales que usan técnicas similares a la nube, es indispensable que las aplicaciones se desarrollen con mecanismos de escalabilidad y comunicación que permitan soportar de manera elástica cargas de procesamiento, hoy puede ser que nuestra aplicación atienda diez personas, pero el día de mañana puede ser que estemos atendiendo a miles de personas en distintos puntos del planeta, solo necesitamos el canal adecuado de comunicación y ZeroMQ viene a resolver ese problema.
Enlaces recomendados:
- Página principal de ZeroMQ: http://www.zeromq.org/
- La guía de ZeroMQ: http://zguide.zeromq.org/
- Codigos de los ejemplos mostrados en este articulo: https://github.com/majimenezp/EjemplosZeroMQ










